Pagination
Every Driftstack GET /v1/... list endpoint uses
cursor-based pagination. This page covers the request shape,
response envelope, and the iteration pattern you should use
from clients.
Why cursors, not offsets?
Offset pagination (page=3&size=50) gets unreliable
when the underlying list mutates between requests — newly-inserted
rows shift indexes and pages can repeat or skip items. Cursors
reference a specific row in a stable sort order, so iteration
stays consistent even under concurrent writes.
Request
List endpoints accept two query parameters:
limit— page size, integer 1-100. Defaults to 50. Hard cap at 100; values above are rejected with 400.cursor— opaque string returned by the previous page. Omit on the first request.
GET /v1/profiles?limit=20&cursor=eyJpZCI6InByb2ZfYWJjIiwidHMiOjE3MTU0NzAwMDB9
Authorization: Bearer ds_live_… Response envelope
{
"data": [
{ "id": "prof_aaa", "name": "us-mobile-bot", ... },
{ "id": "prof_bbb", "name": "eu-desktop-bot", ... }
],
"has_more": true,
"next_cursor": "eyJpZCI6InByb2ZfYmJiIiwidHMiOjE3MTU0Njk5MDB9"
} data— array of resources, at mostlimitentries.has_more— boolean.truewhen there are more pages,falseon the final page.next_cursor— string whenhas_moreis true;nullon the final page. Pass it ascursoron the next request.
Iteration pattern
The canonical client loop:
async function* iterateAll(client) {
let cursor = undefined;
while (true) {
const url = new URL('/v1/profiles', client.baseUrl);
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, { headers: client.headers });
const page = await res.json();
for (const item of page.data) yield item;
if (!page.has_more) break;
cursor = page.next_cursor;
}
} Sort order
All list endpoints sort by created_at DESC with
id DESC as the tiebreaker. New resources appear on
the first page; older resources page off the back. If you need
a different order (e.g. oldest-first or by name), fetch the full
list and sort client-side — server-side sort overrides aren't
currently supported.
Cursor stability
Cursors are opaque — they're base64-encoded
tuples of (created_at, id) from the row at the page
boundary. You should treat them as black boxes:
- Don't parse or modify cursor strings — the encoding is not a stable API surface and may change.
- Cursors are valid indefinitely. You can persist a cursor and resume iteration days later.
- If a cursor's referenced row is deleted, the next page starts from the closest row in sort order; no error is raised.
Filter parameters
Some endpoints accept additional filters in the query string
(e.g. GET /v1/account/audit-log?action=api_key.minted).
Filters compose with pagination — the cursor encodes the
filter context, so paging through filtered results is
consistent. Switching filters mid-iteration requires a new
cursor (omit cursor on the first filtered request).
Total counts
List responses do not include a total-count
field. Computing total counts on large tables is expensive
and most clients don't need it. If you need a count for a
specific resource, the resource's dedicated endpoint exposes
it (e.g. GET /v1/account/me carries
profile_count for the current account).
Rate limits during iteration
List requests count against the global bucket —
every authenticated request increments it. A full iteration
through 10,000 profiles at limit=100 is 100
requests, well within the bucket for any non-trial tier. If
you hit a 429 (type URI
https://errors.driftstack.dev/rate-limited), back
off and retry; the Retry-After header and the
retry_after_seconds extension both tell you for
how long. See /docs/rate-limits
for details.
Support
Pagination questions or surprising cursor behaviour: [email protected].