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

# Credentials

## Bearer authentication

Authenticate every request with a Bearer token:

```
Authorization: Bearer rray_live_...
```

A missing, malformed or revoked credential returns `401 auth.unauthorized`. Every
response carries an `RRay-Request-Id` header — quote it to support.

The public error catalog (`GET /errors` and `GET /errors/{code}`) is the one
unauthenticated surface: it is the dereference target of the `type` URL on every error,
so it has to be readable without a key.

## Two kinds of credential

| Credential        | Looks like                        | Where it comes from            | Scope                          |
| ----------------- | --------------------------------- | ------------------------------ | ------------------------------ |
| **Workspace key** | `rray_live_...` / `rray_test_...` | Dashboard → **Keys**           | Bound to exactly one workspace |
| **Platform key**  | account-level token               | Dashboard → **Settings → API** | Acts for the whole account     |

A **workspace key** is the recommended send credential. It carries its workspace, so it
never names one per request. The secret is shown once at creation; we store only a
peppered hash. Revoking a key takes effect immediately.

A **platform key** acts for the whole account: it can send mail *and* manage
account-level resources (workspaces, billing limits, keys). Because it is not tied to a
workspace, it must name the target workspace on every operation that acts in one.

## The workspace header

`RRay-Workspace-Id` selects the workspace for a single-workspace operation: sending,
listing messages, reading one message or its body. Proprietary headers are
`RRay-`-prefixed; there are no `X-` headers in this API.

| Credential    | `RRay-Workspace-Id` | Outcome                                    |
| ------------- | ------------------- | ------------------------------------------ |
| Workspace key | omitted             | Routed to the key's own workspace          |
| Workspace key | sent                | `400 validation.workspace_header_invalid`  |
| Platform key  | sent, owned by you  | Routed to that workspace                   |
| Platform key  | omitted             | `400 validation.workspace_header_required` |
| Platform key  | not one of yours    | `404 workspace.not_found`                  |

A workspace key rejects the header rather than ignoring it — even when the value matches
its own workspace. Silently accepting it would let you believe the header was routing
somewhere it never was.

```json
{
  "type": "https://api.rray.app/errors/validation.workspace_header_invalid",
  "title": "Invalid workspace header",
  "status": 400,
  "code": "validation.workspace_header_invalid",
  "category": "validation",
  "detail": "The workspace header does not match the grammar this endpoint expects.",
  "request_id": "36661076a55a4275bb6acc7714494f1f",
  "instance": "/v1/messages",
  "invalid_params": [
    {
      "field": "RRay-Workspace-Id",
      "message": "Workspace headers are not accepted with a workspace-scoped API key — the key already pins its workspace.",
      "kind": "workspace_header_invalid"
    }
  ]
}
```

A foreign or unknown workspace id always returns `workspace.not_found` — we never reveal
whether an id exists under another account. The header rules, including the plural
`RRay-Workspace-Ids` on `GET /v1/stats`, are laid out per operation in
[Workspaces](/docs/concepts/workspaces).

## Which operations need the header

| Operation                                                                 | `RRay-Workspace-Id`                                         |
| ------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `POST /v1/messages`, `POST /v1/messages/batch`                            | Required with a platform key, rejected with a workspace key |
| `GET /v1/messages`, `GET /v1/messages/{id}`, `GET /v1/messages/{id}/body` | Required with a platform key, rejected with a workspace key |
| `GET /v1/stats`                                                           | As above, or `RRay-Workspace-Ids` for several workspaces    |
| `POST /v1/workspaces`, `GET /v1/workspaces`                               | Not used — these act on the account                         |
| `GET /v1/usage`, `GET /v1/billing`                                        | Not used — these act on the account                         |

## Errors you can get from a credential

| Code                                   | Status | When                                                                    |
| -------------------------------------- | ------ | ----------------------------------------------------------------------- |
| `auth.unauthorized`                    | 401    | No credential, or one that is malformed, expired or revoked             |
| `auth.forbidden`                       | 403    | A valid credential that this operation does not accept                  |
| `auth.platform_credential_required`    | 403    | A workspace key on an account-level operation                           |
| `validation.workspace_header_required` | 400    | Platform key without a workspace header                                 |
| `validation.workspace_header_invalid`  | 400    | Header sent with a workspace key, or the wrong header for the operation |
| `validation.workspace_header_conflict` | 400    | Both `RRay-Workspace-Id` and `RRay-Workspace-Ids` sent                  |
| `workspace.not_found`                  | 404    | A workspace id that is not one of yours                                 |

## Keeping keys safe

* Keys are bearer secrets. Send them from your server, never from a browser or a mobile
  app.
* Use `rray_test_...` for development and `rray_live_...` for production, so a mistake in
  one cannot send real mail from the other.
* Rotate by minting the new key, deploying it, then revoking the old one. Revocation is
  immediate, so the overlap matters.
* Minting more keys for a workspace does not raise its
  [rate limit](/docs/concepts/rate-limits) — they share one budget.