Pagination
One list envelope, one cursor, every collection.
Every list endpoint answers the same shape:
{
"object": "list",
"data": [{ "id": "msg_3aF9c1", "status": "delivered" }],
"has_more": true,
"next_cursor": "MjAyNi0xMC0wNVQxMDowMDowMC4wMDBafG1zZ18zYUY5YzE"
}To fetch the next page, pass next_cursor back as ?cursor= and keep going
until has_more is false:
curl "https://sendandretain.com/api/v1/emails?limit=100&cursor=MjAyNi0x..." \
-H "Authorization: Bearer aem_your_key"| Parameter | |
|---|---|
limit | Rows per page. Defaults to 50; values above the endpoint's maximum are clamped to it. |
cursor | Opaque. Only ever pass a next_cursor you received — never build one. |
Pages are newest first and stable: rows created while you page do not shift the pages you have not read yet, and rows that share a timestamp are never skipped at a page boundary.
A malformed cursor is a 400, never a silent restart from page one —
restarting is how a consumer processes the same rows twice.
Which lists page
| Endpoint | Max limit |
|---|---|
GET /api/v1/emails | 100 |
GET /api/v1/contacts | 100 |
GET /api/v1/contacts/{id}/events | 200 |
GET /api/v1/automations/{id}/runs | 200 |
GET /api/v1/webhooks/{id}/deliveries | 100 |
GET /api/v1/suppressions | 500 |
The other collections — templates, template versions, automations, segments,
domains, senders and webhook endpoints — are small by construction and return
everything in one page, in the same envelope with has_more: false. Code that
follows next_cursor works on both.
The SDKs do the looping for you:
for await (const email of sendandretain.emails.list.iterate({ status: "bounced" })) {
console.log(email.to);
}?before= is deprecated
GET /emails, GET /contacts and GET /webhooks/{id}/deliveries still accept
the old ?before=<ISO created_at> and answer it correctly. Those responses carry
Deprecation, Sunset and Link headers naming ?cursor= as the replacement.
before cannot be combined with cursor.