Pagination and change feeds
List endpoints return records a page at a time with an opaque cursor. With updatedSince, the same mechanism becomes a change feed you can resume. This guide explains both.
Pages and cursors
Every list endpoint returns one page at a time:
Response
{
"data": [
{ "id": "…", "updatedAt": "2026-10-02T07:15:00.000Z", "…": "…" }
],
"nextCursor": "eyJ…",
"hasMore": true
}| Parameter | Meaning |
|---|---|
limit | Records per page. Default 50, maximum 200. |
cursor | The previous page’s nextCursor. Leave it out for the first page. |
updatedSince | An ISO 8601 UTC time. Returns records whose updatedAt is at or after it, oldest first. |
externalCode | Filter by an external code, written system:code, for example dms:UG2000123. |
includeDeleted | true adds deleted records as tombstones where the resource keeps them. |
- The cursor is opaque. Pass it back unchanged; do not build or parse it. Stop when
hasMoreisfalse;nextCursoris thennull. - Without
updatedSince, records come newest first by creation time. With it, they come inupdatedAtorder, oldest first. - Each resource’s own filters are listed in the API reference.
Change feeds with updatedSince
updatedSince and the cursor together make a change feed you can resume. Keep a checkpoint: the newest updatedAt you have processed. Each run reads from the checkpoint and pages to the end.
- Feeds order records by
updatedAt. A write can become visible a few seconds after itsupdatedAt, so a record changed during your read can appear after you have paged past its time. - To never miss a change, start each poll from your last checkpoint minus an overlap of 5 minutes, and skip records whose
idandupdatedAtyou have already processed. Records can therefore repeat. - For real-time changes, subscribe to webhooks.
- Within one run, the cursor only moves forward. A record changed while you page comes again later with its new
updatedAt; records that keep changing do not hold the others back. - At the end of a feed,
hasMoreisfalseandnextCursorisnull; there is no cursor to keep. Keep your checkpoint instead. - The same applies to
sinceon/stock-movementsand/trips/{id}/locations. On/returns,externalCodecannot be combined withupdatedSince: a code matches at most a few records, so read them without it. - A change-feed cursor issued before 2 October 2026 is refused with
400 invalid_field. Restart from the first page with the sameupdatedSince. - Every record carries
updatedAt. When the same record reaches you twice, keep the copy with the laterupdatedAt.
// Read every record changed since the last checkpoint, page by page.
// Store the checkpoint durably and pass it again next run.
const OVERLAP_MS = 5 * 60 * 1000; // a write can become visible after its updatedAt
export async function readChanges(path, checkpoint) {
const from = new Date(Date.parse(checkpoint) - OVERLAP_MS).toISOString();
let cursor = null;
let newest = checkpoint;
do {
const query = new URLSearchParams({ limit: "200", updatedSince: from });
if (cursor) query.set("cursor", cursor);
const response = await fetch(`${process.env.BASE}${path}?${query}`, {
headers: { Authorization: `Bearer ${process.env.KEY}` },
});
if (!response.ok) throw new Error(`${response.status} ${response.headers.get("x-request-id")}`);
const page = await response.json();
for (const record of page.data) {
// The overlap repeats records: skip an id + updatedAt you already processed.
if (await alreadyProcessed(record.id, record.updatedAt)) continue;
await upsertLocally(record);
if (record.updatedAt > newest) newest = record.updatedAt;
}
cursor = page.hasMore ? page.nextCursor : null;
} while (cursor);
return newest; // the next run's checkpoint
}Deletions
- Where a resource keeps deleted records (customers do),
includeDeleted=truereturns them as tombstones:{ "id": "…", "deleted": true, "deletedAt": "…" }. - Where a resource removes records outright, the record’s
deletedwebhook event is the only signal. Subscribe to it if you mirror that resource.