> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.rray.app/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.rray.app/_mcp/server.

# Pagination

List endpoints (`GET /v1/messages`, `GET /v1/workspaces`) paginate by
**cursor** and always return rows newest-first.

## The envelope

```json
{
  "object": "list",
  "data": [ ... ],
  "pagination": {
    "limit": 2,
    "total": 13,
    "total_capped": false,
    "has_next": true,
    "has_prev": false,
    "next_cursor": "v1_eyJiIjogWyIyMDI2LTEwLTAyVDA2OjM4OjU0...",
    "prev_cursor": "v1_eyJiIjogWyIyMDI2LTEwLTA2VDEzOjExOjIx..."
  }
}
```

## 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` (equivalently, when `has_next` is `false`).

```bash
curl "https://api.rray.app/v1/messages?status=delivered&limit=50" \
  -H "Authorization: Bearer $RRAY_API_KEY"
# 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` and `has_prev`.

## Filtering messages

`GET /v1/messages` composes these filters (all AND-ed), and each one is part of
the cursor fingerprint:

| Parameter    | Behaviour                                                                         |
| ------------ | --------------------------------------------------------------------------------- |
| `id`         | The exact message id returned by `POST /v1/messages`                              |
| `status`     | Aggregate status; repeat the param for several (`status=queued&status=delivered`) |
| `recipient`  | Messages addressed to this exact address on any channel (to, cc or bcc)           |
| `created_at` | Creation time, RFC 3339 — see below                                               |

`created_at` takes one or two values:

* **Two values** → an inclusive range, order irrelevant
  (`created_at=2026-10-01T00:00:00Z&created_at=2026-10-31T23:59:59Z`).
* **One full timestamp** → exact match (`created_at=2026-10-07T09:30:00Z`).
* **One date, no time** → that whole calendar day (`created_at=2026-10-07`).

Include an offset for local-time filtering (`2026-10-07T00:00:00+07:00`); a
value without an offset is read as UTC. To show a user their own day, send the
two local-day boundaries with their offset.