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).

StatusMeaningDescription
200OKEverything worked.
201CreatedA new resource was created.
400Bad requestRequest is malformed or missing required parameters.
401UnauthorizedMissing or invalid API key.
402Payment failedProvider declined the charge.
403ForbiddenKey kind is not allowed for this endpoint.
404Not foundResource does not exist.
409ConflictIdempotency-Key reused with a different request body.
415Unsupported media typeContent-Type must be application/json.
422Unprocessable entityNo provider in the cascade can accept this charge.
429Too many requestsRate limit exceeded — back off and retry with jitter.
500Server errorInternal failure. Safe to retry idempotently.
503Service unavailableTemporary 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
CodeHTTPWhen it's returned
missing_credentials401El request no llegó con header Authorization (o lo enviaste vacío).
invalid_api_key401La key no existe, está rotada, o pertenece a otro entorno (sandbox vs production).
invalid_token401JWT corrupto, alterado o firmado con otro secreto.
token_expired401El JWT expiró (vida útil 1h). Reinicia el flow llamando POST /v1/auth/token.
production_key_refused400El endpoint sandbox rechaza credenciales live por seguridad.
environment_mismatch403El 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_required401El token no carga `shopId`. Probablemente vino de credenciales merchant-level legacy.
mobile_auth_failed401El 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_invalid401El 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
CodeHTTPWhen it's returned
key_kind_not_allowed403Estás usando una `pk_*` en una operación que requiere `sk_*`.
forbidden403El recurso pertenece a otro merchant/shop o tu rol no lo permite.
permission_denied403Tu rol en el shop no incluye este permiso. Pedile a un usuario con 'Gestionar usuarios' que te lo otorgue.
merchant_suspended403Un administrador suspendió la cuenta. Reactivala en /admin/merchants para volver a procesar.
shop_suspended403Un administrador suspendió el shop. Reactivalo en /admin/shops para volver a procesar.
method_not_enabled_for_shop403El 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
CodeHTTPWhen it's returned
content_type_required415Mandaste form-urlencoded, multipart u otro tipo. Setea `Content-Type: application/json`.
invalid_json400El cuerpo no se pudo parsear. Revisa comas y comillas.
invalid_request400Uno o más campos no cumplen el schema. Mira el array `details.issues` para detalles.
missing_field400Falta un campo obligatorio. El campo se indica en `param`.
unsupported_method400El `method` solicitado no está en el allow-list del shop. Usa GET /v1/payment-methods para ver los disponibles.
unsupported_country400El método elegido no aplica al país del usuario.
unsupported_currency400Revisa `currencyLimits` del método para ver las monedas válidas.
amount_invalid400El monto vino vacío, en cero, negativo o no numérico.
amount_below_minimum400El monto es menor al mínimo que el provider acepta para este método.
amount_above_maximum400El monto excede el máximo que el provider acepta para este método.
idempotency_conflict409Reusaste una `Idempotency-Key` con un body distinto. Genera una nueva o repite el body original.
card_declined402El emisor rechazó el cobro. `decline_code` indica la razón específica.
insufficient_funds402Sub-caso de card_declined.
fraud_blocked402El motor de riesgo (nuestro o del provider) marcó la operación.
kyc_required402El monto/método requiere verificación. Inicia con POST /v1/kyc/init.
three_ds_required402Redirige al usuario al `redirectUrl` que devuelve el response.
three_ds_failed402El usuario no completó el challenge o falló. La transacción queda en estado failed.
expired_card402Sub-caso de card_declined.
missing_required_fields422El 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_limits422The 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_insufficient422Aplicado al solicitar withdrawals/refunds/payouts que exceden el available.
fx_unavailable422The 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_funded422El 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_unavailable422El `methodId` no existe en el catálogo. Listá los métodos en GET /api/v1/me/payout/methods.
payout_not_found404No existe un payout con ese id para tu cuenta.
topup_not_found404No existe una recarga con ese id para tu cuenta.
deposit_account_not_found404No 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_unavailable422Tu 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_conflict409Hay 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_unavailable404El 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_paid409No se puede pagar dos veces el mismo settlement.
claim_already_resolved409Los claims terminales no aceptan más evidencia ni cambios de estado.
Rate limit errors · 1
CodeHTTPWhen it's returned
rate_limited429Excediste el rate limit del tier. El header `Retry-After` indica cuánto esperar.
API / server errors · 16
CodeHTTPWhen it's returned
shop_not_found404El shop fue eliminado o nunca existió. Verifica el id en /admin/shops.
merchant_not_found404El merchant referenciado no existe.
transaction_not_found404El `id` de transacción no existe o pertenece a otro merchant.
checkout_session_not_found404El token del checkout no existe o expiró (TTL 24h). Crea una nueva sesión con POST /api/v1/checkout/sessions.
checkout_session_already_completed409La sesión ya fue usada para crear una transacción. Para reintentar con otro método, crea una nueva sesión.
settlement_not_found404El settlement no existe o ya fue archivado.
claim_not_found404El claim no existe o pertenece a otro merchant.
wallet_not_found404La payout wallet no existe en este merchant.
withdrawal_not_found404El retiro no existe o pertenece a otro merchant.
webhook_not_found404El webhook subscription no existe (o pertenece a otro merchant). Verifica el id contra GET /api/v1/webhooks.
webhook_delivery_not_found404El delivery id no existe bajo este webhook subscription. Lista las entregas con GET /api/v1/webhooks/{id}/deliveries.
cascade_exhausted422Todos los providers configurados rechazaron o están deshabilitados. Revisa `attempts` en el response.
payout_dispatch_failed422El 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_unavailable503El 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_unavailable503Mantenimiento o un upstream caído. Reintenta con backoff exponencial.
internal_error500Bug 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.