Skip to content

Pagination

Large collections are returned one page at a time, using cursor pagination — not offset/limit. Each page carries its rows plus a cursor pointing at the next page, so pagination stays stable even as new rows are created while you iterate.

A cursor-paginated list responds with:

{
"data": [ /* ...rows for this page... */ ],
"has_more": true,
"next_cursor": "c3Vic2NyaWJlZA"
}
FieldMeaning
dataThe rows for this page.
has_moretrue if more pages remain after this one.
next_cursorPass this back as ?cursor= to fetch the next page. null on the last page.

Control page size with ?limit= — an integer from 1 to 100, defaulting to 25. A value outside that range (or a non-integer) is rejected with 400 invalid_limit rather than being silently clamped.

A cursor is an opaque token. Never parse, construct, or decode one — its internal format is not part of the contract and can change without notice. Pass back exactly the next_cursor you were given. A stale or malformed cursor is ignored (treated as “start from the top”), not an error.

Loop until has_more is false, threading each next_cursor into the following request:

async function listAll(path, apiKey) {
const rows = [];
let cursor = null;
do {
const url = new URL(`https://app.personablocks.io/api/v1${path}`);
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const page = await res.json();
rows.push(...page.data);
cursor = page.next_cursor;
} while (cursor);
return rows;
}

Cursor-paginated (return data + has_more + next_cursor):

  • GET /v1/verification-sessions
  • GET /v1/customers
  • GET /v1/events

Plain lists (return just { "data": [...] }, no cursor — these are naturally bounded per merchant):

  • GET /v1/customers/{address}/notes
  • GET /v1/webhook-endpoints
  • GET /v1/webhook-endpoints/{id}/deliveries