Seguir el pago hasta el desenlace
POST /payouts devuelve submitted casi siempre. El desenlace llega después.
1 · Recibe el webhook
Sección titulada «1 · Recibe el webhook»Te enviamos un POST a tu webhookUrl:
{ "id": "40545", "eventType": "payment.settled", "attempt": 1, "payload": { "paymentId": "89920401-26da-4f69-b57d-bc20e0366ce2", "asset": "BS", "amount": "5000000", "bankReference": "012345678901", "method": "pago_movil" } }eventType |
Cuándo |
|---|---|
payment.settled |
La dispersión quedó firme: el beneficiario tiene sus Bs |
payment.failed |
No se ejecutó. El asiento ya está revertido |
payment.reversed |
El banco devolvió una dispersión que ya estaba liquidada |
Sale una sola vez por dispersión.
Antes de procesar, verifica la firma y descarta duplicados:
app.post('/webhooks/dispersion', express.raw({ type: '*/*' }), async (req, res) => { const ts = req.headers['x-timestamp']; const raw = req.body; // los BYTES, no el objeto reserializado
// 1 · Firma const esperado = crypto.createHmac('sha256', WHSEC).update(`${ts}.${raw}`).digest('hex'); const firma = String(req.headers['x-signature']).replace('sha256=', ''); if (!crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(firma))) return res.sendStatus(401);
// 2 · Anti-replay: ±5 minutos if (Math.abs(Date.now() - Number(ts)) > 300_000) return res.sendStatus(401);
const evento = JSON.parse(raw); if (await yaProcesado(evento.id)) return res.sendStatus(200); // 3 · deduplica
res.sendStatus(200); // 4 · responde YA await encolar(evento);});Cabeceras, firma y reintentos: webhooks.
2 · Lee el estado completo
Sección titulada «2 · Lee el estado completo»curl -s $BASE/v1/payments/89920401-26da-4f69-b57d-bc20e0366ce2 -H "x-api-key: $KEY"{ "id": "89920401-26da-4f69-b57d-bc20e0366ce2", "customer_id": null, "beneficiary_id": "8ade3e33-6f1c-4d0a-9d33-2c6b0a41f7e2", "status": "settled", "status_reason": null, "payout_asset": "BS", "payout_amount": "5000000", "fee_amount": "50000", "source_asset": "BS", "source_amount": "5050000", "method": "pago_movil", "bank_reference": "012345678901", "compensated": false, "settled_at": "2026-08-31T19:06:44.120Z", "created_at": "2026-08-31T19:04:01.812Z", "events": [ { "from_status": null, "to_status": "submitted", "reason": "ledger_committed", "actor": "api", "created_at": "2026-08-31T19:04:01.900Z" }, { "from_status": "submitted", "to_status": "submitted", "reason": "rail_accepted_async", "actor": "api", "created_at": "2026-08-31T19:04:02.551Z" }, { "from_status": "submitted", "to_status": "settled", "reason": "resolved_by_reaper", "actor": "reaper", "created_at": "2026-08-31T19:06:44.120Z" } ]}El recorrido típico: el asiento se confirma, el envío se acepta de forma
asíncrona (sigue en submitted) y se cierra al reconsultar. Entre el segundo
y el tercer evento pasan segundos o minutos.
bank_reference
Sección titulada «bank_reference»El comprobante que el beneficiario ve en su banco. Es null hasta que el envío
se completa, y es el único identificador del envío que publicamos. Para abrir un
ticket con nosotros, usa el id del pago.
La historia (events[])
Sección titulada «La historia (events[])»Cada transición, con su actor (quién la provocó) y su reason. La lista de
valores está en estados de un pago.
3 · Y si el webhook no llega
Sección titulada «3 · Y si el webhook no llega»# 1 · Mira si lo enviamos: cada entrega, con su código HTTP y su latenciacurl -s "$BASE/v1/events?eventType=payment.settled&limit=50" -H "x-api-key: $KEY"curl -s "$BASE/v1/events/40545" -H "x-api-key: $KEY"
# 2 · Tu cola de fallidos: lo que no llegó no se pierde ni se da por entregadocurl -s "$BASE/v1/events?status=dead" -H "x-api-key: $KEY"
# 3 · Pide la reentrega, uno o todoscurl -s -X POST "$BASE/v1/events/40545/retry" -H "x-api-key: $KEY"curl -s -X POST "$BASE/v1/events/retry-dead" -H "x-api-key: $KEY"El detalle intento a intento está en webhooks.
Barrido de respaldo, no polling
Sección titulada «Barrido de respaldo, no polling»No hagas polling por pago. Como red de seguridad basta un barrido periódico por
status — submitted (liquidando), unknown (en duda, no reintentar) y
failed:
curl -s "$BASE/v1/payments?status=submitted&limit=100" -H "x-api-key: $KEY"Los listados van paginados por cursor, máximo 100.