DOCUMENTACIÓN PARA DEVELOPERS

Exchange API

Conecta una integración con las carpetas públicas de intercambio de Mana Stained: consulta cartas ofrecidas o buscadas, crea reservas temporales y consulta su estado.

Documentación pública Especificación OpenAPI

Inicio rápido y credenciales

La API usa JSON. Sustituye los valores de ejemplo por credenciales emitidas para tu integración y por los identificadores públicos de las cartas de la carpeta compartida.

Token de carpeta

El propietario comparte por separado la carpeta Offered o Wanted desde la aplicación. Envía el token en el encabezado X-Exchange-Share-Token; nunca lo incluyas en una URL.

Clave de integración

Un administrador de Mana Stained crea o rota la integración. La clave sin procesar se muestra una sola vez; guárdala en un gestor de secretos y envíala en X-Integration-Key.

Sesión del propietario

La consulta de estado puede usar la sesión autenticada del propietario, y solo el propietario puede cerrar una reserva. Usa el token de sesión Serverpod como Authorization: Bearer; las integraciones no pueden ejecutar PATCH.

Conectar Mana Stained con OAuth 2.0

Tu aplicación usa Authorization Code con PKCE. La clave de integración identifica la aplicación registrada; el token OAuth representa al usuario que concedió el acceso. La clave, los tokens y el verificador PKCE deben permanecer en el backend de tu aplicación.

Registra en el panel de administración la URL HTTPS exacta de retorno. Genera en el backend un valor state aleatorio y un verificador/desafío PKCE S256; después, redirige al usuario a esta página de autorización. Tras iniciar sesión y dar su consentimiento, Mana Stained devuelve un código de un solo uso y el state original a la URL registrada. Canjea el código en cinco minutos; no se puede reutilizar.

https://mana.javiprato.eu/app/oauth/authorize?response_type=code&client_id=<opaque-client-id>&redirect_uri=<url-encoded-registered-callback>&scope=exchange%3Aread%20exchange%3Areservations&state=<random-state>&code_challenge=<pkce-s256-challenge>&code_challenge_method=S256
Paso a paso desde tu aplicación
  1. Una sola vez: en la app de Mana Stained abre Ajustes → «Mis aplicaciones de desarrollador» y registra tu aplicación (nombre, URL, email de contacto y la URL HTTPS exacta de tu callback). Un administrador la revisa; cuando se apruebe, genera allí tu clave. Obtendrás un client_id público opaco (una cadena, no el ID numérico interno) y la clave de integración (se muestra una sola vez).
  2. Cuando el usuario elija “Conectar con Mana Stained”, genera en tu backend un state aleatorio y un par PKCE: code_verifier (de 43 a 128 caracteres entre A–Z, a–z, 0–9, “-”, “.”, “_” y “~”) y code_challenge = BASE64URL(SHA-256(code_verifier)) sin relleno “=”. Guarda state y code_verifier en tu servidor, asociados a la sesión de ese usuario.
  3. Redirige el navegador del usuario a la URL de autorización de arriba, con client_id, redirect_uri, scope, state, code_challenge y code_challenge_method=S256.
  4. Mana Stained pide iniciar sesión si hace falta y muestra la pantalla de consentimiento. Si el usuario acepta, el navegador vuelve a tu callback con ?code=…&state=…; si rechaza, con ?error=access_denied&state=….
  5. En tu callback, comprueba que state coincide con el guardado y que no hay error. Si no, descarta el intento.
  6. En menos de cinco minutos, tu backend envía POST /oauth/token con grant_type=authorization_code, code, el mismo redirect_uri, el code_verifier guardado y la cabecera X-Integration-Key. La respuesta incluye access_token, refresh_token, expires_in y scope.
  7. Guarda ambos tokens cifrados en tu backend, asociados a ese usuario; nunca los envíes al navegador. Borra el code_verifier y el state usados.
  8. Llama a la API enviando X-Integration-Key y Authorization: Bearer <access_token>. Puedes leer Offered y Wanted del usuario y gestionar las reservas que hayas creado.
  9. Cuando el access token caduque (15 minutos) o la API responda 401 invalid_user_authorization, envía POST /oauth/token con grant_type=refresh_token. Sustituye ambos tokens guardados por los nuevos; el refresh token anterior deja de valer. Si recibes invalid_grant, el usuario debe empezar de nuevo desde el paso 1.
  10. Para desconectar, llama a POST /oauth/revoke con el token y borra los tokens guardados. El usuario también puede revocar desde Ajustes; en ese caso el siguiente refresh fallará y deberá volver a conectar.
