Incremental Sync

Documents change after they are issued — a receipt settles an invoice, a credit note reverses one, an allocation number arrives late. None of that is a request you made, so none of it comes back on a response. The change feed on GET /documents is how you pull those changes without re-reading everything.

Opening a Sweep

Pass updatedSince (any ISO 8601 timestamp) to start one. The listing switches from newest-issued-first to oldest-change-first — the only order a sweep can resume in without gaps — and the response gains nextCursor and hasMore.

GET /documents?updatedSince=2026-08-01T00:00:00Z&limit=200
{
  "data": [
    "..."
  ],
  "total": 500,
  "pagination": {
    "total": 500,
    "limit": 200,
    "offset": 0,
    "hasMore": true,
    "page": 1,
    "pageSize": 200,
    "totalPages": 3
  },
  "nextCursor": "MTc3NDQyODAwMDAwMDo4NDIxMw",
  "hasMore": true
}

Continuing with a Cursor

Hand nextCursor back as cursor to fetch the next batch. The cursor is built from the last row you actually received, so paging stays stable no matter what changes underneath — unlike an offset, which describes a position in a set that is moving.

GET /documents?cursor=MTc3NDQyODAwMDAwMDo4NDIxMw&limit=200

Treat the cursor as opaque: it is a token to hand back, not a value to parse or construct. When both are sent, cursor wins over updatedSince. A short page means the feed is drained — store the cursor and poll again later instead of replaying what you just read.

Errors

A parameter that cannot be understood is rejected, never ignored — falling back to an ordinary listing would hand you a page of unrelated documents and let you believe you had synced.

CodeCause
INVALID_CURSORThe cursor was not one we issued. Pass nextCursor back verbatim.
INVALID_UPDATED_SINCENot a valid ISO 8601 date.

A Complete Sync Loop

// cursor is persisted between runs; on the very first run there is none.
let cursor = await store.get("glance:documents:cursor");
const params = cursor
  ? { cursor }
  : { updatedSince: "2026-01-01T00:00:00Z" };

while (true) {
  const res = await glance.documents.list({ ...params, limit: 200 });

  for (const doc of res.data) upsert(doc); // keyed by visibleId

  if (res.nextCursor) {
    cursor = res.nextCursor;
    await store.set("glance:documents:cursor", cursor);
    params.cursor = cursor;
    delete params.updatedSince;
  }

  if (!res.hasMore) break; // drained — resume from the stored cursor later
}
Upsert, do not append. The feed reports documents whose state changed, so the same document appears again every time it moves. Key your store on visibleId and overwrite.
Prefer push where you can. A sweep is the right tool for backfills and for catching up after downtime. For ongoing changes, webhooks deliver the same events without polling — the two are complementary: webhooks for the live path, the cursor as the safety net.

Last updated