Admin API pagination
The Driftstack admin API uses cursor-based pagination on list endpoints. A cursor is an opaque, server-issued token that resumes the scan immediately after the last row of the previous page. This page documents the contract.
Why cursors and not offsets
Offset pagination (?page=2) is convenient but breaks
when rows are inserted or removed between page requests:
callers can skip rows or see duplicates. The admin surface
lists rows ordered by created_at DESC with
order_id as the tiebreaker; new orders arrive at
the front of the list constantly. A cursor anchored to the
last seen (created_at, order_id) pair gives a
stable walk across the list even while new rows are being
added.
The contract
A list response looks like:
{
"orders": [ ...up to `limit` rows... ],
"next_cursor": "eyJ0cyI6MTcwMDAwMDAwMCwiaWQiOiJvcmRfYWJjIn0"
} orderscontains up tolimitrows from the page (defaultlimit=50; max200on this endpoint).next_cursoris a string when at least one more row exists beyond the page;nullwhen the page reaches the end of the list.
The walk loop
let cursor = null;
const all = [];
do {{
const params = new URLSearchParams({{ limit: '50' }});
if (cursor) params.set('cursor', cursor);
const res = await fetch(`/v1/admin/crypto-orders?${params}`, {{
headers: {{ authorization: `Bearer $ADMIN_KEY` }}
}});
const body = await res.json();
all.push(...body.orders);
cursor = body.next_cursor;
}} while (cursor !== null);
Stop when next_cursor is null. Do not
try to parse the cursor — its internal shape is not part of
the contract and may change between releases. Treat it as
opaque bytes.
Combining with filters
Pagination composes with the existing filter parameters
(status, search,
account_id). Send the same filter values on every
page request; the server applies them before walking past the
cursor anchor. Changing a filter mid-walk is undefined —
always start a fresh walk (drop the cursor) when filters
change.
Cursor lifetime
Cursors are not signed and do not expire on a timer; the
server treats them as the literal
(created_at, order_id) pair to seek past. A
cursor remains useful as long as the row it anchors to is
still in the scan window (default 1000 rows on filtered
queries, 51 rows on the unfiltered first page — the
limit + 1 overflow probe).
If you walk slowly enough that the anchor row falls out of
the window, the server returns an empty page with
next_cursor: null. The caller should treat that
as "end of walk" and restart from the front if a full scan is
still needed.
Validation errors
- A cursor longer than 512 characters returns
400 Bad Request. - A malformed cursor (not valid base64url JSON of
{ts, id}) returns an empty page withnext_cursor: null; the server prefers a benign empty result over surfacing decode internals.
Other endpoints
The cursor convention will roll out to other admin list
endpoints over time. Each endpoint's documentation will note
whether it returns next_cursor; assume an
endpoint does NOT paginate until its documentation lists the
field.