Reconcile your ledger
Page the event feed from a stored cursor to keep your ledger in step with every payout, then check the money against the balance transactions. Reconcile with the event feed builds the sync step by step, in Node and Python.
The event feed is every change to your payouts, in sequence order, complete
and replayable. Webhooks, which arrive in batches within about 15 seconds of a
transition, tell you when to read it.
The event feed
Section titled “The event feed”GET /payments/organizations/{orgId}/events returns everything after a cursor.
since is a sequence from a previous response, not a timestamp. On the first
call omit it or send since=0 (an empty since= is the same), then carry each
page’s nextSince. limit is 1 to 500 (default 100), payoutId narrows the
feed to one payout, and type takes a comma-separated list. An unknown type is
a 400 VALIDATION_ERROR, never an empty page.
A row’s data is the webhook body’s data, verbatim, and its
id is the delivery’s svix-id. It carries the amounts, fee, rate,
reference and endUserId, so you can book from the feed alone.
Payout, batch, approval, endpoint and checkout-payment events share one
sequence and are told apart by type (Webhook events).
A payout_batch.* row has batchId set and payoutId: null; a
checkout_payment.* row has no payoutId and a decimal-string fee. Pass
type= to book payouts only, or skip types you don’t recognize.
Events become readable about two seconds after they happen. A row can get its
sequence before its transaction commits, and the delay keeps a reader at the
edge of a page from skipping it. No status change makes a network call inside
its transaction, so two seconds is enough.
The feed is in sequence order, so events for one payout arrive in the order
they happened; webhooks can arrive out of order. An event is never updated in
place, and the feed replays from any cursor, so nothing is lost while your
service is down. Read it when a webhook arrives and on a schedule, because a
webhook you never received looks the same as one that never fired.
A receiving bank can return a payout days after it reported completed. The
feed appends a payout.returned event with status: failed and
failureCode: returned_by_bank; reverse the booking you made on completed.
Check fundsReturned before you credit anything back: a missing field means we
do not know yet, which is not the same as false.
Reconcile your balance
Section titled “Reconcile your balance”The event feed records state; the balance transactions feed records money. It has one row per change to what you can spend, newest first, each with the balance after it.
GET /payments/organizations/{orgId}/balance_transactions?type=payout,payout_return&limit=100x-api-key: avvio_live_…{ "data": [ { "id": "48213", "type": "payout", "amount": "-100.00", "fee": "0.50", "net": "-99.50", "currency": "USD", "balanceAfter": "9800.00", "orderId": "sbx_pay_7c1c0f2e-6d9b-4a3e-9d1a-2f4b8c9e0a11", "snapshotId": "sbx_quote_…", "batchId": null, "reference": "payroll-2026-09", "endUserId": "emp_412", "reason": null, "description": null, "createdAt": "2026-09-01T10:00:00.000Z" } ], "hasMore": true, "nextCursor": "48213"}| Field | Meaning |
|---|---|
amount |
Signed change to what you can spend, fee included. Positive for funding, payout_return and hold_release; negative for payout and hold; either for adjustment |
fee |
The part of amount that was the fee. null means the network published none: unknown, not zero |
net |
|amount| − fee with amount’s sign, the part converted for the recipient. Present only when fee is known |
balanceAfter |
Your balance after this row |
A 50.00 USD payout with a 0.26 fee is one row (amount: -50.00,
fee: 0.26, net: -49.74), with no separate fee row. A payout_return row’s
amount is what came back: the full 50.00 in the sandbox. On a live rail,
whether the fee is kept on a return depends on the rail, and this row shows it.
To reconcile:
- Pull newest first until you reach a row you have. Rows are append-only and
idis monotonic, socursor=<the last id you saw>returns strictly older rows. Stop whenhasMoreis false or a page holds only known rows. - Upsert by
id, neverorderId: one payout produces several rows (a hold, its release, the debit, a later return). - Check
balanceAfter, not the total. The first row where your running balance disagrees is where the difference started. - Join to payouts on
orderId, and to the event feed for when a state changed.
Compare against the network-held part of GET /balance (provider[], or
ledger[].available), not the amount headline, which also counts your
wallet. A shortfall there is a hold: a hold row until it becomes a payout
(hold_release plus payout, net zero) or is released (hold_release alone).
A hold is placed before the network answers, so its orderId is null. Every
row sized by a quote snapshot carries its snapshotId: the hold, its
hold_release and the payout. Join the hold to its payout on snapshotId,
then read orderId off the payout row. On the two-step flow, snapshotId is
the id from POST /quotes/offramp. funding and adjustment rows have
snapshotId: null. The feed is served on every environment, and an empty page
is not an error.
Cursors and lists
Section titled “Cursors and lists”GET /events |
GET /orders |
|
|---|---|---|
| Ordering | Monotonic sequence |
createdAt descending; updatedSince switches to oldest-changed-first |
| A change to an old payout | Appended as a new event | Not reordered by default |
| Pagination | since=<sequence>, then nextSince |
cursor=<nextCursor>, passed back verbatim |
| Use it for | Automated ledger sync | Dashboard history and search |
Both allow 600 requests a minute. The /orders cursor is opaque: pass the
nextCursor you were given back unchanged. A value we didn’t issue, such as a
bare payout id, is a 400 VALIDATION_ERROR (cursor: unrecognized). The other
lists (/balance_transactions, /audit-events) page by cursor=<the last id you saw>, then nextCursor.
Unknown query parameters are ignored, not refused. cursor sent to /events
reads from the start of the feed, and since sent to a cursor list is
dropped, so check the parameter name if a page looks wrong.
Was this page helpful?