Skip to main content

Pagination

Every list endpoint covered by this skill uses opaque cursor pagination — with one exception: GET /v1/canvases/search is offset-paginated (see Search is the offset lane below). Newer surfaces outside this catalog (the drive folder/file lists, the website list) are offset lanes too, with their own {<name>, limit, offset, has_more} shapes. Read the endpoint’s own reference before assuming the cursor contract.

Response shape

  • data — the page of items.
  • next_cursor — opaque signed string. Pass back as ?cursor=<value> to get the next page. null means you’ve reached the end.
  • returned / has_more — derived server-side (len(data) and next_cursor != null), so a page can never silently look complete. Never infer the end from len(data) < limit.
  • limit — the effective page size the response was computed with.
  • total — see below: a real count on some lanes, null on others.

Iteration pattern

TypeScript:
Python:

Limits

Always request as large a page as you can tolerate — fewer roundtrips, less rate pressure.

Sort order

List endpoints sort by (created_at DESC, id DESC) on immutable columns. This guarantees pagination correctness when rows are added or modified mid-scan — no skips, no duplicates — at the cost of “sort by updated_at” being unavailable. Client-side sort if you need a different order.

total is per-lane

total is present in the envelope but only populated where the collection supports a cheap true count — GET /v1/tasks, GET /v1/brand-kits, and GET /v1/organizations return a real count after filters. It stays null on the lanes where counting is as expensive as the query itself: GET /v1/canvases (the doc-kind filter reads un-indexed JSONB), GET /v1/events, GET /v1/webhook_deliveries, and the relevance-ranked search. So: use total when you have it, and never require it. has_more is the authoritative “is there more” signal on every lane. For a progress bar on a null-total lane, the common pattern is “processed 127 so far…” without a denominator.

Opaque cursors

Cursors are HMAC-signed + base64url-encoded. Don’t:
  • Parse or mutate the cursor value.
  • Attempt to construct one by hand.
  • Reuse a cursor from a different list endpoint.
The server detects tampering and returns 400 invalid_request. Cursors are valid across sessions but have a bounded TTL — don’t store one for a week and expect it to resume correctly. For long-running iterations (> a few hours), consider re-starting from the beginning.

Endpoints that paginate

All of these return {data, next_cursor}:
  • GET /v1/canvases
  • GET /v1/tasks
  • GET /v1/brand-kits
  • GET /v1/organizations
  • GET /v1/events
  • GET /v1/webhook_deliveries
Single-resource endpoints (GET /v1/canvases/{id}, GET /v1/tasks/{id}, GET /v1/credits, etc.) don’t paginate.

Search is the offset lane

GET /v1/canvases/search is relevance-ranked, so a cursor over (created_at, id) would be meaningless. It pages by offset and uses its own response shape:
Items are under canvases, not data. There is no next_cursor — page with offset + limit while has_more is true. limit defaults to 20 and caps at 100, same as the /canvases cursor lane (see Limits — the /events and /webhook_deliveries lanes are 50/200).

Concurrency

Don’t parallelize pagination. Each page depends on the previous one’s cursor. Parallelize the work done per item inside a page instead.

Migration from offset pagination

If you’re carrying code from the legacy 2026-04-12 shape:

Common wrong guesses

  • Assuming total is always there — or never there. It is a real count on /tasks, /brand-kits, and /organizations, and null on /canvases, /events, /webhook_deliveries, and search. Branch on it; rely on has_more.
  • Parsing / mutating cursors. Opaque; server rejects tampered values.
  • Passing offset= to a cursor endpoint. Ignored (or rejected, depending on endpoint) on 2026-05-01. Use the cursor — except on /canvases/search, where offset is the mechanism.
  • Holding a cursor for days. Cursors expire; restart the iteration.
  • Parallel page fetches. Each page depends on the prior cursor.
  • Reading response.canvases / response.tasks / response.brand_kits. The cursor lanes always put items under data. GET /canvases/search is the exception — its items are under canvases.
  • Expecting a next_cursor from /canvases/search. It has none. Page with offset while has_more is true.

Upstream