Pagination
Every list endpoint pages the same way: with an opaque cursor, never an offset. An offset page shifts underneath you while rows are being written — the row at position 50 on one request is at 51 on the next — so a sync that pages by offset both repeats rows and misses them. A cursor names a place in the list, not a position, so neither happens.
Query parameters
| Parameter | Default | Meaning |
|---|---|---|
limit | 50 | Rows per page, 1–100. |
cursor | — | The next_cursor from the previous page. Omit it to start. |
The bounds are API.LIST in src/lib/const/api.const.ts.
An unknown query parameter is a 400, not ignored. A misspelt filter that was silently dropped would answer with every row, which is the worst possible way to discover the typo:
json
{
"error": {
"code": "INVALID_INPUT",
"message": "Unknown query parameter `updated_after`.",
"path": ["updated_after"]
}
}The response
json
{
"data": [],
"pagination": {
"limit": 50,
"has_more": true,
"next_cursor": "WyIyMDI2LTEwLTA5VDEyOjAwOjAwLjEyMzQ1NloiLCIwMTk5YTFiMi1jM2Q0LTdlNWYtOGE5Yi0wYzFkMmUzZjRhNWIiXQ"
}
}| Field | Meaning |
|---|---|
limit | The page size this page was fetched with. |
has_more | Whether another page follows. Loop on this. |
next_cursor | Pass it as cursor for the next page. null exactly when has_more is false. |
There is no total. Counting every matching row on every page costs more than the page itself on a large list, and a count is out of date by the time you read it anyway.
A cursor is opaque: do not decode it, build one, or store it long-term. It is valid only with the same filters it was issued under — send the same query string with a new cursor, nothing else changed. A cursor this API did not issue is a 400 on cursor.
A full read
bash
cursor=""
while :; do
page=$(curl -sS "https://<your-app-domain>/api/public/v1/<list>?limit=100${cursor:+&cursor=$cursor}" \
-H "authorization: Bearer $CALL_CENTER_API_KEY")
echo "$page" | jq -c '.data[]'
[ "$(echo "$page" | jq -r '.pagination.has_more')" = "true" ] || break
cursor=$(echo "$page" | jq -r '.pagination.next_cursor')
doneSyncing changes: updated_since
A list that supports it — its page says so — takes updated_since, an ISO 8601 timestamp with an offset (2026-10-09T12:00:00Z, 2026-10-09T14:00:00+02:00). A timestamp with no offset is a 400, because it would be read in nobody's time zone. Such a list returns rows changed at or after that instant, oldest change first.
To keep a copy in step:
- Read every page with
updated_sinceset to your last checkpoint. - Upsert each row by
id. - Take the largest
updated_atyou saw as the next checkpoint, minus a minute.
The minute of overlap is deliberate. A change is stamped when it is made, but becomes visible when its transaction commits, so a change made just before your read can appear just after it with an earlier timestamp than rows you already have. Overlapping the window, and upserting by id, means a late commit is read next time instead of never.
Paging oldest change first also means a row updated while you are paging moves to the end of the list, where you will meet it again — it is never skipped.