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:

GET /payments, a page
json
{
  "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[]required

The page of resources, newest first (createdAt desc, id as the tie-break).

pagination.limitnumberrequired

The page size that was applied.

pagination.hasMorebooleanrequired

true if more pages exist. Stop when this is false.

pagination.nextCursorstring | nullrequired

An opaque base64url token. Pass it as ?cursor= to fetch the next page; null on the last page.

limitnumberqueryoptionaldefault: 25

How many items to return (capped server-side). Applies to the first call and is carried forward by the cursor.

cursorstringqueryoptional

The 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.

list-all-payments.ts
ts
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;
}
the same, with curl
bash
# 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.

400 INVALID_CURSOR
json
{
  "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" }
}