Drıftstack docs
Docs

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 — true when at least one more page follows. This is returned for sessions, profiles, recipes, webhooks, agent-sessions, and profile-snapshots.
  • next_cursor — opaque token. Pass on the next request to fetch the following page. null when the page is the last. Some endpoints signal the end only via next_cursor: null — treat either has_more: false or 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_at desc) 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: if a record is deleted between page-1 and page-2 reads, page-2 still resolves correctly (the cursor points at a position, not a specific id). The deleted record is simply absent.

Limit bounds

Per-endpoint limit ranges:

  • Default: 50 on most list endpoints (webhook deliveries and the public incident feed GET /v1/status/incidents, for example). The Help conversations list (GET /v1/support/conversations) defaults to 20. When the page size matters, pass limit rather than relying on a default.
  • Maximum: 100.
  • Out-of-range values surface as 400 problem+json. The problem type varies by endpoint — some return .../validation-failed, others .../bad-request, so branch on the 400 status rather than on one type.
  • The per-endpoint bound is in the issues extension, not in detail. detail is a fixed human sentence (“One or more fields failed validation.”); issues carries the field-level detail, including the maximum a rejected limit exceeded. Read issues if 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 cursor with a from / to time 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_cursor for null and break — the drive-to-completion examples above do this; copy them rather than rolling your own.