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

# Errors

Every error — from any endpoint, any status — is a flat
[RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) body with content type
`application/problem+json`:

```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"
}
```

| Field        | What it is                                                 |
| ------------ | ---------------------------------------------------------- |
| `code`       | The stable machine key. Match on this.                     |
| `category`   | Coarse "how should I react" bucket — see below.            |
| `type`       | Dereferenceable URL for `code` in the public catalog.      |
| `title`      | Short human label. For display, not for matching.          |
| `detail`     | Constant, client-safe sentence explaining what to do.      |
| `status`     | The HTTP status, repeated in the body.                     |
| `request_id` | Quote this to support; also returned as `RRay-Request-Id`. |
| `instance`   | The request path that produced the error.                  |

## Match on `code`, not on text

`code` is the contract: dotted, namespaced (`<domain>.<outcome>`), and never
reused for a different meaning. `title` is for humans and may be reworded;
`status` alone is too coarse (several unrelated conflicts all map to 409).

```python
if problem["code"] == "rate_limit.exceeded":
    sleep(int(response.headers["Retry-After"]))
elif problem["code"].startswith("idempotency."):
    ...
```

## Switch on `category` for retry policy

When you want one branch per *kind* of failure rather than per code, use
`category`. It is deliberately small and stable:

| Category         | React by                                            |
| ---------------- | --------------------------------------------------- |
| `validation`     | Fixing the request — it will never succeed as sent  |
| `authentication` | Presenting a valid credential                       |
| `authorization`  | Using a credential that is allowed to do this       |
| `not_found`      | Not retrying; the resource does not exist for you   |
| `conflict`       | Resolving the state clash (idempotency, uniqueness) |
| `rate_limit`     | Waiting out `Retry-After`, then retrying            |
| `billing`        | Raising a limit, paying, or upgrading the plan      |
| `upstream`       | Retrying — a downstream dependency failed           |
| `server`         | Retrying; if it persists, contact support           |

## Every `type` URL resolves

Follow the `type` link, or browse
[`GET /errors`](https://api.rray.app/errors) for the whole catalog and
`GET /errors/{code}` for one entry. The catalog is generated from the same
registry the API raises from, so it is never out of date. It is
unauthenticated — it *is* the documentation. Each API Reference endpoint also
documents exactly the errors it can return, with examples.

## Structured causes

Errors that point at a specific cause carry one extra array.

`invalid_params` locates a bad request field or header:

```json
{
  "code": "validation.request_invalid",
  "category": "validation",
  "status": 422,
  "invalid_params": [
    {"field": "to", "message": "field required", "kind": "missing"}
  ]
}
```

`422 validation.request_invalid` is the schema-validation failure and always
carries `invalid_params`, one entry per failing field. Errors about the state
of a resource rather than a field carry `state_errors` in the same shape, with
`resource` and `resource_id` in place of `field`.

For `POST /v1/messages/batch` in `permissive` mode, each failing position
carries this same problem shape inside its own `items[].error` object, so a
per-item failure reads exactly like a top-level one.

## When you contact support

Quote the `request_id` — it links your response to our internal logs exactly.