Pagination
Cursor (keyset) pagination, following nextCursor until hasMore is false. No page numbers, no total count, no drift.
Every list endpoint paginates the same way: cursor (keyset) pagination. You
ask for a page, you get up to limit items plus a nextCursor, and you keep
passing that cursor back until hasMore is false. There are no page numbers and
no total count, and that's deliberate.
Offset pagination (?page=3) breaks the moment the underlying data changes: a new
record inserted while you're paging shifts every offset, so you skip rows or see
them twice. Keyset pagination pins each page to a stable position in the sort
order (createdAt, id) descending, so a page is consistent even as new resources
arrive. New items appear on page one; the page you're reading doesn't shuffle
under you.
No total, on purpose
There is no total count. Counting an entire tenant's history on every list
call is expensive and almost never what you need. Follow hasMore until it's
false: that's your "end of list".
A list response is the standard envelope with data as an array and an extra
pagination block:
{
"success": true,
"statusCode": 200,
"data": [
{ "id": "acuinf7h3k9q2x8m4npay", "status": "settled", "baseAmount": 150000, "currency": "NGN" },
{ "id": "acuinf2c5v8b1n4m7kpay", "status": "expired", "baseAmount": 50000, "currency": "NGN" }
],
"pagination": {
"limit": 25,
"hasMore": true,
"nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA2LTI0VDA5OjQxOjEyLjAwNFoiLCJpZCI6Ii4uLiJ9"
},
"meta": { "requestId": "req_1a3c5e7b9d2f4a6c8b0e1d33" }
}dataT[]requiredThe page of resources, newest first (createdAt desc, id as the tie-break).
pagination.limitnumberrequiredThe page size that was applied.
pagination.hasMorebooleanrequiredtrue if more pages exist. Stop when this is false.
pagination.nextCursorstring | nullrequiredAn opaque base64url token. Pass it as ?cursor= to fetch the next page; null
on the last page.
limitnumberqueryoptionaldefault: 25How many items to return (capped server-side). Applies to the first call and is carried forward by the cursor.
cursorstringqueryoptionalThe nextCursor from the previous page. Omit it for the first page. It's an
opaque token: don't decode, construct, or persist it as anything but a
string; its internals can change.
The cursor is opaque
Never parse, build, or hand-edit a cursor. A tampered or malformed token is
rejected with a 400 (INVALID_CURSOR). Treat it as a black box you only
ever echo back.
async function listAllPayments(key: string) {
const all: PaymentResponseData[] = [];
let cursor: string | null = null;
do {
const url = new URL("https://sandbox.api.acute.network/v1/payments");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
const page: ApiPaginated<PaymentResponseData> = await res.json();
all.push(...page.data);
cursor = page.pagination.hasMore ? page.pagination.nextCursor : null;
} while (cursor);
return all;
}# first page
curl "https://sandbox.api.acute.network/v1/payments?limit=100" -H "Authorization: Bearer acuinf_test_…"
# next page: feed back the nextCursor verbatim
curl "https://sandbox.api.acute.network/v1/payments?limit=100&cursor=eyJjcmVhdGVkQXQi…" -H "Authorization: Bearer acuinf_test_…"Loop on hasMore, not on a count
The clean termination condition is hasMore === false (which is when
nextCursor is null). Don't try to compute a page count from a total: there
isn't one.
{
"success": false,
"statusCode": 400,
"error": {
"type": "validation_error",
"code": "INVALID_CURSOR",
"message": "The pagination cursor is malformed or invalid.",
"details": { "fields": [] }
},
"meta": { "requestId": "req_5d7f9a1c3e6b8a0d2c4f6e55" }
}