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

# Errors

The API uses ordinary HTTP status codes. A `2xx` means success. Anything else comes with a JSON body that says what went wrong.

## The error shape

Every error body has the same shape:

```json
{
  "statusCode": 403,
  "error": "insufficient_scope",
  "message": "This API key needs the experts:write scope.",
  "requiredScopes": ["experts:write"],
  "missingScopes": ["experts:write"],
  "requestId": "req_01M726GS84892WE68HMF286YFH"
}
```

| Field        | What it is                                                                                                                                                                                                                            |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `statusCode` | The HTTP status, repeated in the body.                                                                                                                                                                                                |
| `error`      | A short code. On the API's own refusals it is a stable `snake_case` code, such as `insufficient_scope` or `rate_limited`, that you can branch on. On some generic errors it is the HTTP reason phrase instead, such as `"Not Found"`. |
| `message`    | A sentence for people. It can change; don't parse it.                                                                                                                                                                                 |
| `requestId`  | The request's id. It matches the `X-Request-Id` response header.                                                                                                                                                                      |

Some errors add fields of their own: `requiredScopes` and `missingScopes` on `insufficient_scope`, and `retryAfterSeconds` on `rate_limited`.

Branch on `statusCode` first, then on `error`.

### Request ids

Every response, success or error, carries an `X-Request-Id` header such as `req_01M726GS2P1N4DCP7FARE8MA4F`. Quote it when you contact support. It also finds the request in **Developers → Request log**.

You can send your own `X-Request-Id` (up to 64 letters, digits, `.`, `_`, `:` or `-`). The API keeps it and returns it, so you can match API requests to your own logs. Anything else is replaced with a new id.

### Server errors

An unexpected failure answers `500` with a generic message and an `errorId`, which is the same value as `requestId`:

```json
{
  "statusCode": 500,
  "message": "Something went wrong. Please try again.",
  "errorId": "req_01M726GS2P1N4DCP7FARE8MA4F",
  "requestId": "req_01M726GS2P1N4DCP7FARE8MA4F"
}
```

The details stay in Rydhm's logs under that id.

## Status codes

| Status | Codes in `error`            | What it means and what to do                                                                                                                                                            |
| ------ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | varies                      | The request is not valid: a missing or malformed field. The `message` says which. Fix the request.                                                                                      |
| `401`  | `invalid_api_key`           | The key is unknown, wrong, or deleted.                                                                                                                                                  |
| `401`  | `api_key_disabled`          | The key, or its service account, is disabled. An admin can enable it.                                                                                                                   |
| `401`  | `api_key_expired`           | The key has expired. Create a new one.                                                                                                                                                  |
| `401`  | `Unauthorized`              | No key was sent.                                                                                                                                                                        |
| `402`  | `payment_required`          | The workspace has run out of credits. See the note below.                                                                                                                               |
| `403`  | `insufficient_scope`        | The key lacks a scope the route needs. `missingScopes` lists them.                                                                                                                      |
| `403`  | `expert_not_allowed`        | The key is limited to other experts.                                                                                                                                                    |
| `403`  | `not_available_to_api_keys` | The route can't be called with a key at all: it is only for people signed in to the app.                                                                                                |
| `403`  | `ip_not_allowed`            | The key's IP allowlist doesn't include your address.                                                                                                                                    |
| `403`  | `plan_limit`                | The workspace's plan allows no more of this, such as experts or knowledge. See the note below.                                                                                          |
| `403`  | `workspace_deleted`         | The workspace has been deleted.                                                                                                                                                         |
| `404`  | `Not Found`                 | Nothing with that id exists in your workspace. Ids from other workspaces also answer `404`.                                                                                             |
| `409`  | `stale_edit` (in `code`)    | On `PATCH /v1/agents/{id}` with `baseUpdatedAt`: someone saved the expert after you loaded it. Reload and try again. This one error puts its code in a `code` field instead of `error`. |
| `409`  | `idempotency_in_progress`   | A request with the same `Idempotency-Key` is still running.                                                                                                                             |
| `422`  | `idempotency_key_reused`    | The `Idempotency-Key` was already used with a different body.                                                                                                                           |
| `429`  | `rate_limited`              | Too many requests. Wait `Retry-After` seconds.                                                                                                                                          |
| `429`  | `concurrency_limit`         | On voice sessions: the workspace already has as many calls running as it may. Try again when one ends.                                                                                  |
| `5xx`  | varies                      | A problem on Rydhm's side. Retry with backoff, and quote the `requestId` if it keeps happening.                                                                                         |

