Skip to content

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.

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.

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 cursor
let 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.