Skip to main content
List endpoints (e.g. GET /v1/customers) use cursor pagination. No offset, no page number — cursors are deterministic under writes and won’t skip or duplicate records when new customers are ingested mid-walk.

How it works

GET /v1/customers?limit=50
{
  "object": "list",
  "data": [ /* up to 50 customers */ ],
  "has_more": true,
  "next_cursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA2LTI0VDAwOjAwOjAwLjAwMFoiLCJpZCI6IjU1MGU4NDAwLS4uIn0",
  "limit": 50
}
FieldTypeMeaning
objectstringAlways "list". Lets you distinguish list envelopes from single-record responses.
dataarrayThe page of records, never null. Empty when no records match.
has_morebooleantrue if at least one more page exists. Use this as the loop condition.
next_cursorstring | nullOpaque cursor to pass back. null on the last page (mirrors has_more === false).
limitintegerThe limit echoed back (after defaulting/clamping).
total_countintegerOnly present when include=total_count was requested. See below.
Pass next_cursor back as cursor on the next request:
GET /v1/customers?limit=50&cursor=eyJjcmVhdGVkQXQiOiI...
When you reach the end, has_more is false and next_cursor is null.

Limit

MinDefaultMax
limit150100

What’s in a cursor

The cursor is opaque — base64url-encoded JSON containing the last row’s (createdAt, id). Don’t parse or generate cursors yourself; pass them back as-is.

Combining with filters

Filters work alongside cursors. The cursor sticks to the filter set from the original request — don’t change filters mid-walk:
GET /v1/customers?status=IN_PROGRESS&aml_status=NEEDS_REVIEW&limit=100

Sample walk loop

async function* walkAllCustomers(filters = {}) {
  let cursor = null;
  while (true) {
    const url = new URL('https://app.instantcompliance.ai/api/v1/customers');
    Object.entries(filters).forEach(([k, v]) => url.searchParams.set(k, v));
    url.searchParams.set('limit', '100');
    if (cursor) url.searchParams.set('cursor', cursor);

    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.IC_API_KEY}` }
    });
    const { data, has_more, next_cursor } = await res.json();
    for (const customer of data) yield customer;
    if (!has_more) break;
    cursor = next_cursor;
  }
}

Total count (opt-in)

For UIs that want to render “Showing 50 of 1,283” or for sizing a background job, request include=total_count:
GET /v1/customers?limit=50&include=total_count
{
  "object": "list",
  "data": [ /* … */ ],
  "has_more": true,
  "next_cursor": "eyJjcmVhdGVkQXQiOiI…",
  "limit": 50,
  "total_count": 1283
}
total_count reflects the full filter set, not the current page — walking forward does not shrink it. It costs an extra COUNT(*) query per call, so don’t request it on hot polling loops where you don’t need the number. If include is omitted, total_count is not returned at all (rather than null) so consumers can detect “did I ask for it?” unambiguously.

Polling for changes

For “what changed since I last looked?” use the updated_since filter alongside pagination. See Polling for status for a robust loop.