El estado unknown
Cuando mueves dinero, la mayoría de la gente cree que hay dos finales posibles: el pago salió o el pago no salió. Pero existe un tercero, y es justo donde la industria de pagos pierde dinero de verdad: no sabemos todavía. El banco no respondió a tiempo, se cayó la conexión o hubo un timeout, y en ese instante nadie —ni tú ni nosotros— puede afirmar si el dinero se movió o no. Alibanca trata ese tercer final como un estado de primera clase llamado unknown. Esta página te explica, desde cero, qué significa, por qué existe, cuál es la regla de oro para manejarlo y cómo tu integración debe reaccionar para que un timeout nunca se convierta en un doble-pago.
Mover dinero tiene tres desenlaces, no dos
Imagínate que le entregas un paquete a un mensajero para que lo lleve a una dirección. Cuando lo mandas, pueden pasar tres cosas, no dos:
Uno. El mensajero regresa y te dice "entregado, aquí está la firma de quien lo recibió". Sabes con certeza que el paquete llegó. Eso es completed.
Dos. El mensajero regresa y te dice "no pude entregarlo, aquí te devuelvo el paquete intacto". No llegó, pero tampoco perdiste nada: el paquete está de vuelta en tus manos, completo. Eso es failed.
Tres. El mensajero sale con el paquete… y el rastreo se apaga en el camino. No volvió, no te llamó, no sabes si lo entregó, si se le quedó guardado o si va en camino. No lo sabes. Eso es unknown.
El error clásico —el que hace perder dinero— es tratar ese tercer caso como si fuera el segundo. Si asumes "como no me confirmaron, seguro no llegó" y mandas otro mensajero con un paquete idéntico, y resulta que el primero sí había entregado, acabas de pagar dos veces. En dinero real, ese paquete duplicado es plata que se fue y no vuelve. Por eso unknown no es un error ni un estado "raro": es una respuesta legítima y valiosa que significa exactamente "espera, todavía no puedo decírtelo con certeza".
unknown llega dentro de una respuesta HTTP exitosa (por ejemplo 201), no dentro de un error. Recibir unknown significa que la API funcionó correctamente y te está reportando, con honestidad, que el resultado del movimiento aún no está confirmado. Un error (cuerpo { "error": "…" }) es otra cosa: es que la petición misma no se procesó.Por qué unknown es un estado de primera clase
La tentación de cualquier sistema es esconder la incertidumbre. Muchas plataformas, cuando el banco no responde, devuelven un error genérico o directamente marcan el pago como fallido para "cerrar el caso". Eso es cómodo para la máquina y peligroso para tu dinero, porque convierte un "no sé" en un "no pasó" — y esas dos frases significan cosas opuestas para tu integración.
Alibanca hace lo contrario: eleva la incertidumbre a un estado propio, con nombre, que viaja en el campo state del payload junto a un subStatus que da más contexto. Modelar el "no sé" explícitamente es lo que te permite tomar la decisión correcta —no reintentar— en vez de la decisión cómoda y equivocada. En sistemas de dinero, la honestidad sobre lo que no se sabe vale más que una respuesta rápida pero falsa.
Detrás de escena, cada unknown entra a un proceso de conciliación continua que cuadra el saldo real del banco contra el ledger de doble partida de Alibanca. Ese proceso es el que, con el tiempo, resuelve el suspenso: descubre si el dinero se movió o no y actualiza el estado del pago. Tú no tienes que adivinar; el sistema lo averigua por ti. Puedes leer cómo funciona en Conciliación →.
La regla de oro
Si de toda esta página te llevas una sola idea, que sea esta. Es la línea que separa una integración segura de una que duplica pagos:
failed garantiza que NO hubo débito — es seguro reintentar, siempre con una idempotency-key nueva. unknown significa NO reintentar — nunca crees un pago nuevo encima; se resuelve solo por conciliación de saldo.Desglosémosla en cristiano. Cuando un pago termina en failed, Alibanca te está dando una promesa fuerte: el dinero no se movió, punto. El destinatario no recibió nada, tu saldo no bajó. Como sabes con certeza que no pasó nada, puedes intentarlo de nuevo con tranquilidad; usas una idempotency-key nueva porque es, conceptualmente, un pago fresco.
Cuando un pago termina en unknown, no tienes esa promesa. El dinero pudo haberse movido o no. Reintentar aquí sería como mandar el segundo mensajero sin saber si el primero ya entregó: te expones al doble-pago. Por eso la regla es tajante — sobre un unknown no se crea jamás un pago nuevo. Te quedas quieto y dejas que la conciliación resuelva el estado real.
Los tres estados, uno por uno
completed El dinero salió
El banco confirmó el movimiento. El destinatario recibió, tu saldo refleja el débito y el caso está cerrado con éxito. No hay nada que hacer salvo, si quieres, marcar la operación como exitosa en tu sistema. Verás state: "completed" tanto al consultar el pago (GET /v2/payments/:id) como en el webhook correspondiente.
failed El dinero no salió
Hubo un rechazo confirmado: fondos insuficientes, destino inválido, el banco dijo "no". Lo clave es la garantía de no-débito: no se cobró nada. Es el único estado sobre el que reintentar es seguro, y siempre con idempotency-key nueva. El subStatus te dice el motivo del rechazo para que decidas si vale la pena reintentar o corregir algo antes.
unknown No sabemos todavía
El banco no respondió a tiempo (timeout), se cayó, o cortó la conexión en el peor momento. El resultado del movimiento quedó en el aire. No reintentes. Deja que la conciliación lo resuelva y consulta el estado por poll o espera el webhook: el mismo pago pasará, más tarde, a completed o a failed cuando se aclare la realidad. El subStatus te da pistas de por qué quedó en suspenso (por ejemplo, un timeout del proveedor).
Por qué nunca se crea un pago nuevo sobre un unknown
Aquí está el corazón del asunto. Un unknown abarca dos realidades que, desde tu lado, se ven idénticas pero son opuestas:
Realidad A — el banco recibió la orden, movió el dinero y después se cayó la conexión antes de poder confirmártelo. El dinero ya salió; solo falta que te lleguen las noticias.
Realidad B — el banco nunca llegó a procesar la orden. El dinero no se movió.
Desde afuera no puedes distinguir A de B en el momento. Si asumes que estás en B y emites otro pago, pero en realidad estabas en A, el destinatario recibe dos veces. Ese es el trap del doble-pago, y es exactamente la trampa que la industria cae. Por eso la disciplina de Alibanca es no dejar que un unknown engendre un pago nuevo: el mismo pago original tiene que resolverse, no reemplazarse.
failed o para recuperarte de un corte de red en tu propio lado. Pero no es una excusa para "reintentar" un unknown: sobre un unknown te quedas quieto, no repites nada.Cómo manejar cada estado en tu integración
Tu integración tiene dos formas de enterarse del estado final de un pago, y lo recomendable es usar las dos juntas: el poll (tú preguntas) y el webhook (nosotros te avisamos). Se complementan: el webhook te da la noticia al instante en que cambia el estado, y el poll es tu red de respaldo por si un webhook se pierde. Este es el flujo mental que debes implementar:
Emites el pago y lees el state de la respuesta
Haces POST /v2/payments. La respuesta ya trae un state. Si es completed, listo. Si es failed, es seguro reintentar con idempotency-key nueva. Si es unknown, pasas al modo espera — sin reintentar.
Ante un unknown, esperas — no reintentas
Guarda el id del pago y su unknown en tu sistema como "en conciliación". No emitas nada nuevo con ese destino y ese monto por esa operación lógica. La pelota está en la cancha de la conciliación.
Escuchas el webhook
Configuraste tu endpoint con PUT /v2/webhook. Cuando la conciliación resuelve el pago, te llega un evento firmado con el nuevo state. Verificas la firma con la llave pública del JWKS → (GET /v2/webhook/keys) — sin secreto compartido — y actualizas tu registro.
Haces poll como respaldo
Cada tanto, consulta GET /v2/payments/:id para los pagos que sigan en unknown. Si por lo que sea no te llegó el webhook, el poll te trae el estado actualizado igual. Nunca dependas de un solo canal para dinero.
Fíjate que en ningún paso de este flujo tu código "reintenta" un unknown. La resolución llega sola, por conciliación, y tú simplemente te enteras — por webhook, por poll, o por ambos.
Un request que cae en unknown
Veámoslo en vivo. Este es un POST /v2/payments por pago móvil (rail: "PM") hacia un teléfono, por un monto en unidades menores (céntimos, como string en amountMinor). En el sandbox, el céntimo del monto dispara escenarios contra tu código real: un monto que termina en .02 —aquí 150002— fuerza deliberadamente un desenlace unknown para que puedas ensayar tu manejo del timeout.
curl -X POST https://alibanca-api-production.up.railway.app/v2/payments \ -H "x-api-key: sk_test_..." \ -H "x-signature: <firma RSA-SHA256 del envelope canónico>" \ -H "idempotency-key: pago-nomina-abril-0031" \ -H "Content-Type: application/json" \ -d '{ "amountMinor": "150002", "currency": "VES", "rail": "PM", "beneficiary": "04141234567" }'
La API responde con éxito (201) — recuerda, unknown no es un error — y el payload te dice, con toda honestidad, que el resultado quedó en suspenso:
{ "status": "success", "traceId": "trc_9f2c7a10...", "payload": { "id": "pay_01HZK8Q...", "state": "unknown", "subStatus": "provider_timeout", "amountMinor": "150002", "currency": "VES", "rail": "PM" } }
Frente a esta respuesta, tu código correcto no vuelve a llamar a POST /v2/payments. Guarda el id, lo marca "en conciliación" y espera. Más tarde, ese mismo pay_01HZK8Q… aparecerá como completed o failed — por webhook y por poll — cuando la conciliación aclare la realidad.
Pruébalo en el sandbox con montos mágicos
El sandbox de Alibanca usa un proveedor simulado: no mueve un solo céntimo real, así que puedes ensayar los peores escenarios sin miedo. La forma de disparar cada desenlace es el céntimo del monto. Es la manera de comprobar que tu integración maneja unknown como debe antes de tocar dinero de verdad.
.02 (ej. 150002) → queda en unknown por timeout. Monto que termina en .03 (ej. 150003) → el dinero "se mueve" pero igual devuelve unknown: es el trap del doble-pago, ahí verificas que tu integración NO re-emite el pago. Cualquier otro monto → completed.El escenario .03 es el más didáctico de todos, porque reproduce la Realidad A que vimos antes: el dinero sí salió pero la confirmación se perdió. Si tu código, al ver ese unknown, comete el error de reintentar, en producción habrías pagado dos veces. Aquí, en el sandbox, lo detectas gratis. Aprovéchalo.
La conciliación te cubre las espaldas
Quizá te preguntes: "si me quedo quieto ante un unknown, ¿quién resuelve el suspenso?". La respuesta es la conciliación, que corre de forma continua cuadrando el saldo real del banco contra el ledger de doble partida de Alibanca. Ese cruce es lo que caza los casos peligrosos antes que tú:
Detecta un débito bancario que no tiene un payout dueño (un ORPHAN, que escala como excepción de máxima prioridad); un crédito que aparece sin fondeo (una devolución); un doble-débito byte-idéntico; o un payout que quedó "confirmado" sin débito real detrás. En otras palabras: el sistema vigila el dinero por ti y no descansa hasta que cada unknown queda resuelto y cada céntimo cuadra. Por eso puedes darte el lujo de no reaccionar en caliente: la red de seguridad es estructural, no depende de que tú adivines bien.
Errores comunes y buenas prácticas
Error #1 — tratar unknown como failed. Es el más caro. "No me confirmaron, seguro no pasó" → emites de nuevo → doble-pago. Nunca asumas no-débito sobre un unknown; esa garantía solo la da failed.
Error #2 — reintentar un unknown con idempotency-key nueva. Una clave nueva le dice a Alibanca "esto es una operación distinta", así que sí crearía un pago nuevo — justo lo que no quieres. Sobre un unknown no reintentas ni con clave vieja ni con clave nueva: esperas.
Error #3 — depender de un solo canal. Si solo escuchas webhooks y uno se pierde, un pago se te queda colgado en unknown para siempre en tu sistema. Combina webhook (rápido) con poll de respaldo (seguro) para los que sigan en suspenso.
Error #4 — no persistir el state. Guarda el id y el state de cada pago en tu base de datos apenas los recibes. Así, ante cualquier duda, tu fuente de verdad es tu registro más el GET /v2/payments/:id, no la memoria de un proceso que se pudo reiniciar.
failed → reintenta con idempotency-key nueva. unknown → no reintentes, deja que la conciliación lo resuelva y entérate por poll + webhook. Un timeout bien manejado no cuesta un centavo; uno mal manejado cuesta el pago entero, dos veces.Con esto ya sabes lo esencial de la capa de money-safety de Alibanca. Para profundizar, sigue con Idempotency-key →, Firma de requests →, Webhooks → y Conciliación →.