Skip to main content
Driftstack DRIFTSTACK

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"
}

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

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.

Related