Pagination
The offset and cursor envelopes, and why the total count is sometimes absent rather than zero.
Every collection endpoint returns an envelope, never a bare JSON array:
{
"data": [ ... ],
"pagination": { ... }
}
The array is always under data, and the paging state is always under
pagination — never flat on the response body. That is what lets one client
helper walk any collection in the API without knowing which endpoint it is
talking to.
Offset pagination — the default
Most collections page by offset. Send limit and offset as query parameters:
curl 'https://api.foxguide.io/v1/tables?limit=20&offset=40' \
-H 'Authorization: Bearer fgd_live_...'
{
"data": [ ... 20 items ... ],
"pagination": { "total": 137, "limit": 20, "offset": 40, "hasMore": true }
}
Defaults are limit: 20, offset: 0. Endpoints may cap limit; a request above
an endpoint’s cap is clamped rather than refused. Stop on hasMore: false —
it is the one field present on every paginated response and it is the loop
condition to write.
Cursor pagination — high-volume collections
Endpoints over high-churn data (message lists, event and delivery logs) page by cursor instead. Offset paging over a collection being written to concurrently skips and repeats rows; a cursor does not.
{
"data": [ ... ],
"pagination": {
"hasMore": true,
"cursor": "6712f0b3c4a1e2d3f4a5b6c7",
"direction": "next",
"limit": 50
}
}
Pass the returned cursor back as the cursor query parameter to get the next
page. Two properties of cursor are worth writing your client against:
- It may be explicitly
null, not merely absent, when there is no next page. Treatnulland absent identically, and preferhasMoreas the loop condition either way. - It is opaque. Do not parse it, do not construct one, and do not persist one across a schema change — its only contract is that handing it back returns the next page.
total is optional, and its absence is information
total is present when the collection’s cardinality is genuinely known — the
endpoint ran a real count over the same filter as the page. It is omitted
when the count is not knowable: a passthrough to an upstream that reports no
count, or a limit-only surface with no count query behind it.
An absent total means unknown. It never means zero, and it is never
synthesised from the page length — a total equal to data.length sitting
beside hasMore: true would contradict itself on every truncated page. Write
clients that render “137 results” only when total is present, and fall back to
“showing 20” or a plain next/previous control when it is not.
total is orthogonal to which paging mechanism an endpoint uses: a cursor
endpoint may still know its cardinality and report it.
Walking a collection safely
let offset = 0;
const all = [];
for (;;) {
const res = await fetch(`${base}/v1/tables?limit=100&offset=${offset}`, { headers });
if (res.status === 429) { await backoff(res); continue; }
const page = await res.json();
all.push(...page.data);
if (!page.pagination.hasMore) break;
offset += page.pagination.limit;
}
Two things this loop gets right and a naive one does not: it terminates on
hasMore rather than on an expected total, and it treats
429 as a retry rather than as the end of the collection.