Skip to main content
Driftstack DRIFTSTACK

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:

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"
}

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:

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].