Skip to content

Track the payment to its outcome

POST /payouts almost always returns submitted. The outcome comes later.

We send a POST to your 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 When
payment.settled The payout is final: the beneficiary has their Bs
payment.failed It was not executed. The entry is already reversed
payment.reversed The bank returned a payout that had already settled

It is sent only once per payout.

Before processing, verify the signature and discard duplicates:

Node — minimal receiver
app.post('/webhooks/dispersion', express.raw({ type: '*/*' }), async (req, res) => {
const ts = req.headers['x-timestamp'];
const raw = req.body; // the BYTES, not the re-serialized object
// 1 · Signature
const expected = crypto.createHmac('sha256', WHSEC).update(`${ts}.${raw}`).digest('hex');
const signature = String(req.headers['x-signature']).replace('sha256=', '');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) return res.sendStatus(401);
// 2 · Anti-replay: ±5 minutes
if (Math.abs(Date.now() - Number(ts)) > 300_000) return res.sendStatus(401);
const event = JSON.parse(raw);
if (await alreadyProcessed(event.id)) return res.sendStatus(200); // 3 · deduplicate
res.sendStatus(200); // 4 · respond NOW
await enqueue(event);
});

Headers, signature and retries: 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": "payout_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" }
]
}

The typical path: the entry is committed, the transfer is accepted asynchronously (still submitted) and it closes on re-check. Seconds or minutes pass between the second and third events.

The receipt the beneficiary sees at their bank. It is null until the transfer completes, and it is the only transfer identifier we publish. To open a ticket with us, use the payment id.

Each transition, with its actor (who caused it) and its reason. The list of values is in payment states.

Ventana de terminal
# 1 · Check whether we sent it: each delivery, with its HTTP code and latency
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 · Your failed queue: what did not arrive is neither lost nor considered delivered
curl -s "$BASE/v1/events?status=dead" -H "x-api-key: $KEY"
# 3 · Request redelivery, one or all
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"

The attempt-by-attempt detail is in webhooks.

Do not poll per payment. As a safety net, a periodic sweep by status is enough — submitted (settling), unknown (in doubt, do not retry) and failed:

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

Lists are cursor-paginated, maximum 100.