Pagination
Two list styles — keyset cursors on Emails, limit/offset elsewhere.
SendMailOS uses two pagination models. Both are honest about how much data is left; pick the calls accordingly.
Keyset (cursor) — the Emails list
High-volume, append-only lists like GET /v1/emails
use keyset pagination. It's stable under concurrent writes and never skips or
repeats rows.
{
"object": "list",
"has_more": true,
"next_cursor": "3f8b1c2d-1a2b-4c3d-9e8f-aabbccddeeff",
"data": [ /* ... */ ]
}next_cursor is the id of the last email in the page. Pass it back as after
to fetch the next (older) page, and stop when has_more is false; use
before to page in the other direction.
curl "https://api.sendmailos.com/v1/emails?limit=50&after=3f8b1c2d-1a2b-4c3d-9e8f-aabbccddeeff" \
-H "Authorization: Bearer sk_live_your_key"limit is clamped to a maximum of 100 on this endpoint.
Offset — most list resources
Lower-volume resources (subscribers, webhooks, api-keys, suppressions,
audiences, …) use limit + offset with a pagination summary. The array is
keyed by the resource name, not data:
{
"subscribers": [ /* ... */ ],
"pagination": { "total": 240, "limit": 20, "offset": 40, "has_more": true }
}curl "https://api.sendmailos.com/v1/subscribers?limit=20&offset=40" \
-H "Authorization: Bearer sk_live_your_key"On these offset endpoints a limit outside 1–100 falls back to the default
of 20 (not the maximum) — always follow has_more rather than assuming a
page count. A few resources (such as domains) return
their full list with no pagination at all.
Idempotency
Make every write safely retryable with an Idempotency-Key.
Per-campaign email stats GET
Read-only rollup for one campaign: the latest delivery run's send-pipeline counters (`run`, null when the campaign has never been sent) plus `email_logs` engagement aggregates. The campaign must belong to the caller's org — an unknown or foreign-org id returns 404 (never zeroed aggregates).