Manejo de errores
La página de códigos de error es el catálogo
B2B_* completo — cada código, su HTTP status, su categoría y si un reintento
puede servir. Esta página es sobre la parte que sorprende: no todas las fallas
vuelven con esa forma. Hay cuatro capas que pueden rechazar una request, y cada
una responde en su propio formato.
Cuatro formas, no una
Errores de dominio — code / category
La forma que vas a manejar la mayor parte del tiempo. Todo lo que la plataforma rechaza por una razón de negocio:
{
"code": "B2B_INSUFFICIENT_BALANCE",
"category": "PERMANENT",
"message": "El balance disponible es insuficiente para completar la operación.",
"correlationId": "b6e1b6f0-0000-4000-8000-000000000000"
}
El message viene en español, sin importar en qué idioma leas esta doc.
Ramifica por code, nunca por message: los mensajes son para humanos, pueden
cambiar y no están localizados. Loguea el correlationId: es la forma más
rápida de que encontremos tu request.
Un código agrega un quinto campo. B2B_PROVIDER_REJECTED — la red de pagos
rechazó la request por una razón que todavía no catalogamos — trae details, el
mensaje de la propia red, campo por campo y textual:
{
"code": "B2B_PROVIDER_REJECTED",
"category": "PERMANENT",
"message": "El proveedor rechazó la operación con un error no catalogado. Verifique los datos enviados; la operación no se reintentará automáticamente.",
"details": ["members.1.receiver.beneficiary documentType must be one of V, E, J, G"]
}
Trata details como un mensaje de validación: le dice a un humano qué corregir,
en inglés, y su redacción no es algo que podamos prometer. Loguéalo, muéstraselo
a tus operadores y nunca ramifiques sobre él. La página de
códigos de error explica
cuándo aparece.
Errores de validación — 400 con una lista
Una request malformada nunca llega a la capa de dominio, así que nunca recibe un
code. La validación del framework responde con message como array:
{
"statusCode": 400,
"message": ["quotation_id must be a UUID"],
"error": "Bad Request"
}
Un objeto obligatorio que falta también cae aquí, antes de que se mire nada
de lo que tiene adentro — un POST /orders sin sender, o un POST /quotations
sin source:
{
"statusCode": 400,
"message": ["sender should not be null or undefined"],
"error": "Bad Request"
}
Los campos desconocidos también se rechazan aquí. Mandar un campo que no está
en el contrato — un typo, un resto de otra integración, o un campo read-only como
bank_display_name — es un 400, no se ignora en silencio. Envía exactamente lo
que declara el schema.
Un header requerido que falta también cae aquí, con message como string
plano en lugar de array:
{ "statusCode": 400, "message": "X-Idempotency-Key header is required", "error": "Bad Request" }
Así que incluso dentro de esta misma forma, message puede ser de cualquiera de
los dos tipos. Maneja los dos.
Errores de transporte y auth — message como string
Rechazos que ocurren antes de que corra código de aplicación. Las mismas tres
claves que arriba, pero message es un string plano:
{ "message": "Bearer token required", "error": "Unauthorized", "statusCode": 401 }
Hay tres de estos en el camino de auth, y la redacción te dice cuál te tocó:
Bearer token required (no hay token), Invalid api token (un token que
rechazamos) y Client identity not established — un token que verificó pero no
lleva un cliente resoluble. Este último no se arregla reintentando ni pidiendo
otro token: hay que revisar tus credenciales de nuestro lado.
Vienen de la verificación del token en autenticación.
Una falla de firma NO se ve así. Pasa por la capa de dominio, así que llega
con la primera forma, con un code por el que puedes ramificar:
{
"code": "B2B_UNAUTHORIZED",
"category": "PERMANENT",
"message": "No tiene autorización para realizar esta operación o sus credenciales no son válidas."
}
Esa diferencia es un diagnóstico gratis: un 401 con code es tu firma o tu
nonce; un 401 con error es tu token.
Si parseas errores, maneja las dos formas de message. Un cliente que asume que
message siempre es string se rompe con el primer error de validación, y uno que
asume que siempre es array se rompe con el primer token vencido.
Errores del gateway — message y requestId
Una forma más, y la única que nunca llega al servicio. Los requisitos declarados en
el borde — como el header X-Idempotency-Key en POST /orders — se aplican ahí:
{
"message": "\"Missing required request parameters: [X-Idempotency-Key]\"",
"requestId": "92885917-5fb9-41d1-8272-70cfdefe6c23"
}
Sin code, sin statusCode, sin error — un requestId en su lugar. Si parseas
bodies de error, este es el cuarto caso, y el que más fácilmente se le escapa a una
suite que solo prueba contra el servicio directo.
Qué reintentar
category es toda la respuesta, y solo está presente en los errores de dominio:
| Categoría | Significado | Qué hacer |
|---|---|---|
TRANSIENT | Algo aguas arriba no estaba disponible o limitó el tráfico | Reintentar con backoff |
PERMANENT | La request está mal, o el estado no lo permite | No reintentar — corregir la request |
UNKNOWN | No pudimos clasificarlo | Reintentar una vez y escalar con el correlationId |
Reintentar un error PERMANENT te cuesta tiempo y devuelve exactamente lo mismo.
B2B_QUOTATION_ALREADY_USED es el caso más claro: una cotización respalda
exactamente una orden, para siempre, así que la misma llamada falla igual por
siempre — ver idempotencia.
Los 400 y 401 tampoco conviene reintentarlos sin cambios: nada del próximo
intento idéntico va a ser distinto.
Un reintento que no es un reintento
Si una llamada a POST /orders te da timeout o falla de una forma que no puedes
clasificar, no crees una orden nueva. Envía la misma request con la misma
X-Idempotency-Key — si el primer intento sí llegó, recibes la orden original
en lugar de un pago duplicado. Eso es exactamente para lo que existe la
idempotencia.