Track the payment to its outcome
POST /payouts almost always returns submitted. The outcome comes later.
1 · Receive the webhook
Section titled “1 · Receive the webhook”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:
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.
2 · Read the full state
Section titled “2 · Read the full state”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": "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.
bank_reference
Section titled “bank_reference”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.
The history (events[])
Section titled “The history (events[])”Each transition, with its actor (who caused it) and its reason. The list
of values is in payment states.
3 · And if the webhook does not arrive
Section titled “3 · And if the webhook does not arrive”# 1 · Check whether we sent it: each delivery, with its HTTP code and latencycurl -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 deliveredcurl -s "$BASE/v1/events?status=dead" -H "x-api-key: $KEY"
# 3 · Request redelivery, one or allcurl -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.
A fallback sweep, not polling
Section titled “A fallback sweep, not polling”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:
curl -s "$BASE/v1/payments?status=submitted&limit=100" -H "x-api-key: $KEY"Lists are cursor-paginated, maximum 100.