// Step 1 (pseudocode)
code_verifier  = base64url(random_bytes(32))            // 43 characters
code_challenge = base64url(sha256(code_verifier))       // no "=" padding
state          = base64url(random_bytes(16))
save(user_session, { state, code_verifier })
redirect(authorize_url(client_id, redirect_uri, scope, state, code_challenge))

// Step 4 (callback)
if query.error or query.state != load(user_session).state: abort()
tokens = POST /oauth/token(code, redirect_uri, code_verifier)  // step 5

Los permisos concedidos permiten leer Offered y Wanted y gestionar reservas. La integración solo puede completar, rechazar o cancelar reservas que ella misma haya creado; no puede editar binders ni cerrar reservas de otra integración. El usuario puede revocar el acceso en Ajustes.

Solicitud de token (ejemplo)
curl -X POST "$BASE_URL/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "X-Integration-Key: $INTEGRATION_KEY" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "code=$AUTHORIZATION_CODE" \
  --data-urlencode "redirect_uri=$REGISTERED_REDIRECT_URI" \
  --data-urlencode "code_verifier=$PKCE_CODE_VERIFIER"

The token response includes an access token (15 minutes) and a refresh token (30 days). Refresh tokens rotate on every use; send the new token to your backend storage and discard the old one. Reusing an old token fails.

curl -X POST "$BASE_URL/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "X-Integration-Key: $INTEGRATION_KEY" \
  --data-urlencode "grant_type=refresh_token" \
  --data-urlencode "refresh_token=$OAUTH_REFRESH_TOKEN"

To disconnect a user from your application, revoke its access or refresh token. The Mana Stained user can also revoke the connection from Settings.

curl -X POST "$BASE_URL/oauth/revoke" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "X-Integration-Key: $INTEGRATION_KEY" \
  --data-urlencode "token=$OAUTH_REFRESH_TOKEN"

URL base y convenciones

export BASE_URL="https://mana.javiprato.eu/api/v1/exchange"
export OFFERED_SHARE_TOKEN="<token-provided-by-owner>"
export WANTED_SHARE_TOKEN="<token-provided-by-owner>"
export INTEGRATION_KEY="<key-provided-by-admin>"
export OAUTH_ACCESS_TOKEN="<user-authorized-access-token>"
export OAUTH_REFRESH_TOKEN="<rotating-refresh-token>"

Los ejemplos usan curl y variables de entorno para no incrustar secretos en el código. Las respuestas y errores usan JSON. Los IDs itemId son UUID públicos de elementos del binder, no IDs internos de inventario.

1. Consultar carpetas compartidas

GET/offered

Obtiene las cartas ofrecidas y la cantidad disponible. La cantidad ya descuenta las copias retenidas por reservas pendientes; un elemento sin copias disponibles no aparece.

Autenticación

X-Exchange-Share-Token or X-Integration-Key + Authorization: Bearer

curl "$BASE_URL/offered" \
  -H "X-Integration-Key: $INTEGRATION_KEY" \
  -H "Authorization: Bearer $OAUTH_ACCESS_TOKEN"
Respuesta 201 (ejemplo)
{
  "type": "offered",
  "items": [{
    "itemId": "11111111-1111-4111-8111-111111111111",
    "scryfallId": "EXAMPLE_PRINTING_ID",
    "cardName": "Example Card",
    "setCode": "SET",
    "collectorNumber": "42",
    "quantity": 2,
    "finish": "nonfoil"
  }]
}

GET/wanted

Obtiene las impresiones concretas que el propietario busca. Los elementos incluyen identificadores públicos que se pueden enviar como itemId al solicitar una reserva.

Autenticación

X-Exchange-Share-Token or X-Integration-Key + Authorization: Bearer

curl "$BASE_URL/wanted" \
  -H "X-Integration-Key: $INTEGRATION_KEY" \
  -H "Authorization: Bearer $OAUTH_ACCESS_TOKEN"
Respuesta 200 (ejemplo)
{
  "type": "wanted",
  "items": [{
    "itemId": "22222222-2222-4222-8222-222222222222",
    "scryfallId": "EXAMPLE_WANTED_PRINTING",
    "cardName": "Another Example",
    "setCode": "SET",
    "collectorNumber": "7",
    "quantity": 1,
    "finish": "foil"
  }]
}

2. Crear y seguir una reserva

POST/reservations

Crea una solicitud pending para el propietario de la carpeta Offered. Cada línea debe referenciar una carta de esa carpeta (offeredItems) o de su carpeta Wanted (wantedItems).

Autenticación

