Skip to content

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.

ParameterDefaultMeaning
limit20How many items to return. 1 to 100.
starting_afternoneThe 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.

ListFilters
GET /v1/casesstatus, patient, created_after
GET /v1/patientsexternal_id
GET /v1/eventssince, type, include_resource
GET /v1/webhook_endpoints/{id}/deliveriesstatus

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.