# Sync data incrementally

Poll what changed since your last run instead of refetching a window and diffing, and avoid reacting to your own writes.

Source: https://sourceloop.ai/help/api/incremental-sync/

---

If you are building anything that runs on a schedule, a warehouse sync, an alert, a Slack digest, `GET /v1/changes` is the endpoint to build it on. It answers "what changed since my last run" directly, so you do not refetch a rolling window and diff it client-side.

The difference is not just tidiness. A window-and-diff job re-reads the same rows every run, gets slower as the workspace grows, and silently misses anything that changed outside the window it happened to pick.

## The loop

Store the cursor from each run. Pass it back on the next one.

```ts
const state = await loadState();          // { cursor?: string }

const url = new URL("https://app.sourceloop.ai/api/v1/changes");
url.searchParams.set("website", "acme.com");
url.searchParams.set("limit", "500");
if (state.cursor) url.searchParams.set("cursor", state.cursor);
else url.searchParams.set("since", "2026-08-01T00:00:00Z");   // first run only

const res = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.SOURCELOOP_API_KEY}` },
});
const page = await res.json();

for (const change of page.changes) {
  await apply(change);
}

if (page.next_cursor) await saveState({ cursor: page.next_cursor });
```

On the very first run there is no cursor, so pass `since` instead. With neither, you get the last 24 hours.

Keep paging while `meta.has_more` is `true`, and save the cursor only after the page has been applied successfully. Saving it first means a crash mid-page loses those changes permanently.

Changes are ordered newest first and retained for 90 days. A job that has been down for longer than that should do a full read rather than trying to resume.

## Filter to what you actually handle

```
?types=conversion.created,deal.stage_changed
```

Available types include `conversion.created`, `conversion.value_changed`, `stage.changed` and `deal.stage_changed`. Filtering server-side is cheaper than fetching everything and discarding most of it, and it keeps your cursor moving at a sensible rate.

## Do not react to your own writes

Every change carries a `source`. When your own job writes back through the API, the resulting change appears in the feed like any other, and a job that reacts to it will trigger itself forever.

```ts
for (const change of page.changes) {
  if (change.source?.startsWith("api:")) continue;   // our own write, skip it
  await apply(change);
}
```

Writes made with an API key are stamped `api:<keyId>`, so you can skip your own specifically rather than skipping every programmatic change. This one line is the difference between a sync that settles and one that loops.

## Scheduling

Poll on a schedule that matches how fresh the data needs to be. Every five minutes is plenty for a Slack alert; hourly is plenty for a warehouse. There is no webhook to wait on, which means no endpoint of yours to keep publicly reachable, and no signature to verify.

The endpoint needs `conversions:read`. Rate limits are per workspace, so if several jobs poll in parallel, stagger them rather than having them all fire on the hour. See [Errors and rate limits »](/help/api/errors/).

## When to use something else

`GET /v1/changes` tells you what moved. It is not the right way to build a report: for aggregate numbers, ask [`/v1/metrics`](/help/api/metrics/) for the window you want, in one call, rather than reconstructing totals from a change feed.

A good rule: changes drive **reactions**, metrics drive **reports**.