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
limitRows per page. Defaults to 50; values above the endpoint's maximum are clamped to it.
cursorOpaque. 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

EndpointMax limit
GET /api/v1/emails100
GET /api/v1/contacts100
GET /api/v1/contacts/{id}/events200
GET /api/v1/automations/{id}/runs200
GET /api/v1/webhooks/{id}/deliveries100
GET /api/v1/suppressions500

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.

On this page