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. Treat null and absent identically, and prefer hasMore as 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.

← All documentation