Ir al contenido

Seguir el pago hasta el desenlace

POST /payouts devuelve submitted casi siempre. El desenlace llega después.

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:

Node — receptor mínimo
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.

Ventana de terminal
curl -s $BASE/v1/payments/89920401-26da-4f69-b57d-bc20e0366ce2 -H "x-api-key: $KEY"
200 OK
{
"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.

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.

Cada transición, con su actor (quién la provocó) y su reason. La lista de valores está en estados de un pago.

Ventana de terminal
# 1 · Mira si lo enviamos: cada entrega, con su código HTTP y su latencia
curl -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 entregado
curl -s "$BASE/v1/events?status=dead" -H "x-api-key: $KEY"
# 3 · Pide la reentrega, uno o todos
curl -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.

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:

Ventana de terminal
curl -s "$BASE/v1/payments?status=submitted&limit=100" -H "x-api-key: $KEY"

Los listados van paginados por cursor, máximo 100.