> **Note**
>
> Rydhm doesn't enforce credits or plan limits yet. When it does, routes that start paid work (chats, voice sessions, meetings) can answer `402 payment_required`, and creating experts or adding knowledge can answer `403 plan_limit`. Handle both now so your integration is ready.

### Errors during a stream

A chat reply stream (`GET /v1/chat/conversations/{id}/stream`) sends its headers before the reply is generated. If the reply fails after that, the stream carries an `error` frame instead of an HTTP status:

```text
data: {"type":"error","message":"Something went wrong. Please try again."}
```

## Rate limits

Rate limits stop runaway loops and abuse. They are set well above normal use.

Each limited route counts requests in fixed windows, in two buckets at once:

* **the workspace**, shared by everyone and every key in it;
* **the key**, so one integration can't use up the whole workspace's budget.

A request is refused when either bucket is full.

### Headers

Rate-limited routes return the state of the tightest budget the request touched:

| Header                  | Meaning                                           |
| ----------------------- | ------------------------------------------------- |
| `X-RateLimit-Limit`     | Requests allowed in the current window.           |
| `X-RateLimit-Remaining` | Requests left in the current window.              |
| `X-RateLimit-Reset`     | Seconds until the window resets.                  |
| `Retry-After`           | On a `429` only: seconds to wait before retrying. |

```text
HTTP/1.1 201 Created
X-RateLimit-Limit: 240
X-RateLimit-Remaining: 233
X-RateLimit-Reset: 21
```

Routes without a limit, such as reading an expert, don't send these headers.

### When you hit a limit

The API answers `429`:

```json
{
  "statusCode": 429,
  "error": "rate_limited",
  "message": "Too many chat requests. Try again in 12 seconds.",
  "retryAfterSeconds": 12,
  "requestId": "req_01M726GS2P1N4DCP7FARE8MA4F"
}
```

Wait `Retry-After` seconds, then retry. Don't retry in a tight loop: every retry inside the window is refused too. The [SDKs](/developers/sdk-typescript) retry `429` responses for you with backoff.

### Limits by route

| Routes                                                                                        | Per workspace                  | Per key                        |
| --------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------ |
| Chat: `/v1/chat/conversations/…` (start, send, stream, read, delete)                          | 240 per minute                 | 240 per minute                 |
| Knowledge: `/v1/agents/{id}/kb/…`, `/v1/kb/collections/…`, `/v1/kb/docs/…`, `/v1/kb/crawls/…` | 60 per minute                  | 60 per minute                  |
| Website imports: `POST …/kb/crawl`, `POST /v1/kb/crawls/{id}/refresh`                         | 10 per hour                    | 10 per hour                    |
| Website import preview: `POST /v1/kb/crawl/preview`                                           | 30 per hour                    | 30 per hour                    |
| Voice sessions: `POST /v1/voice/sessions`                                                     | 60 per minute                  | 30 per minute                  |
| Meetings (reads and changes): `/v1/meetings/…`                                                | 600 per minute                 | 600 per minute                 |
| Send to a meeting: `POST /v1/meetings`                                                        | 30 per minute and 300 per hour | 20 per minute and 300 per hour |
| Analytics: `/v1/analytics/…`                                                                  | 300 per minute                 | 300 per minute                 |
| Analytics live view: `GET /v1/analytics/live`                                                 | 600 per minute                 | 600 per minute                 |
| Analytics CSV export: `GET /v1/analytics/export.csv`                                          | 10 per minute                  | 10 per minute                  |
| Tool tests: `POST /v1/workspace-tools/{id}/test`, `POST /v1/workspace-tools/test-draft`       | 30 per minute                  | 30 per minute                  |
| Tool from curl: `POST /v1/workspace-tools/parse-curl`                                         | 60 per minute                  | 60 per minute                  |
| Tool drafts: `POST /v1/workspace-tools/draft`                                                 | 30 per hour                    | 30 per hour                    |
| MCP: `POST /mcp/experts/{id}`                                                                 | 120 per minute                 | 60 per minute                  |

The MCP endpoint also lets one key run at most 3 `ask_…` questions at the same time. See [MCP](/developers/mcp).

Voice sessions are also capped by how many calls a workspace may run at once. Past the cap, a new session gets `429` with `error: "concurrency_limit"`.

### Public routes

The public routes under `/v1/public/…` need no key. They are limited per visitor IP address and per expert instead. A refused request gets `429` with a `Retry-After` header and the message "Too many requests. Please wait a moment and try again." These routes don't send the `X-RateLimit-*` headers.