# Errors and limits

> The REST API's error shapes and codes, which errors to retry, rate limits and their headers, status codes, pagination, and CORS.

Source: https://docs.runbear.io/api/errors-and-limits

Last updated: 2026-09-30

This page is the reference for what the REST API sends back when something goes
wrong, and for the limits that apply to every integration. Use it to decide,
from the status and `code` alone, whether to retry, fix the request, or stop.

## Error shape

Most errors share one envelope:

```json
{
  "statusCode": 403,
  "code": "forbidden_by_agent_allowlist",
  "error": "forbidden",
  "message": "This API key is not permitted to access the specified agent."
}
```

- `statusCode` repeats the HTTP status.
- `code` is the machine-readable cause, when there is one. Branch on it.
- `error` names the status class: `bad_request` (400), `unauthorized` (401),
  `forbidden` (403), `not_found` (404), `conflict` (409) or
  `too_many_requests` (429).
- `message` is written for a person. Don't parse it.

A body that fails schema validation answers `400` with
`"code": "FST_ERR_VALIDATION"` and a message naming the invalid field.

A server error hides its cause:

```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An internal error occurred. Please try again later."
}
```

### Other shapes

A few responses use a route-specific body with no `statusCode` or `code`. The
specific cause is in `error` instead:

| Where                                                 | Body                                                                                                                         |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `POST /v1/sessions` and `/v1/sessions/refresh`, `404` | `{ "error": "agent_not_found" \| "thread_not_found", "message" }`                                                            |
| Agent and assistant create and update, `400`          | `{ "error": "<cause>", "message" }`, for example `agent_limit_reached`, `system_prompt_too_long` or `provider_type_mismatch` |
| Chat and run routes, `422`                            | `{ "error": "unprocessable_entity", "message" }`                                                                             |

Read `code` when it is present and fall back to `error`.

## Status codes

