Pagination
Every list endpoint that can grow beyond a single page uses
cursor-based pagination. Cursors are opaque strings the
server mints; clients pass them back to fetch the next page. This
contract is shared across /v1/sessions, /v1/profiles,
/v1/agent-sessions, /v1/recipes, /v1/webhooks/{id}/deliveries
and others — this page is the canonical reference.
Offset / page-number pagination is not supported. Cursor pagination is stable under concurrent inserts (page 2 doesn’t shift just because page 1 grew); offset pagination isn’t, and we don’t want to expose you to that problem.
Request shape
| Query parameter | Required | Notes |
|---|---|---|
limit |
optional | Per-page size. Defaults + max vary per endpoint (see Limit bounds below). |
cursor |
optional | Pagination token from a prior page’s next_cursor. |
The first request omits cursor; subsequent requests pass the
prior response’s next_cursor back.
Response shape
{
"data": [
{ /* resource */ },
/* … */
],
"has_more": false,
"next_cursor": "<opaque-cursor>" | null
}
data— the page’s resources, ordered newest-first by default.has_more—truewhen at least one more page follows. This is returned for sessions, profiles, recipes, webhook deliveries, agent-sessions, and profile-snapshots.next_cursor— opaque token. Pass on the next request to fetch the following page.nullwhen the page is the last. Some endpoints signal the end only vianext_cursor: null— treat eitherhas_more: falseor a null cursor as “stop”.
The cursor is opaque — do not try to parse it. Its internal shape can change between API versions without notice; only the “pass it back exactly as received” contract is stable.
Canonical drive-to-completion loop
TypeScript
import type { Driftstack } from '@driftstack/sdk';
async function listAllProfiles(client: Driftstack): Promise<unknown[]> {
const all: unknown[] = [];
let cursor: string | undefined;
for (;;) {
const page = await client.profiles.list({ limit: 100, cursor });
all.push(...page.data);
if (!page.next_cursor) break;
cursor = page.next_cursor;
}
return all;
}
Python
from driftstack import Driftstack
def list_all_profiles(client: Driftstack) -> list:
out, cursor = [], None
while True:
page = client.profiles.list(limit=100, cursor=cursor)
out.extend(page["data"])
if not page["next_cursor"]:
break
cursor = page["next_cursor"]
return out
Go
func ListAllProfiles(ctx context.Context, c *driftstack.Client) ([]driftstack.Profile, error) {
var out []driftstack.Profile
var cursor string
for {
page, err := c.Profiles.List(ctx, &driftstack.ListProfilesQuery{
Limit: 100,
Cursor: cursor,
})
if err != nil {
return nil, err
}
out = append(out, page.Data...)
// NextCursor is *string — nil means no more pages.
if page.NextCursor == nil || *page.NextCursor == "" {
break
}
cursor = *page.NextCursor
}
return out, nil
}
Ordering + stability
-
Default order is newest-first (
created_atdesc) on every paginated endpoint. If an endpoint ever adopts a different natural ordering, it documents that variation inline; cursor semantics are unchanged either way. -
Stability under writes: because cursors encode position relative to a specific record (not an offset), concurrent inserts during a paginated read never shift the page boundary. A record inserted between page-1 and page-2 simply ends up on page 1 (the newer side) on a future read; the current walk doesn’t re-emit it.
-
Stability under deletes: on most lists the cursor is the id of the last row on the page. If that row is deleted for good before you ask for the next page, the server cannot find it and returns the first page again, with no error. Your loop then sees rows it already has. Skip ids you have already seen, or do not delete rows from a list while you walk it.
Limit bounds
Per-endpoint limit ranges:
- Default:
50on most list endpoints (webhook deliveries and the public incident feedGET /v1/status/incidents, for example). The Help conversations list (GET /v1/support/conversations) defaults to20. When the page size matters, passlimitrather than relying on a default. - Maximum:
100on most list endpoints;50on the Help conversations list (GET /v1/support/conversations). - Out-of-range values surface as
400problem+json. The problemtypevaries by endpoint — some return.../validation-failed, others.../bad-request, so branch on the400status rather than on one type. - The per-endpoint bound is in the
issuesextension, not indetail.detailis a fixed human sentence (“One or more fields failed validation.”);issuescarries the field-level detail, including the maximum a rejectedlimitexceeded. Readissuesif you want to show the bound.
Anti-patterns
- Don’t decode the cursor. It’s opaque; we change the encoding freely between minor versions. Clients that try to parse it break on the next bump.
- Don’t combine
cursorwith afrom/totime filter on the resumed call unless the filter is identical to the original page-1 call. Mixing them produces undefined results (the cursor encodes the original filter context; you’d be asking for a different walk). - Don’t loop without an exit condition. Every paginated read
loop must check
next_cursorfor null and break — the drive-to-completion examples above do this; copy them rather than rolling your own.