Hay tres capas distintas y conviene no confundirlas:
| Capa | Ejemplo | Qué significa |
|---|---|---|
| HTTP | 422 |
Cómo respondió nuestra API |
error.code |
payment_already_used |
Qué pasó, con una clave estable. Es lo que tu código debe leer |
| Validación | errors.payment_card.cvv |
Tu petición no cumple las reglas. Se arregla en tu código |
| Código de red | 51, M12, V68 |
La operación llegó a la red y la red decidió. No siempre se arregla reintentando |
Un pago rechazado no es un error de tu integración: la petición fue correcta y el
resultado fue «no». Devuelve 200 u 422 según el caso, y siempre trae el estado de la
transacción para que sepas en qué quedó.
{warning} Nunca reintentes automáticamente un rechazo. Si la razón fue fondos insuficientes o una tarjeta bloqueada, reintentar da el mismo resultado y algunos emisores penalizan los reintentos.
errorTodo error de la API v1 (HTTP 4xx) trae un objeto error con la misma forma, además
de los campos que ya traía (message, errors en un 422, changed/canceled/applied: false en las acciones de suscripción). Nada de lo anterior desaparece.
{
"message": "Este cobro ya tiene una transacción y no permite reintentos",
"error": {
"type": "invalid_request_error",
"code": "payment_already_used",
"message": "Este cobro ya tiene una transacción y no permite reintentos",
"param": "payment.id"
}
}
| Campo | Para qué |
|---|---|
error.type |
Familia del error. Ver la tabla de abajo |
error.code |
Clave estable en inglés. Decide con esta, no con el texto de message ni solo con el HTTP |
error.message |
Texto legible, para tu log. Puede cambiar de redacción |
error.param |
El campo que causó el error, cuando aplica (payment.id, metadata, el primer campo inválido de un 422). Si no, null |
error.type |
HTTP | Cuándo |
|---|---|---|
invalid_request_error |
400, 404, 409, 422 |
La petición no se puede aplicar: regla de negocio, recurso inexistente, validación |
authentication_error |
401 |
Falta el token o no es válido |
card_error |
402 |
La tarjeta fue rechazada al cobrar |
authorization_error |
403 |
Token válido, pero sin permiso para esto |
rate_limit_error |
429 |
Ya hay una operación igual en curso |
idempotency_error |
409 |
Choque de Idempotency-Key |
Si el error no tiene un código propio, error.code toma el de su HTTP:
| HTTP | error.code por defecto |
|---|---|
400 |
bad_request |
401 |
unauthenticated |
402 |
card_declined |
403 |
forbidden |
404 |
not_found |
405 |
method_not_allowed |
409 |
conflict |
422 |
validation_failed (con param = el primer campo inválido) |
429 |
too_many_requests |
{warning} Cambio de forma en el
429de «otra transacción en proceso». Antes respondía{"error": "texto"}, conerrorcomo string. Ahoraerrores el objeto de siempre y el texto pasa amessage:{ "message": "Otra transacción esta siendo procesada, por favor intenta de nuevo o mas tarde.", "error": { "type": "rate_limit_error", "code": "payment_in_progress", "message": "Otra transacción esta siendo procesada, por favor intenta de nuevo o mas tarde.", "param": null } }Si tu integración leía
errorcomo texto, cámbiala a leermessageoerror.code. Es el único caso en que un campo existente cambia de tipo.
Los devuelve el checkout por API cuando el
par payment.id + payment.token no permite pagar.
error.code |
HTTP | param |
Qué pasó | Qué hacer |
|---|---|---|---|---|
invalid_payment_credentials |
403 | payment.token |
El id no existe o el token no le corresponde. Es el mismo código en ambos casos, para no revelar qué ids existen |
Revisar el par que guardaste de generate-payment |
payment_already_used |
403 | payment.id |
El cobro ya tiene una transacción (pagada o no) y no admite reintentos | Generar un cobro nuevo |
payment_already_paid |
403 | payment.id |
El cobro ya fue pagado | No cobrar de nuevo |
payment_in_progress |
403 | payment.id |
Tiene transacciones en progreso que agotan su límite | Esperar el resultado de la que está en curso |
payment_in_progress |
429 | — | Otra transacción de este cobro se está procesando ahora mismo (tipo rate_limit_error) |
Esperar y consultar el estado |
payment_expired |
403 | payment.id |
Pasó la fecha límite del cobro («Este cobro ya expiro») | Generar un cobro nuevo |
payment_inactive |
403 | payment.id |
El cobro fue desactivado | Generar un cobro nuevo |
{info} Antes, un cobro ya pagado respondía un
403sin detalle («This action is unauthorized.»), indistinguible de un problema de permisos. Ahora cada caso tiene suerror.code.
error.code |
HTTP | Qué pasó |
|---|---|---|
idempotency_key_reused |
409 | La Idempotency-Key ya se usó con parámetros distintos |
idempotency_key_in_progress |
409 | La petición original con esa clave todavía se está procesando |
error.code |
Qué pasó |
|---|---|
subscription_already_canceled |
La suscripción ya estaba cancelada |
subscription_canceled |
La operación no aplica a una suscripción cancelada (cambio de plan, cambio de tarjeta, mode: "direct") |
subscription_inactive |
La suscripción está inactiva; usa la invitación o la renovación con pago |
subscription_same_plan |
La suscripción ya está en ese plan |
invitation_already_pending |
Ya hay una invitación de cambio de plan o de renovación pendiente |
subscriber_without_card |
El suscriptor no tiene tarjeta guardada |
subscriber_already_subscribed |
El suscriptor ya está suscrito a ese plan |
subscriber_has_active_subscriptions |
No se puede eliminar: tiene suscripciones activas |
subscriber_office_unresolved |
No se pudo determinar la sucursal del suscriptor |
plan_inactive |
El plan está inactivo |
plan_has_active_subscriptions |
No se puede eliminar: el plan tiene suscripciones activas |
group_has_plans |
No se puede eliminar: el grupo tiene planes |
coupon_invalid |
Cupón inválido o no disponible |
card_declined |
402 en un cambio de plan con always_invoice: el cobro inmediato del prorrateo fue rechazado y el plan no cambió. Trae además error.decline_code, el response_code de la transacción |
Estas respuestas conservan los campos que ya traían (changed: false, canceled: false,
applied: false), así que el código que los lee sigue funcionando.
| Código | Cuándo | Qué hacer |
|---|---|---|
200 |
La operación se procesó. Revisa el estado del cuerpo: puede ser un rechazo | Leer status / success |
201 |
Se creó algo (una reserva autorizada, por ejemplo) | Guardar el id |
202 |
Estado indeterminado: no sabemos aún el resultado | No reintentar. Esperar el webhook o consultar |
400 |
Regla de negocio incumplida (por ejemplo, borrar un suscriptor con suscripción activa) | Leer error.code |
401 |
Falta el token o no es válido | Ver Autenticación |
402 |
Rechazo de tarjeta en un cobro inmediato de suscripción | Leer error.decline_code |
403 |
Token válido pero sin permiso, comercio deshabilitado, o cobro que ya no acepta pagos | Leer error.code |
404 |
El recurso no existe, o es de otro comercio, o del otro ambiente | Revisar el id y el tipo de token |
409 |
Conflicto: cobro ya aplicado, o Idempotency-Key reutilizada con otros parámetros |
Leer error.code |
422 |
Validación, o la red rechazó | Leer errors (o error.param) o el estado |
429 |
Ya hay una operación igual en curso | Esperar el resultado, no reintentar |
5xx |
Error de nuestro lado o de la red | Reintentable con espera |
Siempre tienen la misma forma: un message con el primer error, un objeto errors con
todos, indexados por el nombre del campo, y el objeto error con
code: "validation_failed" y param apuntando al primer campo inválido.
{danger} Validación code:
422 { "message": "El campo payment_card.cvv es obligatorio.", "errors": { "payment_card.cvv": ["El campo payment_card.cvv es obligatorio."], "customer_payer.email": ["El campo customer_payer.email debe ser un correo válido."] }, "error": { "type": "invalid_request_error", "code": "validation_failed", "message": "El campo payment_card.cvv es obligatorio.", "param": "payment_card.cvv" } }
Las claves de errors usan notación de punto para los campos anidados. Úsalas para
marcar el campo exacto en tu formulario en lugar de mostrar un mensaje genérico.
{warning} Un caso especial: generar un pago devolvía solo el mapa de errores, con los campos en la raíz. Esas claves se conservan —si las lees así, sigues funcionando— y ahora se suman
message,errorsyerror:{ "payment.amount": ["The payment.amount field is required."], "errors": { "payment.amount": ["The payment.amount field is required."] }, "message": "The payment.amount field is required.", "error": { "type": "invalid_request_error", "code": "validation_failed", "message": "The payment.amount field is required.", "param": "payment.amount" } }Y si en esa misma llamada omites
payment.currency_type, la validación se detiene ahí: recibirás ese único error y ninguno más, porque los límites de monto dependen de la moneda.
Las escrituras de suscripciones
aceptan el header Idempotency-Key. Cuando esa clave choca, responde 409 con
error.type: "idempotency_error" y un error.code propio.
{danger} Misma clave, parámetros distintos code:
409 { "message": "La Idempotency-Key ya fue usada con parámetros distintos.", "error": { "type": "idempotency_error", "code": "idempotency_key_reused", "message": "La Idempotency-Key ya fue usada con parámetros distintos.", "param": null } }
{danger} Misma clave, misma petición todavía en curso code:
409 { "message": "Una solicitud con esta Idempotency-Key aún está en proceso.", "error": { "type": "idempotency_error", "code": "idempotency_key_in_progress", "message": "Una solicitud con esta Idempotency-Key aún está en proceso.", "param": null } }
Cuando en cambio repites exactamente la misma llamada con la misma clave, te
devolvemos la respuesta original con el header Idempotency-Replayed: true y sin volver
a ejecutar el cobro.
| Detalle | Comportamiento |
|---|---|
| Vigencia de la clave | 24 horas |
| Métodos afectados | POST, PUT, PATCH, DELETE |
| Rutas | Las escrituras de /api/v1/subscriptions/*, /api/v1/payment/* y /api/v1/mit/pre-authorizations/*. Ver el detalle en Convenciones |
| Alcance de la clave | Por comercio y usuario del token |
Respuestas 5xx |
No se memorizan, para que puedas reintentar un fallo transitorio |
{success} Úsala en todo cobro. El caso que evita: cobras, tu proceso se cae antes de guardar la respuesta, y al reintentar no sabes si el primer intento llegó. Con la misma
Idempotency-Keyel segundo intento te devuelve la respuesta original en vez de cobrar dos veces.
{info} La clave la eliges tú: una distinta por operación, no por reintento. Un UUID generado al empezar el cobro y reutilizado en todos los reintentos de esa misma operación es lo habitual.
Los más frecuentes. El código llega en response_code de la transacción y, cuando no fue
aprobada, también en error.code con su message, su retryable y su action. Este
error de la transacción es distinto del objeto error de la API: viene
dentro de una respuesta 200 (o de un webhook), no en un 4xx. Llega igual en la
respuesta del checkout, en los webhooks de transacción y en el
webhook retry de suscripción (transaction.error).
{
"status": "Rechazada",
"status_key": "rejected",
"response_code": "51",
"error": { "code": "51", "message": "…", "retryable": false, "action": "contact_issuer" }
}
{info} El catálogo completo, con todos los códigos y su
retryable, se consulta en vivo enGET /api/v1/resources/mit/response-codes. Léelo de ahí en vez de copiar esta tabla: se mantiene sola.
description es el mensaje para el tarjetahabiente y error.message el que nombra la
causa para tu log. En los rechazos de esta tabla ambos coinciden con la causa y con
error.action: la description pide lo mismo que la acción. Los códigos que no están
aquí conservan el mensaje del catálogo de la red.
| Código | Qué pasó | description (tarjetahabiente) |
error.message (tu log) |
retryable |
action |
|---|---|---|---|---|---|
00 |
Aprobada | — | — | — | — |
05 |
El emisor no autorizó, sin causa específica | Tu banco no autorizó el pago. Comunícate con tu banco o usa otro medio de pago. | Transacción declinada por el emisor sin causa específica. | false |
contact_issuer |
14 |
Tarjeta inválida | Esta tarjeta no es válida. Usa otra tarjeta. | Transacción declinada. Tarjeta inválida. | false |
use_another_card |
41 |
Tarjeta reportada como extraviada | Esta tarjeta no se puede usar. Usa otra tarjeta. | Transacción declinada. Tarjeta reportada como extraviada. | false |
use_another_card |
43 |
Tarjeta bloqueada por el emisor | Esta tarjeta está bloqueada. Usa otra tarjeta. | Transacción declinada. Tarjeta bloqueada por el emisor. | false |
use_another_card |
51 |
Fondos insuficientes | Transacción declinada. Fondos insuficientes | Transacción declinada. Fondos insuficientes | false |
contact_issuer |
54 |
Tarjeta vencida | Tu tarjeta está vencida. Usa otra tarjeta. | Transacción declinada. Tarjeta vencida. | false |
use_another_card |
57 |
El emisor no permite este tipo de transacción | Esta tarjeta no permite este pago. Usa otra tarjeta. | Transacción declinada. Tipo de transacción no permitido para la tarjeta. | false |
use_another_card |
61 |
Excede el límite de monto de la tarjeta | El pago supera el límite de tu tarjeta. Comunícate con tu banco o usa otra tarjeta. | Transacción declinada. Excede el límite de monto. | false |
contact_issuer |
62 |
Tarjeta restringida | Esta tarjeta tiene restricciones. Usa otra tarjeta. | Transacción declinada. Tarjeta restringida. | false |
use_another_card |
65 |
Excede la frecuencia de transacciones | Superaste el número de pagos permitidos con esta tarjeta. Comunícate con tu banco o usa otra tarjeta. | Transacción declinada. Excede la frecuencia de transacciones. | false |
contact_issuer |
91 |
El emisor no está disponible | Tu banco no respondió. Intenta de nuevo en unos minutos. | Transacción rechazada. El emisor no está disponible. | true |
retry_later |
96 |
Falla del sistema del emisor o de la red | No pudimos procesar el pago. Intenta de nuevo en unos minutos. | Transacción rechazada. Falla del sistema del emisor o de la red. | true |
retry_later |
98 |
CVV inválido | El código de seguridad (CVV) es incorrecto. Verifica los datos de la tarjeta. | Transacción declinada. CVV inválido. | false |
check_card_data |
error.action toma uno de estos valores (los códigos de
reserva de cupo pueden traer su propio texto de acción):
action |
Qué hacer |
|---|---|
contact_issuer |
El cliente debe hablar con su banco, o usar otro medio |
use_another_card |
Pedir otra tarjeta |
retry_later |
Reintentar más tarde, con espera |
check_card_data |
Pedir al tarjetahabiente que revise número, fecha o CVV |
Puedes provocar cada causal en pruebas con las tarjetas por causal.
error.retryable |
Ejemplos | Qué hacer |
|---|---|---|
false |
05, 14, 41, 43, 51, 54, 57, 61, 62, 65, 98 (fondos, vencida, bloqueada, límite, CVV) |
No reintentes. El resultado será el mismo y algunos emisores penalizan la insistencia. Pide otro medio de pago |
true |
19, 90, 91, 96, E99, S01, S10, S11 (la red o un servicio interno falló), salvo que el catálogo diga otra cosa |
Reintenta con espera creciente |
| — | T01 y cualquier 202 |
Estado indeterminado: no reintentes. No sabemos si la operación se procesó. Consulta el estado antes de hacer nada |
{danger} La diferencia entre «falló» y «no sé si falló» es la que produce cobros dobles. Ante
T01o un202, consulta con estado de transacción antes de reenviar nada.
Estos son propios de las reservas de cupo.
| Código | HTTP | Qué pasó |
|---|---|---|
MIT_PRODUCTION_KEY_REQUIRED |
422 | Intentaste autorizar con llave de prueba; una reserva retiene fondos reales |
MIT_AMOUNT_EXCEEDS_AUTHORIZED |
422 | El cobro final supera lo reservado |
MIT_EXPIRED |
422 | La reserva venció; hay que crear una nueva |
MIT_ALREADY_CONFIRMED |
409 | Esta reserva ya tiene su cobro aplicado |
MIT_VOID_WINDOW_CLOSED |
422 | Ya cerró la ventana para liberar (24 h antes del vencimiento). Deja que venza |
MIT_FRANCHISE_NOT_ENABLED |
422 | Esa franquicia no está habilitada para reservas en tu comercio |
MIT_INDETERMINATE |
202 | No recibimos respuesta de la red. No reintentes |
MIT_IN_PROGRESS |
429 | Ya hay una autorización en curso para esta reserva |
M01 |
409 | La reserva ya fue confirmada |
M02 |
422 | La operación referenciada no es una reserva pre-autorizada |
M04 |
422 | La reserva está vencida en la red |
M05 |
422 | La reserva no fue aprobada, así que no hay cupo que cobrar |
M06 |
422 | La red no encuentra la operación. Solo conserva 30 días |
M08 / M10 / M11 |
422 | La franquicia o el banco emisor no permiten reservas |
M12 |
422 | El valor del cobro final no corresponde al reservado |
309 |
422 | El tipo de transacción MIT no admite 3DS |
310 |
422 | El ECI enviado no es válido: debe ser 05, 06 o 07 |
319 / 320 |
422 | Enviaste el objeto 3DS de la otra franquicia |
| Código | Qué pasó |
|---|---|
A01 |
Pasó la fecha permitida para anular |
A03 – A06 |
La operación no está en un estado que admita anulación |
A07 |
No se puede anular una operación incremental |
V39 |
No se puede liberar una reserva que ya fue cobrada |
V40 |
La reserva no está en fecha válida para liberarse |
V42 |
La operación indicada no es anulable |
| Código | HTTP | Qué pasó | Qué hacer |
|---|---|---|---|
T01 |
202 | Se agotó el tiempo de espera con la red. No sabemos si la operación quedó registrada | No reintentar. Consultar el estado |
E99 |
502 | Error interno de la red | Reintentable con espera |
S10 / S11 |
500 | Fallo de cifrado con la red | Contactar a soporte |
{danger}
T01yMIT_INDETERMINATEson los dos casos en que reintentar es peligroso: si la operación sí se aplicó, un reintento la duplica. Consulta el estado o espera el webhook.
En lugar de copiar esta tabla a tu código, consúltala:
GET /api/v1/resources/mit/response-codes
Cada entrada trae:
| Campo | Para qué |
|---|---|
| code | El código |
| message | El mensaje pensado para el comercio |
| action | Qué hacer |
| retryable | Si tiene sentido reintentar |
Tenemos dos mensajes para cada código y sirven para cosas distintas:
| Campo | Audiencia | Ejemplo |
|---|---|---|
Mensaje al pagador (description) |
El tarjetahabiente | «Tu tarjeta está vencida. Usa otra tarjeta.» |
Mensaje al comercio (error.message) |
Tu operador o tu log | «Transacción declinada. Tarjeta vencida.» |
En la respuesta de una transacción, description trae el mensaje que se le puede
mostrar al cliente tal cual. En un rechazo con código catalogado pide lo mismo que
error.action (ver Rechazos de la red).
{warning} No le muestres al cliente el código crudo ni el mensaje interno. Un
M12en pantalla no le dice nada a nadie, y los mensajes internos pueden mencionar tu configuración.