Pagination
List endpoints return results a page at a time:
{ "data": [ { "id": "18342", "…": "…" } ], "has_more": true, "next_cursor": "eyJ1IjoiMjAyNi0xMS0wNFQxNToyNDoxMVoiLCJpIjoiMTgzNDIifQ"}| Parameter | Description |
|---|---|
limit |
Results per page, 1–100. Default 25. |
cursor |
The next_cursor from the previous page. Omit it for the first page. |
Keep requesting with the latest next_cursor until has_more is false
(next_cursor is then null).
Cursors are opaque. Don’t decode, build, or store them long-term. A cursor is tied to the filters it was issued with, so keep the other query parameters the same while paging.
Ordering
Section titled “Ordering”GET /patients, GET /cases, and GET /practices/{practice_id}/providers are
ordered by updated_at, oldest first, then by id. Records that change while you page move to the end, so a full pass never
skips a record, though you may see one twice. De-duplicate by id.
Message threads (GET /cases/{case_id}/messages) are ordered by created_at,
oldest first.
Syncing changes
Section titled “Syncing changes”To keep a local copy of a practice in sync, combine updated_since with pagination, using that practice’s key in headers:
let since = loadCheckpoint() // e.g. "2026-11-01T00:00:00Z"let cursorlet latest = since
do { const url = new URL(`${THEA_API}/cases`) url.searchParams.set('updated_since', since) url.searchParams.set('limit', '100') if (cursor) url.searchParams.set('cursor', cursor)
const page = await fetch(url, { headers }).then((r) => r.json()) for (const c of page.data) { upsertLocalCase(c) if (c.updated_at > latest) latest = c.updated_at } cursor = page.next_cursor} while (cursor)
saveCheckpoint(latest)updated_since is inclusive, so the next run sees the last record again.
That’s expected and harmless if you upsert by id.
Webhooks tell you about changes as they happen. Many integrations use both: webhooks for speed, and a periodic sync to catch anything missed. See Polling vs. webhooks.

