---
updatedAt: 2026-09-30T15:54:20.000Z
---

Fetch the complete documentation index at: https://docs.avvio.xyz/llms.txt. Use this file to discover all available pages before exploring further. Append .md to any documentation page URL to get its markdown version.

# Reconcile with the event feed

## Store a cursor

You need the three variables from the [Quickstart](/quickstart/). The code uses PostgreSQL; any database with transactions works. [Reconcile your ledger](/reconciliation/) explains the feed this builds on.

The only state you keep is the last `sequence` you processed and the ids of the events you applied. `sequence` is a decimal string: store it as text, or as an integer wide enough for 64 bits. Start at `0`, which reads the feed from its first row.

```sql title="schema.sql"
CREATE TABLE avvio_cursor (id int PRIMARY KEY, since text NOT NULL);
INSERT INTO avvio_cursor VALUES (1, '0');

CREATE TABLE avvio_applied_events (event_id text PRIMARY KEY);
```

## Read a page

Ask for everything after your cursor, then carry each page's `nextSince`. `since` is a sequence, not a timestamp. `limit` goes up to 500, and `type=` keeps the page to payout events.

A row's `data` is the webhook body's `data`, verbatim, so you can book from the feed alone ([what a row carries](/reconciliation/#the-event-feed)).

```bash title="GET /payments/organizations/{orgId}/events" {1}
curl -s "$AVVIO_BASE_URL/payments/organizations/$AVVIO_ORG_ID/events?since=40&limit=500&type=payout.pending,payout.processing,payout.completed,payout.failed,payout.returned,payout.canceled" \
  -H "x-api-key: $AVVIO_API_KEY"
```

```js title="GET /payments/organizations/{orgId}/events" {5-8}
import { PayoutsClient } from '@avvio/payments';

const avvio = new PayoutsClient(); // reads AVVIO_API_KEY, AVVIO_ORG_ID and AVVIO_BASE_URL
const page = await avvio.listEvents({
  since: '40',
  limit: 500,
  type: ['payout.pending', 'payout.processing', 'payout.completed',
         'payout.failed', 'payout.returned', 'payout.canceled'],
});
```

```python title="GET /payments/organizations/{orgId}/events" {7-10}
import os, requests

res = requests.get(
    f"{os.environ['AVVIO_BASE_URL']}/payments/organizations/{os.environ['AVVIO_ORG_ID']}/events",
    headers={"x-api-key": os.environ["AVVIO_API_KEY"]},
    params={
        "since": "40",
        "limit": 500,
        "type": "payout.pending,payout.processing,payout.completed,"
                "payout.failed,payout.returned,payout.canceled",
    },
)
res.raise_for_status()
page = res.json()
```

```json title="Response"
{
  "data": [
    {
      "id": "cmf3k2x9a0001q8b7h4d2e6zt",
      "sequence": "41",
      "type": "payout.processing",
      "payoutId": "sbx_pay_7c1c0f2e-6d9b-4a3e-9d1a-2f4b8c9e0a11",
      "batchId": null,
      "status": "processing",
      "createdAt": "2026-08-17T09:12:00.000Z",
      "apiVersion": 1,
      "data": {
        "payoutId": "sbx_pay_7c1c0f2e-6d9b-4a3e-9d1a-2f4b8c9e0a11",
        "status": "processing",
        "sourceCurrency": "USD",
        "sourceAmount": "200.00",
        "destinationCurrency": "MXN",
        "destinationAmount": "3384.65",
        "destinationAccountId": "sbx_acct_MXN_4471_ae66cbc5",
        "fee": { "currency": "USD", "amount": "1.02" },
        "rate": "16.923250",
        "reference": "ZZ-2026-0042",
        "endUser": { "id": "customer_42" },
        "endUserId": "customer_42",
        "createdAt": "2026-08-17T09:11:58.000Z",
        "completedAt": null
      }
    }
  ],
  "hasMore": false,
  "nextSince": "41"
}
```

## Apply each event once

Write the event id, your ledger change and the new cursor in one database transaction. If the process dies halfway, the next run re-reads the page and skips what it already applied.

**Cursor on `sequence`, dedupe on `id`.** A timestamp cursor drops events and delivery is at least once ([why](/reconciliation/#the-event-feed)). The `id` is the webhook's `svix-id`, so one dedupe table serves both.

```js title="Sync" {16-18,20}
// sync.js
const BASE = `${process.env.AVVIO_BASE_URL}/payments/organizations/${process.env.AVVIO_ORG_ID}`;
const TYPES = 'payout.pending,payout.processing,payout.completed,payout.failed,payout.returned,payout.canceled';

export async function syncOnce(db) {
  let { since } = await db.one('SELECT since FROM avvio_cursor WHERE id = 1');
  for (;;) {
    const res = await fetch(`${BASE}/events?since=${since}&limit=500&type=${TYPES}`, {
      headers: { 'x-api-key': process.env.AVVIO_API_KEY },
    });
    if (!res.ok) throw new Error(`events: ${res.status}`);
    const page = await res.json();

    await db.tx(async (tx) => {
      for (const event of page.data) {
        const fresh = await tx.result(
          'INSERT INTO avvio_applied_events VALUES ($1) ON CONFLICT DO NOTHING', [event.id]);
        if (fresh.rowCount === 1) await applyToLedger(tx, event); // your booking logic
      }
      await tx.none('UPDATE avvio_cursor SET since = $1 WHERE id = 1', [page.nextSince]);
    });

    since = page.nextSince;
    if (!page.hasMore) return;
  }
}
```

```python title="Sync" {22-26}
# sync.py. `conn` is a psycopg 3 connection opened with autocommit=True,
# so each `conn.transaction()` block commits on its own.
import os, requests

BASE = f"{os.environ['AVVIO_BASE_URL']}/payments/organizations/{os.environ['AVVIO_ORG_ID']}"
TYPES = "payout.pending,payout.processing,payout.completed,payout.failed,payout.returned,payout.canceled"

def sync_once(conn):
    (since,) = conn.execute("SELECT since FROM avvio_cursor WHERE id = 1").fetchone()
    while True:
        res = requests.get(
            f"{BASE}/events",
            headers={"x-api-key": os.environ["AVVIO_API_KEY"]},
            params={"since": since, "limit": 500, "type": TYPES},
            timeout=30,
        )
        res.raise_for_status()
        page = res.json()

        with conn.transaction():
            for event in page["data"]:
                fresh = conn.execute(
                    "INSERT INTO avvio_applied_events VALUES (%s) ON CONFLICT DO NOTHING", (event["id"],))
                if fresh.rowcount == 1:
                    apply_to_ledger(conn, event)  # your booking logic
            conn.execute("UPDATE avvio_cursor SET since = %s WHERE id = 1", (page["nextSince"],))

        since = page["nextSince"]
        if not page["hasMore"]:
            return
```

## Handle a return after completed

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`. Route it straight to a reversal of the booking you made on `completed`.

**Check `fundsReturned` before you credit anything back.** `true` means the money is on your balance again. A missing field means we do not know yet, which is not the same as `false`.

In the sandbox, pay an account number ending in `0003` to get this event about 30 seconds after `payout.completed`.

```json title="Response"
{
  "id": "cmf3k2xd70003q8b7n6v0p2ky",
  "sequence": "43",
  "type": "payout.returned",
  "payoutId": "sbx_pay_7c1c0f2e-6d9b-4a3e-9d1a-2f4b8c9e0a11",
  "batchId": null,
  "status": "failed",
  "failureCode": "returned_by_bank",
  "fundsReturned": true,
  "createdAt": "2026-08-19T14:02:11.000Z",
  "apiVersion": 1,
  "data": { "payoutId": "sbx_pay_7c1c0f2e-6d9b-4a3e-9d1a-2f4b8c9e0a11", "status": "failed", "failureCode": "returned_by_bank", "fundsReturned": true }
}
```

## Resume after downtime

Nothing is lost while your service is down. The feed replays from any cursor, so on restart `syncOnce` pages forward until `hasMore` is `false`.

Run `syncOnce` when a webhook arrives and on a schedule, because a webhook you never received looks the same as one that never fired. `GET /events` allows 600 requests a minute. [Receive and verify webhooks](/recipes/receive-and-verify-webhooks/) sets up the trigger, and [Reconcile your ledger](/reconciliation/#reconcile-your-balance) checks the money against the balance transactions.
