> 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.

# Quickstart

## 1. Create a workspace key

Open the **Keys** page in the [dashboard](https://dash.rray.app) and create a
key. You'll get an `rray_live_...` (or `rray_test_...`) secret — store it
safely, it is shown exactly once.

A workspace key is bound to one workspace, so it needs no workspace header.
Every request authenticates with a Bearer header:

```bash
export RRAY_API_KEY="rray_live_..."
```

## 2. Send a message

```bash
curl -X POST https://api.rray.app/v1/messages \
  -H "Authorization: Bearer $RRAY_API_KEY" \
  -H "Idempotency-Key: order-1042-receipt" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "receipts@yourdomain.com",
    "to": ["customer@example.com"],
    "subject": "Your receipt",
    "html": "<h1>Thanks!</h1>",
    "text": "Thanks!"
  }'
```

Use a `from` address on a sending domain you've added in the dashboard — that
is what lets us DKIM-sign the message. The API won't reject an unknown domain,
but unsigned mail is very likely to be rejected by the recipient's provider.
`to` is an array; `cc`, `bcc`, `reply_to` and `attachments` are optional.

A `202 Accepted` returns the message id and nothing else — the message has been
accepted for delivery, not yet delivered:

```json
{
  "id": "msg_01M4ANAVD7EYMHY0PWSH5PBDJD"
}
```

Two headers worth understanding before production:

* `Idempotency-Key` — makes retries safe; see
  [Idempotency](/docs/concepts/idempotency).
* `RRay-Workspace-Id` — required only when you authenticate with a platform
  key. A workspace key rejects it; see
  [Credentials](/docs/concepts/authentication).

Send responses also carry `RRay-Quota-Limit`, `RRay-Quota-Remaining` and
`RRay-Quota-Reset`, reporting your plan's monthly and daily allowance as
structured-field dictionaries.

To send up to 100 messages in one request, `POST /v1/messages/batch` with a
JSON array of the same message objects. Every ceiling the API enforces — batch
size, attachment budget, rate limits, retention — is listed on
[Limits & facts](/docs/get-started/limits).

## 3. Track delivery

Statuses progress `accepted → queued → delivered`, or end in `bounced`,
`deferred` or `failed` — [Message lifecycle](/docs/concepts/message-lifecycle)
explains what each one means and what to do about it. Poll one message:

```bash
curl https://api.rray.app/v1/messages/msg_01M4ANAVD7EYMHY0PWSH5PBDJD \
  -H "Authorization: Bearer $RRAY_API_KEY"
```

```json
{
  "id": "msg_01M4ANAVD7EYMHY0PWSH5PBDJD",
  "object": "message",
  "status": "delivered",
  "from": "receipts@yourdomain.com",
  "subject": "Your receipt",
  "deliveries": [
    {
      "address": "customer@example.com",
      "role": "to",
      "status": "delivered"
    }
  ],
  "attachments": [],
  "workspace_id": "ws_01KYA3AWSZGCB9YMNKM5AQBA8G",
  "created_at": "2026-10-07T07:48:02.607020Z",
  "events": [
    {
      "id": "evt_01M4ANAVDNW22V8M1EHEPQJ8DM",
      "object": "delivery_event",
      "type": "accepted",
      "address": "customer@example.com",
      "created_at": "2026-10-07T07:48:02.590140Z"
    },
    {
      "id": "evt_bfa8d9d5a3eeaafd9bf798230b2cfaed",
      "object": "delivery_event",
      "type": "queued",
      "address": "customer@example.com",
      "created_at": "2026-10-07T07:48:02Z"
    },
    {
      "id": "evt_54e3166c7b31ddc76f699de9ef0efff7",
      "object": "delivery_event",
      "type": "delivered",
      "address": "customer@example.com",
      "created_at": "2026-10-07T07:48:02Z"
    }
  ]
}
```

Or list recent messages with a filter:

```bash
curl "https://api.rray.app/v1/messages?status=delivered&limit=20" \
  -H "Authorization: Bearer $RRAY_API_KEY"
```

List responses are cursor-paginated — follow `pagination.next_cursor` for
older messages, `pagination.prev_cursor` for newer ones
([how it works](/docs/concepts/pagination)). You can also filter by `id`,
`recipient` and `created_at`.

## If something goes wrong

Errors are flat `application/problem+json` bodies with a stable `code` to
match on and a `request_id` to quote to support:

```json
{
  "type": "https://api.rray.app/errors/message.not_found",
  "title": "Message not found",
  "status": 404,
  "code": "message.not_found",
  "category": "not_found",
  "detail": "The message does not exist or is not visible to your credential.",
  "request_id": "df7b848fabd945889820b257e004c894",
  "instance": "/v1/messages/msg_00000000000000000000000000"
}
```

The full catalog lives at [`GET /errors`](https://api.rray.app/errors) and in
the [Errors guide](/docs/concepts/errors).