Pagination

Cursor-based, both directions, constant cost at any history size

List endpoints (GET /v1/emails, /v1/api-keys, /v1/workspaces) paginate by cursor and always return rows newest-first.

The envelope

1{
2 "object": "list",
3 "data": [ ... ],
4 "pagination": {
5 "limit": 20,
6 "total": 1042,
7 "total_capped": false,
8 "has_next": true,
9 "has_prev": false,
10 "next_cursor": "v1_eyJib3VuZGFyeV9pZCI6...",
11 "prev_cursor": "v1_eyJib3VuZGFyeV9pZCI6..."
12 }
13}

Walking the list

  • First request — no cursor, optionally limit (1–100, default 20).
  • Older items: request again with ?cursor=<next_cursor>.
  • Newer items: ?cursor=<prev_cursor>. From the top of the list this returns rows created after your first request — a built-in “load fresh” poll; an empty data means you’re at the top.
  • Stop when next_cursor is null.
$curl "https://api.rray.app/v1/emails?status=delivered&limit=50" -H "..."
$# then repeat with &cursor=<pagination.next_cursor> until null

Rules cursors enforce

  1. Keep the query identical between hops. The cursor is fingerprinted with your filters and limit; changing them mid-walk returns 400 validation.cursor_filters_mismatch. Start a fresh walk instead.
  2. Cursors are opaque. Don’t parse or construct them — malformed values return 400 validation.cursor_invalid.

About total

Counting is expensive on large histories, so total appears only on the first page (no cursor) and is capped at 10 000: "total": 10000, "total_capped": true means “10 000 or more”. While walking with cursors, total is null — use has_next/has_prev.