Pagination
All list endpoints in the BenchKey API use keyset (cursor) pagination. This approach is stable under inserts and deletes, efficient on large datasets, and avoids the “page drift” problem of offset-based pagination.
Response envelope
Section titled “Response envelope”Every list response follows the same shape:
{ "object": "list", "data": [ /* array of resource objects */ ], "has_more": true, "next_cursor": "cur_01J8X4abc123"}| Field | Type | Description |
|---|---|---|
object | string | Always "list" |
data | array | The page of resource objects |
has_more | boolean | true if more records exist after this page |
next_cursor | string | null | Opaque cursor to pass as ?cursor= in the next request. null when has_more is false |
total | integer | absent | Total record count, only present on endpoints where it is cheap to compute |
Query parameters
Section titled “Query parameters”| Parameter | Default | Max | Description |
|---|---|---|---|
limit | 25 | 100 | Maximum number of records to return |
cursor | — | — | Opaque cursor from a previous response’s next_cursor |
Walking pages
Section titled “Walking pages”To retrieve all records, loop until has_more is false:
CURSOR=""
while true; do if [ -z "$CURSOR" ]; then URL="https://app.benchkey.com/api/v1/customers?limit=50" else URL="https://app.benchkey.com/api/v1/customers?limit=50&cursor=$CURSOR" fi
RESPONSE=$(curl -s "$URL" \ -H "Authorization: Bearer $BENCHKEY_API_KEY" \ -H "BenchKey-Version: 2026-06-13")
echo "$RESPONSE" | jq '.data[] | .name'
HAS_MORE=$(echo "$RESPONSE" | jq -r '.has_more') CURSOR=$(echo "$RESPONSE" | jq -r '.next_cursor // empty')
if [ "$HAS_MORE" != "true" ]; then break fidoneNode.js
Section titled “Node.js”async function fetchAllCustomers() { const customers = []; let cursor = null;
do { const url = new URL("https://app.benchkey.com/api/v1/customers"); url.searchParams.set("limit", "50"); if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.BENCHKEY_API_KEY}`, "BenchKey-Version": "2026-06-13", }, });
if (!res.ok) { const { error } = await res.json(); throw new Error(`[${error.code}] ${error.message}`); }
const page = await res.json(); customers.push(...page.data); cursor = page.next_cursor; } while (cursor);
return customers;}First-page example
Section titled “First-page example”Request
GET /api/v1/customers?limit=2 HTTP/1.1Authorization: Bearer bk_live_acme_xxxxxxxxxxxxBenchKey-Version: 2026-06-13Response
{ "object": "list", "data": [ { "object": "customer", "id": "jane@example.com", "display_name": "Jane Smith", "email": "jane@example.com", "created_at": "2025-03-10T14:22:00.000Z" }, { "object": "customer", "id": "bob@example.com", "display_name": "Bob Jones", "email": "bob@example.com", "created_at": "2025-03-09T09:15:00.000Z" } ], "has_more": true, "next_cursor": "eyJjcmVhdGVkX2F0IjoxNzQxNTE0OTAwMDAwLCJpZCI6ImJvYkBleGFtcGxlLmNvbSJ9"}Next page
GET /api/v1/customers?limit=2&cursor=eyJjcmVhdGVkX2F0IjoxNzQxNTE0OTAwMDAwLCJpZCI6ImJvYkBleGFtcGxlLmNvbSJ9 HTTP/1.1Authorization: Bearer bk_live_acme_xxxxxxxxxxxxBenchKey-Version: 2026-06-13Notes on cursors
Section titled “Notes on cursors”- Cursors are opaque — do not parse or construct them. Their internal format may change without notice.
- A cursor is valid only for the same endpoint and the same query parameters (filters, sort order). Passing a cursor from a different endpoint or a cursor whose internal fields don’t match the target list returns
400 invalid_cursor. - Cursors do not expire, but records returned may change if data is modified between pages.
- Passing a malformed or structurally invalid cursor returns
400 invalid_cursorwithparam: "cursor".