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
| Field | Type | Meaning |
|---|---|---|
object | string | Always "list". Lets you distinguish list envelopes from single-record responses. |
data | array | The page of records, never null. Empty when no records match. |
has_more | boolean | true if at least one more page exists. Use this as the loop condition. |
next_cursor | string | null | Opaque cursor to pass back. null on the last page (mirrors has_more === false). |
limit | integer | The limit echoed back (after defaulting/clamping). |
total_count | integer | Only present when include=total_count was requested. See below. |
next_cursor back as cursor on the next request:
has_more is false and next_cursor is null.
Limit
| Min | Default | Max | |
|---|---|---|---|
limit | 1 | 50 | 100 |
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:Sample walk loop
Total count (opt-in)
For UIs that want to render “Showing 50 of 1,283” or for sizing a background job, requestinclude=total_count:
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 theupdated_since filter
alongside pagination. See Polling for status
for a robust loop.