X-Integration-Key + Authorization: Bearer (OAuth), or the legacy Offered share token + integration key

Solicitud (ejemplo)
curl -X POST "$BASE_URL/reservations" \
  -H "Content-Type: application/json" \
  -H "X-Integration-Key: $INTEGRATION_KEY" \
  -H "Authorization: Bearer $OAUTH_ACCESS_TOKEN" \
  -d '{
    "requesterEmail": "trader@example.com",
    "offeredItems": [{
      "itemId": "11111111-1111-4111-8111-111111111111",
      "quantity": 1
    }],
    "wantedItems": [{
      "itemId": "22222222-2222-4222-8222-222222222222",
      "quantity": 1
    }],
    "expiresInSeconds": 3600
  }'

Ambas listas requieren de 1 a 20 elementos, con IDs únicos y cantidades enteras positivas. requesterEmail es obligatorio. expiresInSeconds es opcional: por defecto son 86400 segundos y el rango permitido es 1–86400.

Respuesta 201 (ejemplo)
{
  "id": "33333333-3333-4333-8333-333333333333",
  "status": "pending",
  "createdAt": "2026-10-10T10:00:00.000Z",
  "expiresAt": "2026-10-10T11:00:00.000Z",
  "terminalAt": null,
  "items": [{
    "side": "offered",
    "cardName": "Example Card",
    "scryfallId": "EXAMPLE_PRINTING_ID",
    "setCode": "SET",
    "collectorNumber": "42",
    "imageUri": null,
    "finish": "nonfoil",
    "quantity": 1
  }]
}

GET/reservations/{id}

Consulta el estado y las líneas guardadas de una reserva. La integración creadora puede consultar sus propias reservas; el propietario también puede consultarlas con su sesión.

Autenticación

X-Integration-Key de la integración creadora, o sesión del propietario.

curl "$BASE_URL/reservations/$RESERVATION_ID" \
  -H "X-Integration-Key: $INTEGRATION_KEY" \
  -H "Authorization: Bearer $OAUTH_ACCESS_TOKEN"

Con sesión de propietario, usa Authorization: Bearer $OWNER_SESSION_TOKEN en lugar de X-Integration-Key. requesterEmail solo se devuelve al propietario; la respuesta de integración no contiene ese campo.

Respuesta 200
{
  "id": "33333333-3333-4333-8333-333333333333",
  "status": "pending",
  "createdAt": "2026-10-10T10:00:00.000Z",
  "expiresAt": "2026-10-10T11:00:00.000Z",
  "terminalAt": null,
  "items": []
}

3. Cerrar una reserva (solo propietario)

PATCH/reservations/{id}

PATCH cierra la reserva definitivamente. complete marca el intercambio como completado y descuenta las cantidades reservadas de Offered; no modifica el inventario de la colección. reject y cancel liberan las cantidades. Una integración no puede ejecutar esta operación.

Autenticación

Sesión autenticada del propietario (Authorization: Bearer).

curl -X PATCH "$BASE_URL/reservations/$RESERVATION_ID" \
  -H "Authorization: Bearer $OWNER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action":"complete"}'

Las acciones disponibles son complete, reject y cancel. La respuesta 200 contiene el estado terminal y las marcas de tiempo.

Solicitud OAuth (ejemplo)
curl -X PATCH "$BASE_URL/reservations/$RESERVATION_ID" \
  -H "X-Integration-Key: $INTEGRATION_KEY" \
  -H "Authorization: Bearer $OAUTH_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action":"complete"}'

Estados, errores y seguridad

Estados: pending, completed, rejected, cancelled y expired. Las solicitudes pending vencen en expiresAt; los registros terminales y el correo del solicitante se purgan 30 días después de terminalAt.

Los errores usan {"error":{"code":"..."}}. Según operación, puedes recibir 400 por JSON o datos no válidos, 401 por credenciales ausentes/no válidas, 404 si el recurso no está disponible y 413 si el cuerpo supera el límite. Guarda las claves en el servidor, no en aplicaciones cliente; no las pongas en URLs, logs ni repositorios.

Códigos comunes: missing_share_token, missing_credentials, invalid_integration_key, owner_authentication_required, invalid_json_body, invalid_reservation_request, invalid_reservation_action, request_body_too_large, invalid_quantity, invalid_expiry, invalid_email, duplicate_items, reservation_conflict, invalid_request y not_found. Los cuerpos máximos son 64 KiB para POST y 8 KiB para PATCH.

Contrato legible por máquinas

La especificación OpenAPI describe las rutas y esquemas para importar en herramientas compatibles.

/api/v1/exchange/openapi.yaml