Skip to content

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.

Every list response follows the same shape:

{
"object": "list",
"data": [ /* array of resource objects */ ],
"has_more": true,
"next_cursor": "cur_01J8X4abc123"
}
FieldTypeDescription
objectstringAlways "list"
dataarrayThe page of resource objects
has_morebooleantrue if more records exist after this page
next_cursorstring | nullOpaque cursor to pass as ?cursor= in the next request. null when has_more is false
totalinteger | absentTotal record count, only present on endpoints where it is cheap to compute
ParameterDefaultMaxDescription
limit25100Maximum number of records to return
cursorOpaque cursor from a previous response’s next_cursor

To retrieve all records, loop until has_more is false:

Terminal window
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
fi
done
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;
}

Request

GET /api/v1/customers?limit=2 HTTP/1.1
Authorization: Bearer bk_live_acme_xxxxxxxxxxxx
BenchKey-Version: 2026-06-13

Response

{
"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.1
Authorization: Bearer bk_live_acme_xxxxxxxxxxxx
BenchKey-Version: 2026-06-13
  • 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_cursor with param: "cursor".