Concepts
Pagination & Incremental Sync
List endpoints are cursor-paginated, and most of them double as a sync surface: pass updated_since and you get only what changed, plus a watermark to store for the next run.
Cursor mechanics
There is no page number and no offset. Cursors are keyset-based: each page's meta.next_cursor encodes where the walk stopped, so a row inserted while you are halfway through a collection cannot make you skip or repeat one the way an offset would.
{
"data": [ /* up to `limit` rows */ ],
"meta": {
"request_id": "req_...",
"has_more": true,
"next_cursor": "cur_...",
"synced_through": "2026-08-03T11:59:55Z"
}
}meta.has_more— whether another page exists. This is the loop condition.meta.next_cursor— acur_-prefixed string, ornullon the last page. It is opaque: store it and send it back, never parse it.meta.synced_through— the sync watermark, present only on the final page. See below.
cursor_invalid. One code means your client needs exactly one branch: discard the saved cursor and restart from the first page.has_more to decide whether to keep going.Parameters
limitintegerdefault: 251..100cursorstringnext_cursor from the previous page. Bound to the sort it was issued under.sortstringdefault: updated_atorderstringdefault: descasc or desc).updated_sincestring (ISO 8601)synced_through you stored.An unsupported value never degrades silently: sort_unsupported and filter_unsupported mean your code is wrong and retrying will not help — distinct from cursor_invalid, which means your saved state is stale. See errors.
Walking a collection
# First page
curl "https://api.mailneo.co/api/v2/subscribers?limit=100" \
-H "x-api-key: $MAILNEO_API_KEY"
# While meta.has_more is true, pass meta.next_cursor back
curl "https://api.mailneo.co/api/v2/subscribers?limit=100&cursor=$CURSOR" \
-H "x-api-key: $MAILNEO_API_KEY"Incremental sync with updated_since
Walking every row on every run is wasteful and will hit the rate limit long before it hits correctness problems. Endpoints that support sync accept updated_since and return a watermark in meta.synced_through. The loop:
# First run: no watermark, walk every page.
curl "https://api.mailneo.co/api/v2/subscribers?limit=100" \
-H "x-api-key: $MAILNEO_API_KEY"
# Follow meta.next_cursor until meta.has_more is false, then store the
# meta.synced_through from that FINAL page.
curl "https://api.mailneo.co/api/v2/subscribers?limit=100&cursor=$CURSOR" \
-H "x-api-key: $MAILNEO_API_KEY"
# Every run after that: only what changed.
curl "https://api.mailneo.co/api/v2/subscribers?updated_since=$SYNCED_THROUGH" \
-H "x-api-key: $MAILNEO_API_KEY"Why synced_through appears only on the final page
The watermark asserts "everything changed up to this instant has been handed to you". Mid-walk, that is not yet true — pages you have not fetched may hold rows the watermark would claim to cover. If the server emitted it on every page, a client that crashed mid-walk and persisted it would silently skip those rows on every future run, forever — the worst kind of sync bug, because nothing ever looks wrong. So the server refuses to emit it until has_more is false, and the one honest place to read it is the final envelope.
Three rules that keep a sync correct
- Upsert by id, never insert. The watermark is held 5 seconds behind real time so a row written during your request cannot slip between two runs. The cost of that safety is occasionally re-seeing a row you already have.
- Store the watermark only after the last page. Saving it mid-walk and crashing loses every row you had not read yet.
- Deletes are invisible. A deleted row has no timestamp to be newer than your watermark, so it simply stops appearing. If your system must mirror deletions, reconcile with a periodic full walk.
updated_sinceis absent rather than accepted-and-ignored — an endpoint that accepted it without honouring it would let you build a poll that silently missed rows forever. Each endpoint's reference page shows exactly which parameters it takes.