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, webhook deliveries, 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: 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: 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 on most list endpoints; 50 on the Help conversations list (GET /v1/support/conversations).
  • 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.