Pagination
Cursor-based pagination — params, response shape, and patterns.
Last updated May 12, 2026
Why cursors, not pages
The API uses cursor-based pagination throughout. Cursors don't shift when underlying data changes, so iterating a list while items are added or removed never re-shows or skips items.
We never used offset / limit pagination, so there's no migration story — every endpoint that returns a list uses cursors from day one.
Request shape
GET /v1/audits?cursor=<opaque>&limit=50
cursor— opaque string from a previous response. Omit on the first request.limit— page size, 1 to 100 inclusive. Default 50.
curl "https://api.aidomination.app/v1/audits?limit=20" \
-H "Authorization: Bearer $AD_TOKEN"
Response shape
{
"data": [
{ "id": "aud_01", "headlineScore": 78 },
{ "id": "aud_02", "headlineScore": 81 }
],
"pagination": {
"nextCursor": "Y3Vyc29yOmF1ZF8wMg==",
"hasMore": true
}
}
data— array of items.pagination.nextCursor— pass to the next request as thecursorparam.nullwhen there are no more pages.pagination.hasMore— convenience boolean.
Full iteration example
async function fetchAllAudits(token: string) {
const audits: unknown[] = [];
let cursor: string | null = null;
while (true) {
const url = new URL("https://api.aidomination.app/v1/audits");
if (cursor) url.searchParams.set("cursor", cursor);
url.searchParams.set("limit", "100");
const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
audits.push(...json.data);
if (!json.pagination.nextCursor) break;
cursor = json.pagination.nextCursor;
}
return audits;
}
Sort order
Lists return in created-descending order by default. The cursor encodes both an id and a created-at timestamp, so this ordering is stable across pages.
Some endpoints (notably /audits and /mentions) support ?sort= overrides — see the endpoint reference.
Filtering with pagination
Filters apply BEFORE pagination. The cursor encodes the filter set, so paginating a filtered list returns only filtered items.
GET /v1/content?status=approved&limit=50
If you paginate, then change the filter mid-iteration, the new cursor is incompatible — the API returns 400 cursor_mismatch. Restart the iteration.
Cursor opacity
Cursors are opaque, base64url-encoded structures. We reserve the right to change the internal format. Don't parse them or construct them yourself; treat them as opaque tokens.
Cursor lifetime
Cursors are valid for 24 hours from issue. After 24 hours, they return cursor_expired and you restart.
Reverse iteration
We support backward pagination via ?direction=before&cursor=<>. Useful for "load older" patterns in a UI. The default direction is forward.
Counts
We don't return total counts in list responses. Computing exact counts requires a full table scan on the larger lists, which is expensive enough that we suppress it by default.
For approximate counts (e.g. "about 1,200 audits"), use GET /v1/audits/count?approximate=true. The endpoint returns a count accurate to ±10%.
For exact counts on small lists (companies, API keys), the response includes a pagination.total field. The threshold is 1,000 items.
Common pitfalls
- Re-using a cursor. Cursors are single-use within a page; calling
GETwith the same cursor twice returns the same page twice. That's fine — but don't assume the cursor mutates server state. - Pagination with sort and filter changing. Always recompute the cursor when sort or filter changes.
- Limit too low. Limit < 10 produces excessive round-trips. Limit > 100 is rejected.
Was this article helpful?