Saltar al contenido principal

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íaSignificadoQué hacer
TRANSIENTAlgo aguas arriba no estaba disponible o limitó el tráficoReintentar con backoff
PERMANENTLa request está mal, o el estado no lo permiteNo reintentar — corregir la request
UNKNOWNNo pudimos clasificarloReintentar 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.