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.
DOCUMENTACIÓN PARA DEVELOPERS
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.
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.
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.
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.
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.
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
// 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"
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.
/offeredObtiene 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ónX-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"
}]
}
/wantedObtiene las impresiones concretas que el propietario busca. Los elementos incluyen identificadores públicos que se pueden enviar como itemId al solicitar una reserva.
AutenticaciónX-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"
}]
}
/reservationsCrea 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ónX-Integration-Key + Authorization: Bearer (OAuth), or the legacy Offered share token + integration key
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
}]
}
/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ónX-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": []
}
/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ónSesió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: 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.
La especificación OpenAPI describe las rutas y esquemas para importar en herramientas compatibles.