> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tolt.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sync your data

> Mirror partners, customers and commissions into your database with webhooks plus a polling backfill.

Use both: webhooks keep you current within seconds, and a scheduled poll catches anything a webhook missed.

Your databaseupsert by idYour serverwebhook + cronToltplatform.tolt.com*1*POST partner.updated*2*upsert part\_8fKq…*3*GET ?updated\_after=…*4*200 data\[], next\_cursor*5*upsert missed rows

Both paths upsert by `id` into the same tables, so it does not matter which one gets there first.

## Handle webhooks

Add an endpoint in **Dashboard → Settings → Webhooks** and copy its signing secret (`whsec_...`). Verify every request with `@tolt/webhooks`, then upsert.

```bash theme={"system"}
npm install @tolt/webhooks
```

```ts app/api/webhooks/tolt/route.ts theme={"system"}
import {
  verifyWebhook,
  WebhookVerificationError,
  type ToltWebhookEvent,
} from "@tolt/webhooks";
import { db } from "@/lib/db"; // your database

export async function POST(req: Request) {
  let event: ToltWebhookEvent;
  try {
    event = verifyWebhook({
      payload: await req.text(),
      headers: req.headers,
      secret: process.env.TOLT_WEBHOOK_SECRET!,
    });
  } catch (err) {
    if (err instanceof WebhookVerificationError) {
      return new Response("Invalid signature", { status: 400 });
    }
    throw err;
  }

  const eventId = req.headers.get("svix-id")!;
  if (await db.webhookEvents.exists(eventId)) {
    return new Response("Duplicate");
  }

  const [resource] = event.type.split(".");
  // Keyed by data.id, skipped when updated_at is older
  await db.upsertIfNewer(resource, event.data);
  await db.webhookEvents.insert(eventId);

  return new Response("OK");
}
```

> [!WARNING]
>
> Deliveries are retried, so the same event can arrive more than once. Dedupe on the `svix-id` header. Deliveries can also arrive out of order: only overwrite a row when the incoming `updated_at` is newer.

Events cover `partner`, `link`, `customer`, `transaction` and `commission`, each with `.created` and `.updated`. There are no delete events, see [Handle deletions](#handle-deletions). API refunds change commissions without a `commission.*` event, which is one more reason to poll. Test locally with the CLI, setting `TOLT_WEBHOOK_SECRET` to the signing secret `tolt listen` prints (see [Local development](/webhooks/local-development)):

```bash theme={"system"}
tolt listen --forward-to http://localhost:3000/api/webhooks/tolt
tolt trigger commission.created
```

## Backfill and reconcile

Page through a resource ordered by `updated_at`, oldest first, between your last cutoff and now. Store the cutoff after each run. The first run, with no `updated_after`, is your full backfill.

```ts TypeScript theme={"system"}
import { ToltApiClient } from "@tolt/api";
import { db } from "./db"; // your database

const tolt = new ToltApiClient({ token: process.env.TOLT_API_KEY! });

export async function syncCommissions() {
  // undefined on the first run
  const checkpoint = await db.getCheckpoint("commissions");
  // Start 10 minutes before the last cutoff, in case this
  // machine's clock runs ahead of Tolt's.
  const since =
    checkpoint &&
    new Date(Date.parse(checkpoint) - 10 * 60 * 1000).toISOString();
  const until = new Date(Date.now() - 5 * 60 * 1000).toISOString();

  const commissions = await tolt.commissions.list({
    sort: "updated_at",
    order: "asc",
    updated_after: since,
    updated_before: until,
    limit: 100,
  });
  for await (const commission of commissions)
    await db.upsertIfNewer("commission", commission);

  await db.setCheckpoint("commissions", until);
}
```

```python Python theme={"system"}
import os
from datetime import datetime, timedelta, timezone
from tolt import ToltApi
from db import db  # your database

client = ToltApi(token=os.environ["TOLT_API_KEY"])


def sync_commissions():
    checkpoint = db.get_checkpoint("commissions")  # None on the first run
    # Start 10 minutes before the last cutoff, in case this
    # machine's clock runs ahead of Tolt's.
    since = checkpoint - timedelta(minutes=10) if checkpoint else None
    until = datetime.now(timezone.utc) - timedelta(minutes=5)

    commissions = client.commissions.list(
        sort="updated_at",
        order="asc",
        updated_after=since,
        updated_before=until,
        limit=100,
    )
    for commission in commissions:
        db.upsert_if_newer("commission", commission)

    db.set_checkpoint("commissions", until)
```

The five-minute lag leaves time for in-flight writes to commit, and the fixed `updated_before` keeps the window steady while you page. Each run re-reads the last ten minutes, so a clock that drifts ahead of Tolt's can't skip rows; `upsertIfNewer` makes the repeats harmless. Run the same loop for `partners`, `links`, `customers` and `transactions`.

> **Why the window has an upper bound**
>
> `updated_after` is inclusive and `updated_before` is exclusive. A row's `updated_at` can become visible slightly after the time it carries; ending the window a few minutes in the past means every row in it has already committed. See [Pagination](/api-reference/pagination) for the full rules.

## Handle deletions

Customers, links, transactions and commissions can be deleted. A deleted row sends no webhook and simply drops out of list results, so neither path above removes it from your copy. Once a day, collect every id Tolt still has and delete the local rows that are gone.

```ts TypeScript theme={"system"}
export async function pruneCommissions() {
  const startedAt = new Date().toISOString();
  const ids = new Set<string>();

  for await (const commission of await tolt.commissions.list({ limit: 100 }))
    ids.add(commission.id);

  // Only rows created before the sweep, so a row added
  // mid-sweep is never removed.
  await db.deleteMissing("commission", ids, { createdBefore: startedAt });
}
```

```python Python theme={"system"}
def prune_commissions():
    started_at = datetime.now(timezone.utc)
    ids = set()

    ids.update(commission.id for commission in client.commissions.list(limit=100))

    # Only rows created before the sweep, so a row added
    # mid-sweep is never removed.
    db.delete_missing("commission", ids, created_before=started_at)
```

Run it for `customers`, `links` and `transactions` too. Partners have no delete endpoint; suspending or rejecting one arrives as `partner.updated`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.