| Status          | Meaning                                                                                                | Retry?                                                                                                         |
| --------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `400`           | The request is invalid                                                                                 | No. Fix the request                                                                                            |
| `401`           | The credential is missing, invalid or expired                                                          | Only `pass_expired`, once, after renewing. See [authentication errors](/api/api-keys.md#authentication-errors) |
| `403`           | The credential isn't allowed to do this                                                                | No                                                                                                             |
| `404`           | The resource doesn't exist, or isn't visible to your organization                                      | No                                                                                                             |
| `409`           | The request conflicts with the resource's current state                                                | No, until the state changes                                                                                    |
| `422`           | The request is valid but this agent can't process it. See [Chat](/api/chat.md#agents-the-api-cant-run) | No                                                                                                             |
| `429`           | A rate limit or quota was reached                                                                      | Yes, after `Retry-After` seconds, unless `code` is a lifetime cap                                              |
| `500` and above | Something failed on Runbear's side                                                                     | Yes, with backoff                                                                                              |

A `404` for a thread is identical whether the thread doesn't exist or belongs to
another organization.

### 403 codes

Every `403` is terminal: retrying, or renewing the credential, won't change the
answer.

| `code`                                   | Cause                                                                    |
| ---------------------------------------- | ------------------------------------------------------------------------ |
| `forbidden_by_key_role`                  | The API key lacks the capability this operation requires                 |
| `forbidden_by_agent_allowlist`           | The agent is outside the API key's allowlist                             |
| `forbidden_org_endpoint_for_scoped_key`  | An API key with an agent allowlist called an organization-level endpoint |
| `forbidden_for_session_pass`             | A Web SDK session pass called a route it can't use                       |
| `forbidden_agent_not_in_pass`            | A session pass named an agent other than its own                         |
| `forbidden_trace_access_for_hipaa_agent` | The agent uses HIPAA data handling, which disables trace access          |
| `forbidden_ai_gateway_not_permitted`     | The organization doesn't have the AI Gateway feature                     |
| `forbidden_google_drive_sa`              | The key's user may not use this Google Drive service account             |
| `forbidden_service_account_manage`       | The key's user may not manage this Google Drive service account          |

See [Authentication and API keys](/api/api-keys.md#how-a-request-is-checked) for how
the first three are decided. Stream errors, which arrive inside a `200`, are
listed on [Streaming](/api/streaming.md#errors).

## Rate limits

| Limit                       | Counted per                         | Applies to                                                                                                                                                   |
| --------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 600 requests per 60 seconds | Organization and agent              | `POST /v1/threads`, `POST /v1/threads/{threadId}/runs` and `/runs/stream`, `POST /v1/chat/completions`, `POST /v1/chat/suggestions`, `POST /v1/files/upload` |
| 600 requests per 60 seconds | Organization and agent              | Creating a Web SDK session, `POST /v1/sessions`                                                                                                              |
| 600 requests per 60 seconds | Organization                        | Renewing a Web SDK session, `POST /v1/sessions/refresh`                                                                                                      |
| 60 requests per 60 seconds  | Web SDK session                     | Every request made with a session pass                                                                                                                       |
| 100 turns                   | Web SDK session, for its whole life | Runs made with a session pass                                                                                                                                |
| 240 renewals                | Web SDK session, for its whole life | `POST /v1/sessions/refresh`                                                                                                                                  |
| 30 requests per minute      | Organization, per API server        | `GET /v1/agents/{id}/traces` and `GET /v1/agents/{id}/messages/{messageId}/trace`, together                                                                  |
| 20 requests per minute      | Organization, per API server        | `GET /v1/agents/{id}/traces/export`                                                                                                                          |

- The chat limit is shared by everyone who talks to the agent through your
  organization, including every visitor of a [Web SDK](/api/web-sdk.md) widget for
  that agent. Contact [support@runbear.io](mailto:support@runbear.io) before a
  high-traffic launch.
- A run made with a Web SDK session pass counts against both the chat limit
  and the session's own limits. See
  [Web SDK session limits](/api/web-sdk/authentication.md#session-limits) for what the
  widget does when one is reached.
- The trace limits are counted on each API server separately, so the budget you
  observe can be higher than the number above. Pace yourself by the headers
  rather than by a fixed count.
- Routes not listed here have no request limit of their own. API-key
  validation can still be throttled under heavy load; that answers `429` with
  `Retry-After` like any other limit. Until a pending API fix ships, a
  throttled validation answers `401` `api_key_invalid` instead, so a sudden
  `401` for a key that worked moments ago is worth one delayed retry.

### Rate-limit headers

Responses from rate-limited routes carry these headers on every response,
successful or not:

| Header                | Value                                                          |
| --------------------- | -------------------------------------------------------------- |
| `RateLimit-Limit`     | The size of the budget                                         |
| `RateLimit-Remaining` | What is left of it                                             |
| `RateLimit-Reset`     | Seconds until it resets                                        |
| `Retry-After`         | On a `429` only: seconds to wait before retrying, at most 3600 |

### 429 responses

```json
{
  "statusCode": 429,
  "code": "rate_limited",
  "error": "too_many_requests",
  "message": "Too many requests. Retry after the window resets."
}
```

| `code`                          | Meaning                                    | What to do                           |
| ------------------------------- | ------------------------------------------ | ------------------------------------ |
| `rate_limited`                  | A per-window limit was reached             | Wait `Retry-After` seconds and retry |
| `session_turn_cap_exhausted`    | The Web SDK session used all its turns     | Start a new session                  |
| `session_refresh_cap_exhausted` | The Web SDK session can't be renewed again | Start a new session                  |

Trace `429`s carry no `code`; wait `Retry-After` seconds. Treat any other `429`
the same way.

## Success status codes

Most successful calls answer `200`. These differ:

| Operation                                                                      | Status                              |
| ------------------------------------------------------------------------------ | ----------------------------------- |
| `POST /v1/threads`                                                             | `200`                               |
| `POST /v1/sessions`                                                            | `201`                               |
| `POST /v1/sessions/refresh`                                                    | `200`                               |
| `POST /v1/api-keys`                                                            | `201`                               |
| `POST /v1/agents`                                                              | `201`                               |
| `POST /v1/agents/{id}/tool-integrations`                                       | `201`                               |
| `POST /v1/integrations/google-drive/service-accounts`                          | `201`                               |
| `GET /v1/ai-gateway`                                                           | `204` when no gateway is configured |
| `DELETE /v1/agents/{id}/hooks` and `DELETE /v1/agents/{id}/session-start-hook` | `204`                               |

## Pagination

| List                                  | Style                           | Page size                                 |
| ------------------------------------- | ------------------------------- | ----------------------------------------- |
| `GET /v1/threads`                     | `cursor`, returns `nextCursor`  | Default 100, max 100                      |
| `GET /v1/credits/usage/periods`       | `cursor`, returns `nextCursor`  | Default 12, max 24                        |
| `GET /v1/credits/usage/entries`       | `cursor`, returns `nextCursor`  | Default 500, max 1000                     |
| `GET /v1/agents/{id}/traces`          | `page` and `limit`              | Default 50, max 100                       |
| `GET /v1/agents/{id}/traces/export`   | `cursor`, returns `meta.cursor` | Varies: pages hold whole traces           |
| `GET /v1/threads/{threadId}/messages` | None                            | The newest 200 messages, with `truncated` |

Cursors are opaque. Pass back exactly what you received, and stop when it is
`null`. On threads and credits, a cursor the API didn't issue is refused with
`400`.

## CORS

The API can be called from a browser on any origin:

- `Access-Control-Allow-Origin: *`, without credentials. Authenticate with the
  `Authorization` header, never cookies.
- Allowed request headers: `content-type`, `authorization`, `connection`,
  `cache-control` and `x-requested-with`.
- Readable response headers: `RateLimit-Limit`, `RateLimit-Remaining`,
  `RateLimit-Reset`, `Retry-After` and `x-runbear-thread-id` (the last is
  still rolling out; see [Chat](/api/chat.md#chat-completions)).
- Methods: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD` and `OPTIONS`.

Calling the API from a browser with an API key exposes the key. Use
[Web SDK session passes](/api/web-sdk/authentication.md) for browser traffic.

## Related

- [Authentication and API keys](/api/api-keys.md)
- [Streaming](/api/streaming.md) — errors inside a stream
- [Web SDK errors](/api/web-sdk/errors.md)
- [REST API overview](/api/overview.md)
