# Códigos de error --- - [Códigos de error](#codigos-de-error) - [Cómo leer un error](#como-leer) - [El objeto `error`](#sobre) - [Códigos de negocio](#codigos-negocio) - [Códigos HTTP](#http) - [Errores de validación](#validacion) - [Conflictos de idempotencia](#idempotencia) - [Rechazos de la red](#red) - [Reserva de cupo](#mit) - [Anulación y reversión](#anulacion) - [Errores de transporte y cifrado](#transporte) - [Catálogo en vivo](#catalogo) - [Qué mostrarle a tu cliente](#mensajes) ## Cómo leer un error [#como-leer] 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ó. :::caution 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. ::: ## El objeto `error` [#sobre] Todo 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. ```json { "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`](#idempotencia) | 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` | :::caution **Cambio de forma en el `429` de «otra transacción en proceso».** Antes respondía `{"error": "texto"}`, con `error` como string. Ahora `error` es el objeto de siempre y el texto pasa a `message`: ```json { "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 `error` como texto, cámbiala a leer `message` o `error.code`. Es el único caso en que un campo existente cambia de tipo. ::: ## Códigos de negocio [#codigos-negocio] ### Cobros Los devuelve el [checkout por API](/checkout-transaction#un-intento-por-cobro) 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 | :::note Antes, un cobro ya pagado respondía un `403` sin detalle («This action is unauthorized.»), indistinguible de un problema de permisos. Ahora cada caso tiene su `error.code`. ::: ### Idempotencia | `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 | ### Suscripciones | `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`](/subscription#changeplan): 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ódigos HTTP [#http] | 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](/authentication) | | `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 | ## Errores de validación [#validacion] 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 ::: Código de respuesta: 422 ```json { "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. :::caution **Un caso especial:** [generar un pago](/generate-transaction) 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`, `errors` y `error`: ```json { "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. ::: ## Conflictos de idempotencia [#idempotencia] Las escrituras de [suscripciones](/subscription#idempotencia) 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 ::: Código de respuesta: 409 ```json { "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 ::: Código de respuesta: 409 ```json { "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](/conventions#idempotencia) | | Alcance de la clave | Por comercio y usuario del token | | Respuestas `5xx` | **No se memorizan**, para que puedas reintentar un fallo transitorio | :::tip **Ú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-Key` el segundo intento te devuelve la respuesta original en vez de cobrar dos veces. ::: :::note 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. ::: ## Rechazos de la red [#red] 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](#sobre): 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](/webhook-transaction) y en el webhook [`retry` de suscripción](/subscription-webhook) (`transaction.error`). ```json { "status": "Rechazada", "status_key": "rejected", "response_code": "51", "error": { "code": "51", "message": "…", "retryable": false, "action": "contact_issuer" } } ``` :::note El catálogo completo, con todos los códigos y su `retryable`, se consulta en vivo en `GET /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](#mit) 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](/sandbox#tarjetas-causal). ### Qué es seguro reintentar | `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 `T01` o un `202`, consulta con [estado de transacción](/status-transaction) antes de reenviar nada. ::: ## Reserva de cupo [#mit] Estos son propios de las [reservas de cupo](/mit-pre-authorization). | 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 | ## Anulación y reversión [#anulacion] | 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 | ## Errores de transporte y cifrado [#transporte] | 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 `T01` y `MIT_INDETERMINATE` son 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. ::: ## Catálogo en vivo [#catalogo] 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 | `GET /api/v1/resources/mit/response-codes` ## Qué mostrarle a tu cliente [#mensajes] 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](#red)). :::caution No le muestres al cliente el código crudo ni el mensaje interno. Un `M12` en pantalla no le dice nada a nadie, y los mensajes internos pueden mencionar tu configuración. :::