Pagination
List endpoints use the pagination model that fits the resource. High-volume email history is cursor-paginated; several resource lists use limit and offset.
Email cursors
GET /emails accepts limit up to 200 and returns next_cursor when another page exists.
GET /emails?limit=50&status=delivered
200 OK
{
"data": [ /* up to 50 rows */ ],
"next_cursor": "2026-06-20T10:32:11.000Z|2f3a..."
}
GET /emails?limit=50&cursor=2026-06-20T10%3A32%3A11.000Z%7C2f3a...&status=deliveredTreat a null cursor as the end of the collection. Cursors are opaque client values; pass them back unchanged through a URL query encoder. The cursor is exclusive, so the first row of the next page follows the final row of the previous page.
Iterate safely
Keep filters fixed while following the cursor chain.
async function* listEmails(status) {
let cursor;
do {
const url = new URL('https://sendix.dev/api/v1/emails');
url.searchParams.set('limit', '200');
if (status) url.searchParams.set('status', status);
if (cursor) url.searchParams.set('cursor', cursor);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.SENDIX_API_KEY}` },
});
if (!response.ok) throw new Error(`List failed: ${response.status}`);
const page = await response.json();
for (const email of page.data) yield email;
cursor = page.next_cursor ?? undefined;
} while (cursor);
}Cursor ordering uses creation time plus the email ID as a deterministic tie-breaker. Do not split, modify, sort, or synthesize cursor values in your application. If the API returns invalid_cursor, discard the stored cursor and restart the scan rather than repeatedly retrying the same request.
status and any other filters unchanged until traversal ends. Reusing a cursor with different filters can skip records or produce a view that is difficult to reason about.Offset pages
Some lower-volume lists, including webhook deliveries, inbound received mail, audiences, broadcasts, automations, and support/admin views, accept limit and offset.
GET /webhook-deliveries?limit=50&offset=100&status=failed
200 OK
{
"data": [ /* 50 rows */ ],
"limit": 50,
"offset": 100
}Endpoint-specific maximums are documented by the API schema; public list endpoints commonly allow 200 or 500 rows. Do not assume every offset endpoint returns a total field. When total or has_more is present, use it; otherwise stop when the returned page contains fewer than the requested limit.
Time-range filters
Analytics and operational endpoints can expose endpoint-specific windows such as days, since, start, or end. Use the exact fields listed for that endpoint; there is no universal time-filter contract shared by all list routes. Send UTC RFC 3339 timestamps whenever a timestamp field is accepted.
Choose bounded windows
For exports and reconciliation, process a bounded period, persist your checkpoint only after the page is committed, then continue. This makes a worker restart predictable and avoids repeatedly scanning the newest records.
Operational rules
- • Set an explicit
limit; do not depend on an endpoint's default forever. - • Deduplicate by resource ID when a long-running export must tolerate restarts or concurrent writes.
- • Apply backoff to
429and transient server failures without advancing the checkpoint. - • Never use array position as a durable identity; records can change while you traverse offset pages.
- • Store only the checkpoint needed for recovery and expire abandoned cursor jobs.

