Pagination
One cursor envelope for every list, why there is no total, and how to page a backfill.
Every list in the API answers the same envelope.
{
"data": [ … ],
"has_more": true
}
Two query parameters drive it.
| Parameter | Default | Meaning |
|---|---|---|
limit | 20 | How many items to return. 1 to 100. |
starting_after | none | The id of the last item you saw. |
Paging forward
Read a page, take the id of the last item in data, and pass it as starting_after on the next
call. Stop when has_more is false.
curl "https://api.cuvo.co/v1/cases?limit=100" \
-H "Authorization: Bearer cuvo_sk_test_…"
curl "https://api.cuvo.co/v1/cases?limit=100&starting_after=case_7d1e4b2a" \
-H "Authorization: Bearer cuvo_sk_test_…"
let cursor;
do {
const url = new URL("https://api.cuvo.co/v1/cases");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("starting_after", cursor);
const page = await fetch(url, { headers: { Authorization: `Bearer ${key}` } }).then((r) => r.json());
for (const item of page.data) await handle(item);
cursor = page.data.at(-1)?.id;
var more = page.has_more;
} while (more);
Both SDKs expose this as an async iterator, so you write a for await loop and never touch the
cursor yourself. See SDKs.
Why there is no total
Counting a tenant-scoped table on every page is exactly the cost a cursor exists to avoid, and
has_more is what a client actually needs to decide whether to ask again. If you need a count of
something, count what you have written on your own side, or aggregate it from
events.
Ordering and stability
Lists are ordered by id. A cursor is an id, not an offset, so rows created while you are paging never shift the page under you and never cause a duplicate or a skip. An id you did not get from this API is not a valid cursor.
Filters
Filters compose with the cursor; keep them identical across every call in one pass, since changing one mid-run makes the cursor mean something else.
| List | Filters |
|---|---|
GET /v1/cases | status, patient, created_after |
GET /v1/patients | external_id |
GET /v1/events | since, type, include_resource |
GET /v1/webhook_endpoints/{id}/deliveries | status |
Backfilling
For a first import, page GET /v1/cases with limit=100 and no filter, then keep up with
GET /v1/events?since=… carrying the created_at of the last event you processed. That pair is the
whole synchronization story: one bounded pass to get level, then an incremental feed to stay level.
Do not run pages in parallel. A cursor is sequential by construction, and parallel requests only spend your rate limit faster than one loop does.