Skip to main content
Every list endpoint returns the same envelope:
  • Pass cursor=<nextCursor> to get the next page.
  • limit is 1–500, default 100.
  • Ordering is ascending on the sort key, so a feed reads like a log: older first, newest last.
  • nextCursor is returned even when hasMore is false. Store it, and the next call returns only rows added or updated since — that is the whole incremental-sync mechanism. Cursors never expire.

Cursor kinds

A cursor is only valid for the endpoint that issued it; sending one elsewhere returns 400 INVALID_CURSOR. Opaque cursors are base64url-encoded JSON — you may inspect them, but do not build them by hand.

Rows are never skipped or repeated

Keyset pagination on a stable sort key means a row inserted while you are paging cannot cause the page boundary to shift under you. What can happen is a row being returned again because it changed:
  • A call whose summary is regenerated gets a new updatedAt and moves forward in /calls.
  • A Decision Room that sees new activity moves forward in /decision-rooms.
Upsert on id, never insert blindly, and you are safe on both counts.

The three-step recipe

1

Backfill

Call each feed from an empty cursor with limit=500 until hasMore is false. Store the final nextCursor per feed. For Decision Rooms, follow each room with GET /decision-rooms/{id}/metrics if you want the roll-ups.
2

Incremental

On a schedule (every few minutes is plenty), call each feed with its stored cursor. Whatever comes back is new or changed. Store the new nextCursor.
3

Webhooks for latency

Register an endpoint so events arrive in seconds instead of at the next tick. Apply the payload, dedupe on the envelope id, and — optionally — trigger an early incremental pass. A dropped webhook then costs latency, never data.

Filters

Filters narrow a feed without changing how it pages:
For incremental sync, prefer the cursor over since / updated_since. A timestamp filter can miss two rows with the same timestamp on either side of a page boundary; a cursor cannot.