Skip to content

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.

Webhook event received Fast, but not guaranteed Read event feed GET /events?since= Deduplicate By event ID Update internal ledger Store the nextSince cursor Trigger

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.

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=100
x-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:

  1. Pull newest first until you reach a row you have. Rows are append-only and id is monotonic, so cursor=<the last id you saw> returns strictly older rows. Stop when hasMore is false or a page holds only known rows.
  2. Upsert by id, never orderId: one payout produces several rows (a hold, its release, the debit, a later return).
  3. Check balanceAfter, not the total. The first row where your running balance disagrees is where the difference started.
  4. 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.

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?