Reference

Pagination

GET /v1/messages pages with a cursor. Ask for a page size, then follow next_cursor until it is null. Other list endpoints use limit and offset.

The message collection can be large, so the API returns it one page at a time. GET /v1/messages is cursor-based. You ask for a page of a given size, and each response returns a next_cursor that points at the row after the last one you received. You follow that cursor to walk the whole collection. The cursor uses no page number and no offset, so it never skips a row and never repeats one when the data changes under you.

How it works

Every paginated request takes two query parameters, and every response carries a next_cursor field:

limitintegerOptional
How many items to return per page. The default is 50 and the maximum is 100. Sendara clamps a value above 100 down to 100. A value that is not a positive integer falls back to the default.
cursorstringOptional
An opaque cursor returned as next_cursor by the previous page. Omit it on the first request. Pass it verbatim and never construct or modify one yourself.

Results are ordered newest first by created_at descending, with id as a stable tie-breaker. Because the cursor encodes that exact position rather than a numeric offset, inserting or deleting rows between page fetches never shifts the window: you will not see a row twice or miss one that slipped across a page boundary.

Fetch the first page

Omit cursor on the first request. Set limit to the page size you want, up to 100.

first-page.sh
curl "https://api.sendara.dev/v1/messages?limit=50" \
  -H "Authorization: Bearer sk_live_xxx"

The response wraps the page in a typed array plus a cursor:

{
  "messages": [
    { "id": "msg_a1b2c3", "channel": "email", "status": "delivered",
      "message_type": "transactional", "created_at": "2026-06-14T10:00:00Z" },
    { "id": "msg_9f21",   "channel": "email", "status": "bounced",
      "message_type": "transactional", "created_at": "2026-06-14T09:15:00Z" }
  ],
  "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNi0xNFQwOToxNTowMFoiLCJpZCI6Im1zZ185ZjIxIn0"
}

The collection lives under a named key, such as messages for GET /v1/messages. The next_cursor field sits alongside it at the top level.

Fetch the next page

Pass the next_cursor from the previous response back as the cursor query parameter. Keep limit the same or change it. The cursor is independent of page size.

next-page.sh
curl "https://api.sendara.dev/v1/messages?limit=50&cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wNi0xNFQwOToxNTowMFoiLCJpZCI6Im1zZ185ZjIxIn0" \
  -H "Authorization: Bearer sk_live_xxx"
The cursor is opaque: it is a base64url-encoded snapshot of the last row's position. Treat it as a black box and pass it back unchanged. Do not decode, parse, or hand-build one. Its internal shape is not part of the API contract and may change.

Know when you are done

When no row follows the page that you received, next_cursor comes back as null. That is your signal to stop. There is no further page to fetch.

{
  "messages": [
    { "id": "msg_0001", "channel": "email", "status": "delivered",
      "message_type": "transactional", "created_at": "2026-06-12T08:00:00Z" }
  ],
  "next_cursor": null
}
Drive your loop off next_cursor, not the page length. A full page can still be the last page, and an empty collection returns "messages": [] with "next_cursor": null, never a missing field.

Auto-paginate the whole collection

To pull every row, loop until next_cursor is null, feeding each page's cursor into the next request. Use the maximum limit of 100 to minimize round-trips, and carry any filters (like status or a date range) on every call.

all-messages.ts
const all = [];

for await (const message of sendara.messages.list({
  status: "bounced",
  limit: 100,
})) {
  all.push(message);
}

console.log(`fetched ${all.length} bounced messages`);
Auto-pagination over a large account can issue many requests in quick succession. Each one counts against your rate limit, so honor X-RateLimit-Remaining and back off on a 429 using the Retry-After header rather than hammering the endpoint. See the API reference for rate-limit details.

Filters and pagination together

Filters narrow what is paginated. The cursor controls where you are within that filtered set. On GET /v1/messages you can combine channel, status, and a from/to time range (both RFC 3339) with limit and cursor. Keep the filters identical across every page of a single walk. Changing them mid-walk invalidates the cursor's position.

curl -G "https://api.sendara.dev/v1/messages" \
  -H "Authorization: Bearer sk_live_xxx" \
  --data-urlencode "status=bounced" \
  --data-urlencode "from=2026-06-01T00:00:00Z" \
  --data-urlencode "to=2026-06-14T23:59:59Z" \
  --data-urlencode "limit=100"

Errors

A malformed or tampered cursor is rejected with 400 invalid_request rather than silently returning the first page, so a corrupted cursor never makes you re-read data you already have. Always pass back exactly what the API returned.

{
  "error": {
    "code": "invalid_request",
    "message": "Invalid cursor",
    "status": 400
  }
}

An out-of-range or non-numeric limit never errors. It is clamped to the 1…100 range (or the default of 50) so a bad value cannot take a request down.