Core concepts
Errors
Standard HTTP status codes plus a typed JSON envelope.
Error envelope
Every error response has the same shape: the error.code field is the source of truth — the messagemay change, the codes won't. Quote the requestId when contacting support.
json
{
"error": {
"code": "card_declined",
"type": "invalid_request_error",
"message": "The card was declined by the issuer.",
"param": "source.number",
"decline_code": "insufficient_funds",
"requestId": "req_3kf91… "
}
}HTTP statuses
Mapping of the HTTP codes the API returns. The response status is consistent with the error type (4xx = client problem, 5xx = our problem).
| Status | Meaning | Description |
|---|---|---|
200 | OK | Everything worked. |
201 | Created | A new resource was created. |
400 | Bad request | Request is malformed or missing required parameters. |
401 | Unauthorized | Missing or invalid API key. |
402 | Payment failed | Provider declined the charge. |
403 | Forbidden | Key kind is not allowed for this endpoint. |
404 | Not found | Resource does not exist. |
409 | Conflict | Idempotency-Key reused with a different request body. |
415 | Unsupported media type | Content-Type must be application/json. |
422 | Unprocessable entity | No provider in the cascade can accept this charge. |
429 | Too many requests | Rate limit exceeded — back off and retry with jitter. |
500 | Server error | Internal failure. Safe to retry idempotently. |
503 | Service unavailable | Temporary maintenance / upstream outage. |
Error codes
Exhaustive list of every error.code that can appear in a /v1 response, grouped by type. These codes are stable — they're versioned with the API. If you get one that isn't here, open an issue.
Authentication errors · 9
| Code | HTTP | When it's returned |
|---|---|---|
missing_credentials | 401 | El request no llegó con header Authorization (o lo enviaste vacío). |
invalid_api_key | 401 | La key no existe, está rotada, o pertenece a otro entorno (sandbox vs production). |
invalid_token | 401 | JWT corrupto, alterado o firmado con otro secreto. |
token_expired | 401 | El JWT expiró (vida útil 1h). Reinicia el flow llamando POST /v1/auth/token. |
production_key_refused | 400 | El endpoint sandbox rechaza credenciales live por seguridad. |
environment_mismatch | 403 | El shop (o la cuenta) fue promovido a producción y seguís usando credenciales de sandbox, o al revés. No es transitorio: cambiá la key, no reintentes. |
shop_required | 401 | El token no carga `shopId`. Probablemente vino de credenciales merchant-level legacy. |
mobile_auth_failed | 401 | El login movil fue rechazado. `details.reason` distingue el motivo (invalid_credentials, totp_required, totp_invalid, ip_not_allowed, portal_mismatch) para que la app pueda pedir exactamente lo que falta. |
mobile_session_invalid | 401 | El token movil no se puede usar: expiro, la sesion fue revocada, el principal cambio o el refresh se reutilizo. `details.reason` lo discrimina; la app debe re-autenticar con contrasena. |
Permission errors · 6
| Code | HTTP | When it's returned |
|---|---|---|
key_kind_not_allowed | 403 | Estás usando una `pk_*` en una operación que requiere `sk_*`. |
forbidden | 403 | El recurso pertenece a otro merchant/shop o tu rol no lo permite. |
permission_denied | 403 | Tu rol en el shop no incluye este permiso. Pedile a un usuario con 'Gestionar usuarios' que te lo otorgue. |
merchant_suspended | 403 | Un administrador suspendió la cuenta. Reactivala en /admin/merchants para volver a procesar. |
shop_suspended | 403 | Un administrador suspendió el shop. Reactivalo en /admin/shops para volver a procesar. |
method_not_enabled_for_shop | 403 | El método elegido en el checkout NO está en el set de métodos habilitados del shop (shop.paymentMethodIds). Cada shop solo puede cobrar con los rieles que tiene habilitados — la lista de GET /api/v1/payment-methods es la autoridad. Si el shop no tiene ningún método habilitado, NO puede iniciar cobros por checkout. Habilitá el rail en /admin/shops → el shop → Payment methods & fees. |
Invalid request errors · 32
| Code | HTTP | When it's returned |
|---|---|---|
content_type_required | 415 | Mandaste form-urlencoded, multipart u otro tipo. Setea `Content-Type: application/json`. |
invalid_json | 400 | El cuerpo no se pudo parsear. Revisa comas y comillas. |
invalid_request | 400 | Uno o más campos no cumplen el schema. Mira el array `details.issues` para detalles. |
missing_field | 400 | Falta un campo obligatorio. El campo se indica en `param`. |
unsupported_method | 400 | El `method` solicitado no está en el allow-list del shop. Usa GET /v1/payment-methods para ver los disponibles. |
unsupported_country | 400 | El método elegido no aplica al país del usuario. |
unsupported_currency | 400 | Revisa `currencyLimits` del método para ver las monedas válidas. |
amount_invalid | 400 | El monto vino vacío, en cero, negativo o no numérico. |
amount_below_minimum | 400 | El monto es menor al mínimo que el provider acepta para este método. |
amount_above_maximum | 400 | El monto excede el máximo que el provider acepta para este método. |
idempotency_conflict | 409 | Reusaste una `Idempotency-Key` con un body distinto. Genera una nueva o repite el body original. |
card_declined | 402 | El emisor rechazó el cobro. `decline_code` indica la razón específica. |
insufficient_funds | 402 | Sub-caso de card_declined. |
fraud_blocked | 402 | El motor de riesgo (nuestro o del provider) marcó la operación. |
kyc_required | 402 | El monto/método requiere verificación. Inicia con POST /v1/kyc/init. |
three_ds_required | 402 | Redirige al usuario al `redirectUrl` que devuelve el response. |
three_ds_failed | 402 | El usuario no completó el challenge o falló. La transacción queda en estado failed. |
expired_card | 402 | Sub-caso de card_declined. |
missing_required_fields | 422 | El método de pago necesita uno o más datos del comprador (documento, teléfono, etc.) que faltan o vienen mal formados. Revisa `details.missingFields` (o `details.malformedFields`): cada uno trae `key`, `type`, `label` y la regla de validación. Recolecta esos campos y reintenta el mismo cobro (puedes reusar la misma Idempotency-Key — los errores no se cachean). |
amount_out_of_limits | 422 | The local-currency amount (after FX conversion) is below the minimum OR above the maximum the upstream rail accepts. Each method advertises its bounds in GET /api/v1/payment-methods → `currencyLimits[]`. The error response includes the exact limit (`details.limit.min`, `details.limit.max`) and which bound was violated (`details.boundViolated`). Adjust the `amount` (USD) so the converted local-currency value sits within the range, or pick a different method. |
balance_insufficient | 422 | Aplicado al solicitar withdrawals/refunds/payouts que exceden el available. |
fx_unavailable | 422 | The charge was initiated in a non-USD `currency` (presentment) but our FX cache has no rate for that currency, so we cannot convert it to the canonical USD amount. We NEVER guess an FX rate. `details.currency` names the currency. Retry with `currency: "USD"` or a currency we price, or wait for the FX cache to refresh. |
currency_not_funded | 422 | El payout se debita del saldo de payout en la divisa del método. Si pedís un payout en MXN sin saldo MXN, hacé un swap USD→MXN primero. Verificá GET /api/v1/me/payout/balance. |
payout_method_unavailable | 422 | El `methodId` no existe en el catálogo. Listá los métodos en GET /api/v1/me/payout/methods. |
payout_not_found | 404 | No existe un payout con ese id para tu cuenta. |
topup_not_found | 404 | No existe una recarga con ese id para tu cuenta. |
deposit_account_not_found | 404 | No existe una cuenta de depósito con ese id para tu shop. El mismo 404 responde a un id de OTRO shop: un id nunca confirma la existencia de una cuenta ajena. |
deposit_accounts_unavailable | 422 | Tu shop todavía no tiene habilitado el método de depósito (CLABE fija). No es un fallo transitorio: reintentar no lo resuelve. Contactá a soporte para habilitarlo. |
deposit_account_conflict | 409 | Hay más de una cuenta de depósito registrada para ese `endUserRef` en este shop, así que no podemos decidir cuál devolverte — devolver una al azar te daría una CLABE distinta en cada llamada. No se creó ninguna cuenta nueva. |
payout_proof_unavailable | 404 | El comprobante existe solo para un payout COMPLETADO por un proveedor real que lo emite (Pagsmile: PIX/SPEI/Transfiya). Un payout en proceso, fallido o de un método sin comprobante no tiene prueba descargable. |
settlement_already_paid | 409 | No se puede pagar dos veces el mismo settlement. |
claim_already_resolved | 409 | Los claims terminales no aceptan más evidencia ni cambios de estado. |
Rate limit errors · 1
| Code | HTTP | When it's returned |
|---|---|---|
rate_limited | 429 | Excediste el rate limit del tier. El header `Retry-After` indica cuánto esperar. |
API / server errors · 16
| Code | HTTP | When it's returned |
|---|---|---|
shop_not_found | 404 | El shop fue eliminado o nunca existió. Verifica el id en /admin/shops. |
merchant_not_found | 404 | El merchant referenciado no existe. |
transaction_not_found | 404 | El `id` de transacción no existe o pertenece a otro merchant. |
checkout_session_not_found | 404 | El token del checkout no existe o expiró (TTL 24h). Crea una nueva sesión con POST /api/v1/checkout/sessions. |
checkout_session_already_completed | 409 | La sesión ya fue usada para crear una transacción. Para reintentar con otro método, crea una nueva sesión. |
settlement_not_found | 404 | El settlement no existe o ya fue archivado. |
claim_not_found | 404 | El claim no existe o pertenece a otro merchant. |
wallet_not_found | 404 | La payout wallet no existe en este merchant. |
withdrawal_not_found | 404 | El retiro no existe o pertenece a otro merchant. |
webhook_not_found | 404 | El webhook subscription no existe (o pertenece a otro merchant). Verifica el id contra GET /api/v1/webhooks. |
webhook_delivery_not_found | 404 | El delivery id no existe bajo este webhook subscription. Lista las entregas con GET /api/v1/webhooks/{id}/deliveries. |
cascade_exhausted | 422 | Todos los providers configurados rechazaron o están deshabilitados. Revisa `attempts` en el response. |
payout_dispatch_failed | 422 | El proveedor rechazó el desembolso — el motivo REAL del proveedor viene en `details.failureReason` (y queda persistido en el payout). El débito se revirtió automáticamente: el saldo quedó intacto. Corregí los datos del beneficiario (o contactá soporte si el motivo apunta a credenciales/habilitación) y reintentá. |
topup_address_unavailable | 503 | El pool de direcciones de depósito está agotado para ese riel (asset+red). El operador debe cargar más direcciones en el admin. En sandbox se genera una dirección sintética automáticamente. |
service_unavailable | 503 | Mantenimiento o un upstream caído. Reintenta con backoff exponencial. |
internal_error | 500 | Bug del servidor o un panic no controlado. Manda el `requestId` a soporte. |
How to handle errors in your integration: we recommend a
switch on error.code to decide the action (retry, show a message to the user, escalate to support). Never parse the message: that text can change between versions.