Errores
Toda integración seria se define por cómo maneja lo que sale mal, no por el camino feliz. En esta página te explicamos, sin dar nada por sabido, cómo Alibanca te reporta un error: la forma exacta del cuerpo de respuesta, qué significa cada código HTTP, y —lo más importante para no perder ni duplicar dinero— cuándo debes reintentar una operación y cuándo NO. Léela completa antes de mandar tu primer pago de verdad: aquí es donde la idempotencia y el estado unknown dejan de ser teoría y se vuelven decisiones de código.
La forma de un error
Cuando una petición sale bien, Alibanca te responde con un cuerpo JSON que siempre tiene la misma estructura: un status en "success", un traceId (un identificador único de esa petición, útil para soporte) y un payload con los datos que pediste. Un error es lo contrario y es igual de predecible: un único campo error con un mensaje legible, acompañado de un código de estado HTTP distinto de 2xx. Nada más. No hay diez formatos de error escondidos; hay uno solo.
{ "error": "amountMinor es obligatorio" }
Dos piezas, entonces, y las dos importan:
El código HTTP te dice la familia del problema
Es lo primero que debe leer tu código, incluso antes del mensaje. El código HTTP (400, 401, 404, 409, 5xx…) es un número estándar de la web que clasifica el error en una familia. De esa familia sale tu decisión: ¿fue culpa de mi request?, ¿fue de autenticación?, ¿fue nuestro?, ¿reintento o no? El mensaje es para el humano; el código es para tu máquina.
El campo error te dice el detalle para depurar
Es una frase en español pensada para que un desarrollador entienda qué corregir: "amountMinor es obligatorio", "firma inválida", "beneficiario no encontrado". Muéstralo en tus logs, no se lo enseñes crudo a tu usuario final. Programa tu lógica contra el código HTTP, nunca contra el texto exacto del mensaje: el texto puede mejorar con el tiempo, el código es el contrato estable.
payload. Si es 4xx o 5xx, lee el campo error. Nunca asumas que un cuerpo de error trae payload, ni que un cuerpo de éxito trae error. Ramifica primero por el código de estado.Un error HTTP NO es un pago fallido
Esta es, de lejos, la confusión que más caro cuesta, así que la aclaramos de una vez. Hay dos mundos distintos y no se tocan:
Mundo 1 — el resultado de la petición HTTP. ¿Alibanca aceptó y procesó tu llamada? Eso lo responde el código HTTP. Un 4xx/5xx significa que la petición como tal no prosperó (o no sabemos si prosperó).
Mundo 2 — el estado del dinero. Cuando emites un pago y la petición SÍ prospera, recibes un 201 con un payload que trae el campo state. Ese state vive dentro de una respuesta exitosa:
Fíjate en la trampa: un pago con state: "failed" te llega dentro de un 201, no de un HTTP 4xx. Que el pago haya fallado no quiere decir que tu request haya fallado — tu request fue perfecto, Alibanca lo aceptó, lo intentó, y el banco lo rechazó. Por eso el estado del pago viaja en el payload de una respuesta de éxito, y no como un error HTTP.
state (completed/unknown/failed) — eso se explica en Estados de un pago →. Para saber si reintentas una petición HTTP que ni siquiera devolvió un pago, mira el código HTTP — eso es lo que cubre esta página. Son mecanismos separados que se coordinan a través de la idempotency-key.El mapa de códigos HTTP
Alibanca usa códigos HTTP estándar, sin sorpresas. Los agrupamos en familias por el primer dígito: los 4xx significan "el problema está en tu petición, corrígela"; los 5xx significan "el problema está de nuestro lado". Esta tabla es tu referencia rápida; abajo desarrollamos cada familia con ejemplos.
| Código | Qué significa | Cómo debe reaccionar tu integración |
|---|---|---|
| 400 | Bad Request | Tu petición está mal formada: falta un campo obligatorio, un tipo no cuadra, el JSON está roto, o violaste una regla como el XOR del destino. No reintentes ciego: corrige el request y vuelve a enviar. |
| 401 | Unauthorized | Fallo de autenticación: x-api-key ausente o inválida, o x-signature que no verifica contra tu llave pública, o un nonce que no supera la marca de agua. No reintentes con los mismos datos: arregla las credenciales o la firma. |
| 404 | Not Found | El recurso que pediste no existe (un paymentId, un beneficiaryId) o la ruta está mal escrita. Reintentar no lo va a materializar. Revisa el id o la URL. |
| 409 | Conflict | Conflicto de estado: una idempotency-key reusada con un cuerpo distinto, o un recurso único que choca. No es un error para reintentar a ciegas: es una señal de que ya existe un resultado. Ve a consultarlo. |
| 422 | Regla de negocio | La petición estaba bien formada pero viola una regla del dominio (por ejemplo, saldo insuficiente o un límite de tu llave). Reintentar idéntico dará el mismo rechazo. Ajusta la operación. |
| 5xx | Server Error | El fallo es nuestro (500) o un componente no estuvo disponible temporalmente. Es seguro reintentar, pero con reglas: mismo idempotency-key y con espera exponencial. Esta familia es especial — la desarrollamos aparte. |
400 — el request está mal formado
Un 400 es un error de validación: Alibanca miró tu petición antes de tocar dinero y encontró que no cumple el contrato. Es el error más común mientras desarrollas, y también el más fácil de arreglar, porque el mensaje te dice exactamente qué campo revisar. Nada de dinero se movió: un 400 se rechaza en la puerta.
Las causas típicas de un 400 en POST /v2/payments:
| Causa | Detalle | Mensaje de ejemplo |
|---|---|---|
| Campo obligatorio ausente | falta | Enviaste el cuerpo sin amountMinor o sin currency. |
| Tipo equivocado | formato | Mandaste amountMinor como número (150000) en vez de string ("150000"). Los montos van en unidades menores como STRING, jamás como float. |
| Violación del XOR de destino | regla | Mandaste beneficiaryId y rail+beneficiary al mismo tiempo, o no mandaste ninguno. El destino es uno o el otro, nunca ambos. |
| JSON malformado | sintaxis | El cuerpo no es JSON válido (una coma de más, comillas sin cerrar). |
// Enviaste beneficiaryId y también rail+beneficiary a la vez { "error": "El destino debe ser beneficiaryId O (rail + beneficiary), no ambos" }
Cómo reaccionar a un 400: no reintentes con la misma petición — vas a recibir el mismo 400 para siempre. Un 400 es determinístico: mismo request, mismo rechazo. Corrige el campo que el mensaje señala y vuelve a enviar. Como no se creó ningún pago, puedes reusar la misma idempotency-key sin peligro cuando reenvíes ya corregido.
401 — autenticación o firma inválida
El 401 dice: "no pude confirmar que eres tú". Como cada petición a Alibanca va autenticada por partida doble —con tu x-api-key y con una firma x-signature RSA-SHA256 sobre un envelope canónico— un 401 puede venir de varios puntos de esa cadena. Repasemos los sospechosos habituales:
| Causa | Qué revisar |
|---|---|
| Llave ausente o equivocada | ¿Mandaste el header x-api-key? ¿Estás usando sk_test_… contra sandbox y sk_live_… contra producción, sin cruzarlas? |
| Firma que no verifica | El x-signature se calcula sobre el envelope canónico: la concatenación determinística de [método HTTP, ruta, api-key, idempotency-key, cuerpo crudo, nonce], EN ESE ORDEN. Si armaste el envelope en otro orden, o firmaste un body distinto al que enviaste (por ejemplo, re-serializaste el JSON), la firma no cuadra. |
| Cuerpo crudo alterado | Debes firmar el raw body exacto, byte por byte, que va en la petición. Si tu framework re-formatea el JSON después de que firmaste, el hash cambia y el 401 aparece. |
| Nonce no creciente | Cada request usa un nonce mayor que el anterior de esa llave (marca de agua creciente, "high-water", que corta los replays). Si repites o bajas el nonce, lo rechazamos. |
{ "error": "firma inválida" }
Cómo reaccionar a un 401: nunca reintentes con exactamente los mismos headers — el resultado no va a cambiar. Un 401 casi siempre es un bug de tu firma o de tus credenciales, no un fallo transitorio. Detén la operación, registra el error, y revisa cómo estás construyendo el envelope. El único "reintento" válido es después de corregir la firma o la llave — y ahí el nonce debe ser uno nuevo y mayor.
404 — el recurso no existe
Un 404 significa que apuntaste a algo que Alibanca no encuentra. Dos variantes: (1) la ruta está mal escrita —por ejemplo /v2/payment/… en singular cuando es /v2/payments/…—, o (2) el identificador no corresponde a ningún recurso tuyo: un paymentId que no existe, un beneficiaryId que archivaste o que nunca creaste, o el id de otra cuenta que no es la de tu llave.
// GET /v2/payments/pay_no_existe { "error": "pago no encontrado" }
Cómo reaccionar a un 404: reintentar no va a crear el recurso, así que no reintentes en bucle. Verifica primero que el id sea el correcto y que pertenezca a tu llave. Hay un matiz útil, eso sí: si acabas de crear algo y lo consultas inmediatamente, un 404 momentáneo puede ser una carrera de lectura muy corta; en ese caso, un único reintento con un pequeño respiro es razonable. Pero un 404 persistente = el recurso no está, punto.
409 — conflicto de idempotencia o unicidad
El 409 es el más interesante y el que más gente malinterpreta, porque no siempre es "algo salió mal" — a veces es "ya hiciste esto, aquí no hay nada nuevo que crear". Recuerda qué es la idempotency-key: un identificador único y determinístico que tú generas por cada operación lógica. Su propósito es que reintentar una operación devuelva el mismo resultado en vez de crear un pago duplicado. El 409 aparece cuando esa promesa entra en tensión:
| Escenario de 409 | Qué pasó y qué hacer |
|---|---|
| Misma key, cuerpo distinto | Reusaste una idempotency-key que ya habías gastado, pero con un cuerpo diferente (otro monto, otro destino). Alibanca lo bloquea: una key representa una operación, no puedes reciclarla para otra. Solución: usa una key nueva para la operación nueva. |
| Recurso único en conflicto | Intentaste crear algo que choca con una restricción de unicidad existente. Solución: consulta el recurso que ya existe en vez de recrearlo. |
{ "error": "idempotency-key ya usada con un cuerpo distinto" }
Cómo reaccionar a un 409: nunca lo trates como "reintento fallido, dale otra vez". Un 409 te está diciendo que hay estado del lado de Alibanca que no debes pisar. Si el conflicto es de idempotencia con cuerpo distinto, revisa tu generación de keys — probablemente estás reusando una key por accidente. Si de verdad querías consultar el resultado de esa operación previa, haz un GET /v2/payments/:id y lee su estado.
idempotency-key y el mismo cuerpo es exactamente lo que la idempotencia protege — te devuelve el resultado original (típicamente un 200/201 idéntico), no un 409. El 409 salta solo cuando la key se reusa con un cuerpo diferente. Por eso el patrón seguro de reintento es: misma key + mismo cuerpo, byte por byte.422 y otras reglas de negocio
Un 422 (u otro código de la familia 4xx que uses para dominio) significa que tu petición estaba sintácticamente perfecta —todos los campos presentes y bien tipados— pero viola una regla del negocio. La diferencia con el 400 es importante: el 400 es "no entiendo tu request"; el 422 es "entiendo tu request perfectamente, pero no puedo cumplirlo". Casos típicos: saldo insuficiente para el monto que pides, o un monto que supera un límite de tu llave (los limits que ves en GET /v2/me, que combinan tus propios topes con los pisos que impone Alibanca).
{ "error": "saldo insuficiente para el monto solicitado" }
Cómo reaccionar: reintentar la misma operación idéntica dará el mismo rechazo, porque la regla no cambió por reintentar. Lo que corresponde es ajustar la operación (bajar el monto, fondear saldo, respetar el límite) y luego enviar. Como no se creó ningún pago, puedes usar una idempotency-key nueva para el intento corregido.
5xx — el problema es nuestro (y aquí sí reintentas)
Los 5xx son la única familia donde tu código sí debe reintentar automáticamente, porque el fallo no está en tu petición sino de nuestro lado: un error interno (500) o un componente temporalmente indisponible. La web se cae a ratos; una buena integración lo absorbe. Pero hay una regla no negociable, y es donde la idempotencia se vuelve tu salvavidas.
POST /v2/payments es ambiguo. No sabes si el pago se creó o no antes de que la conexión se cayera. Si reintentas con una idempotency-key nueva, corres el riesgo de emitir el pago dos veces. La regla: reintenta SIEMPRE con la MISMA idempotency-key y el mismo cuerpo. Así, si el pago ya se había creado, recibes ese mismo resultado; si no se había creado, se crea una sola vez. Nunca doble.Este es el patrón de reintento seguro para un 5xx o un timeout, con espera exponencial ("backoff": esperas cada vez un poco más entre intentos para no golpear un servicio que se está recuperando):
const idem = "pago-orden-8842"; // misma key en TODOS los intentos const body = JSON.stringify({ amountMinor: "150000", currency: "VES", beneficiaryId: "ben_123" }); for (let intento = 0; intento < 5; intento++) { const res = await enviarPago({ idem, body }); // mismo idem, mismo body if (res.status < 500) return res; // 2xx/4xx: resuelto, no reintentar await esperar(2 ** intento * 200); // backoff: 200ms, 400ms, 800ms… }
Cómo reaccionar a un 5xx, resumido: reintenta —pero con la misma key, el mismo cuerpo, un backoff creciente y un tope de intentos—. Si tras varios intentos sigues recibiendo 5xx, deja de martillar y pasa a verificar por otro camino: consulta el estado con GET /v2/payments/:id (usando el id si lo tienes) o revisa tu GET /v2/balance. Y recuerda que, aun cuando la petición eventualmente prospere, el pago resultante puede quedar en unknown — que es otra cosa y se resuelve por conciliación, no por reintento.
El árbol de decisión: ¿reintento o no?
Junta todo lo anterior en una sola regla mental. Cuando recibas una respuesta que no es 2xx, pregúntate esto en orden:
¿Es 400 / 401 / 404 / 422?
No reintentes igual. Son errores determinísticos de tu lado (request mal formado, firma inválida, recurso inexistente, regla de negocio violada). El mismo request dará el mismo error. Corrige la causa y recién entonces reenvía.
¿Es 409?
No reintentes a ciegas. Ya existe estado del otro lado. Si fue idempotencia con cuerpo distinto, arregla tu generación de keys. Si querías el resultado previo, consúltalo con un GET.
¿Es 5xx o un timeout de red?
Reintenta — con disciplina. Misma idempotency-key, mismo cuerpo, backoff exponencial, tope de intentos. Es el único caso donde reintentar es lo correcto, y la idempotencia es lo que lo hace seguro contra el doble-pago.
¿Fue 2xx pero el pago quedó en unknown?
Eso ya no es un error HTTP: es un estado de pago. No crees un pago nuevo sobre un unknown jamás. Se resuelve solo por conciliación de saldo del lado de Alibanca. Sigue el flujo de Estados de un pago →.
idempotency-key nueva. unknown significa "no sabemos todavía" — NO reintentes, no crees un pago nuevo, deja que la conciliación lo cierre. Confundir estos dos es la vía más rápida a un doble-pago. La conciliación de doble partida de Alibanca lo caza antes que tú, pero tu trabajo es no provocarlo.Pruébalo en sandbox
La mejor forma de blindar tu manejo de errores es provocarlos a propósito en el sandbox, donde el proveedor es simulado y no se mueve un centavo real. Genera un 400 mandando un pago sin amountMinor. Genera un 401 firmando con un nonce repetido. Genera un 409 reusando una idempotency-key con un cuerpo distinto. Y para el flujo unknown, usa los montos mágicos.
amountMinor dispara escenarios contra tu código real. Un monto terminado en .02 (por ejemplo "150002") deja el pago en unknown sin mover dinero. Uno terminado en .03 ("150003") mueve el dinero y igual devuelve unknown — es el "trap del doble-pago": si tu integración re-emite sobre ese unknown, acabas de pagar dos veces. Cualquier otro monto termina en completed. Verifica que tu código NO re-emite en ninguno de los dos casos unknown.Buenas prácticas de manejo de errores
Cerramos con la lista corta que separa una integración de juguete de una de producción:
Ramifica por código HTTP, no por texto
Tu lógica de reintento decide sobre el número de estado (4xx vs 5xx vs 2xx). El campo error es para tus logs y para el humano que depura, nunca para un if que compara la frase exacta — ese texto puede cambiar.
Registra siempre lo suficiente para depurar
Guarda en tus logs el código HTTP, el campo error, y en las respuestas de éxito el traceId. Ese traceId es lo que le pasas a soporte para que ubiquen tu petición exacta sin adivinar.
Genera idempotency-key determinísticas por operación lógica
Que la key salga de algo estable de tu dominio (el id de la orden, por ejemplo), no de un aleatorio por intento. Así, cuando un timeout te obligue a reintentar, la misma operación produce la misma key y la idempotencia te protege del duplicado.
Backoff exponencial + tope de intentos en los 5xx
Nunca reintentes en bucle apretado: espera cada vez un poco más y ríndete tras N intentos. Un servicio en recuperación agradece que no lo martilles, y tú evitas colgar tu propio proceso.
Verifica la conexión antes de firmar
Antes de armar tu primer envelope firmado, llama a GET /v2/version (es público, no requiere firma). Si eso ya te da error, el problema es de conectividad o de URL base, y te ahorras horas depurando una firma que en realidad estaba bien.
Con esto tienes el cuadro completo: una sola forma de error ({ "error": "…" } + código HTTP), un árbol de decisión claro por familia de código, y la coordinación entre la idempotencia y el estado unknown que mantiene tu dinero —y el de tus usuarios— a salvo de duplicados. Cuando termines de integrar el camino feliz, vuelve a esta página y asegúrate de que cada rama de error tenga su manejo. Ahí es donde se gana la confianza.