> ## Documentation Index
> Fetch the complete documentation index at: https://www.plungeai.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors, limits & headers

> The platform has two error envelopes. The execution planes (tools, agents, workflows, MCP, discovery, traces) use the router envelope; the models plane (/v1/chat/completions, /v1/embeddings, /v1/models) uses the OpenAI-shaped envelope from the inference gateway. Branch on the HTTP status and, …


The platform has **two error envelopes**. The **execution planes** (tools, agents, workflows, MCP, discovery, traces) use the router envelope; the **models plane** (`/v1/chat/completions`, `/v1/embeddings`, `/v1/models`) uses the OpenAI-shaped envelope from the inference gateway. Branch on the HTTP status and, on the execution planes, the `X-Error-Code` header — not on any field's type.

## 13.1 The two envelopes

**Execution planes** (`orchestration/api-gateway/lib/respond.ts`, `lib/format.ts`):

```json
{ "error": { "code": "unknown_agent", "message": "No such agent: foo", "…": "extra fields" } }
```
- Header `X-Error-Code: <code>` is **always** set — clients can branch on it without parsing the body.
- Header `x-request-id` echoes the server-minted id.
- The body is **format-negotiated**: `?format=` / `body.format` / `Accept` / a `text/yaml` content-type mirror select `json` (default), `yaml`, `markdown`, or `text`. An unknown explicit format → `400 invalid_format` (as JSON). `code`+`message` survive every format; `missing`, `contract`, `hint`/`detail`, `outcome` and the execution id render on `markdown` (the MCP renderer) and `missing` on `text`; every field survives `json`/`yaml`.

**Models plane** (`inference/gateway/src/errors.ts`) — OpenAI shape, no `X-Error-Code`:

```json
{ "error": { "message": "Invalid API key", "type": "authentication_error", "code": "invalid_api_key", "request_id": "…" } }
```
- `code` is a **string**, `type` groups it, `request_id` is inside the body. Response headers carry `x-request-id` (and `x-correlation-id` when you sent an `x-request-id`), never `X-Error-Code`.
- A known path called with the wrong method (`GET /v1/chat/completions`, `POST /v1/models`) answers `405 method_not_allowed` with an `Allow` header naming the one method it serves:

| HTTP | `code` | When | Do |
|---|---|---|---|
| 405 | `method_not_allowed` | A models-plane path called with the wrong method | Use the method in `Allow` |

## 13.2 Execution-plane error codes

| HTTP | `code` | When | Do |
|---|---|---|---|
| 400 | `invalid_json` / `invalid_body` / `invalid_request` | Body is not valid JSON (the message quotes the parser's position) / its root is not an object or mapping | Fix the body |
| 400 | `invalid_format` | `?format=`/`body.format` is not `json\|yaml\|markdown\|text` | Use a known format |
| 400 | `missing_prompt` | Agent execute without a `prompt`/`input` string and without a `messages` user turn | Send a prompt or messages |
| 400 | `missing_workflow` | Workflow execute with none of `workflow`, `agent` or `tasks` | Send a `workflow:` document or a bare `agent:` body ([10.2](/one-api/workflows#102-inline-execute)) |
| 400 | `ambiguous_body` | `agent:` next to `workflow:`/`tasks:` in one body | Send one of them |
| 400 | `reserved_field` | A bare-agent field named like an engine task key (`id`, `type`, `agent`, `depends_on`, `mission_ref`); the body names them | Rename the field |
| 400 / 422 | `invalid_workflow` | Engine rejected the workflow (structural) / a save route refused the YAML: schema or kind lint, with `errors` and `warnings` and nothing written ([16.1](/one-api/bots#161-lifecycle)) | Fix the CNL |
| 400 | `invalid_yaml` | A `text/yaml` body is not valid YAML (every execute door) | Fix the YAML syntax (the message quotes js-yaml's reason and position) |
| 401 | `unauthorized` | Missing/invalid/revoked `ozk_` key | Send a valid key ([Auth](/one-api/authentication)) |
| 401 | `invalid_signature` | Internal HMAC identity failed | Service-to-service only |
| 403 | `refused` | Trust fence — agent/tool refused ([13.5](#135-trust-fence)) | Do not retry |
| 404 | `not_found` | No route for method+path | Check the path |
| 403 | `agent_not_active` | The agent's card is not `status:active` (refused before dispatch) | Pick an active agent ([Discovery](/one-api/discovery)) |
| 403 | `lock_admin_required` | `PUT /v1/workflows/{id}/permissions` set `locks` and the caller is not a team admin over the workflow; nothing was written ([16.2](/one-api/bots#162-policy-and-budget)) | Ask an admin |
| 404 | `unknown_agent` | No such agent id on the agents, tools and workflows planes, and for a task inside a workflow body | Discover a valid id ([Discovery](/one-api/discovery)) |
| 404 | `agent_not_found` | `POST /v1/execute` ([16.9](/one-api/bots#169-the-one-door)): `agent:` names no saved agent of yours (or snapshot) and no building block | Check the id, and the `@n` snapshot |
| 404 | `unknown_tool` | No such tool id | Discover a valid id |
| 404 | `workflow_not_found` | No saved workflow with that id for this key (a foreign id answers the same) | Check the id/owner |
| 404 | `execution_not_found`, `schedule_not_found`, `trigger_not_found`, `group_not_found`, `binding_not_found`, `file_not_found`, `version_not_found`, `template_not_found`, `skill_not_found`, `memory_not_found`, `connection_not_found`, `unknown_provider` | The Bots routes ([16](/one-api/bots)) and connections ([7](/one-api/connectors)): no such row for this key. Another account's id answers the same | Check the id |
| 404 | `run_not_found` | MCP run never opened, closed, or expired (30 min idle); on `GET /v1/memory/runs/{id}`, no past run with that id | Open a new run with `POST /v1/mcp/runs`; retrying never revives it |
| 404 | `not_ready` | Result not stored yet (still running) | Poll again |
| 409 | `approval_required` | Trust fence — human approval pending ([13.5](#135-trust-fence)) | Approve, then retry |
| 409 | `execution_failed` | Result route: the run failed | Read the message |
| 409 | `duplicate_execution_id` | Replayed `x-trace-id` UUID hit the run PK | Send a fresh UUID or omit it |
| 409 | `not_paused`, `execution_finished` | `POST /v1/executions/{id}/continue` on a run that already completed; `steer` or `stream` on a run that is no longer running ([16.3](/one-api/bots#163-executions-and-approvals)) | Use `followup`, or read the output |
| 409 | `memory_conflict`, `skill_name_taken`, `name_taken`, `binding_limit` | A memory id that is also inside another entry; a skill name held by a platform skill or another account; a bot-group name you already use; the account's binding limit ([16.6](/one-api/bots#166-workspace-inbox-bindings-groups), [16.7](/one-api/bots#167-memory-and-skills)) | Forget by text; pick another name; unbind one |
| 410 | `session_expired`, `result_expired` | No conversation session is left for `continue`; the stored result of `output` is gone ([16.3](/one-api/bots#163-executions-and-approvals)) | Start a new run |
| 413 | `payload_too_large` | Body over the size cap ([13.6](#136-limits-body-caps-timeouts)) | Shrink the body |
| 422 | `invalid_params` | Outcome `needs_input` — agent needs different args | Fix the params |
| 422 | `unknown_model` | Pinned model absent/inactive in the catalog | Discover a model |
| 422 | `not_paired`, `memory_rejected`, `invalid_name`, `invalid_members` | A chat you did not pair; a memory write the store refuses (it carries `current_entries`); a bot-group name or member list that fails its checks ([16.6](/one-api/bots#166-workspace-inbox-bindings-groups), [16.7](/one-api/bots#167-memory-and-skills)) | Fix the request |
| 422 | `empty_completion` | Run completed but produced no visible content | Raise `max_tokens` (≥64) |
| 424 | `connection_required` | Outcome `needs_connection` — no connected account | Connect the account ([Connectors](/one-api/connectors)) |
| 424 | `credential_required` | Outcome `needs_api_key` — no API key for a connector | Add the key |
| 429 | `rate_limited` | Tier RPM/RPD or failed-auth cap ([13.4](#134-rate-limits)) | Honour `Retry-After` |
| 500 | `config_error` | Server misconfiguration | Report with `x-request-id` |
| 500 | `internal_error` | Unhandled router error | Report with `x-request-id` |
| 502 | `engine_error` | Engine failed (untagged) | Retry; report if persistent |
| 502 | `upstream_error`, `storage_error`, `scheduler_error`, `registry_unavailable` | Upstream service/storage error; on the Bots routes the storage, scheduler or skill registry failed and no detail is returned | Retry with backoff |
| 502 | `tool_call_failed` | Tool execution failed | Read the message |
| 502 | `no_servers_connected` | MCP: no server connected | Connect a server |
| 502 | `result_unavailable` | Run finished but the result could not be retrieved | Retry the result route |
| 503 | `not_enabled`, `unavailable`, `connections_unavailable` | A feature is not switched on in this deployment yet: channel bindings, bot groups, outbound hooks, connections ([16](/one-api/bots)) | Retry later |
| 503 | `agent_unavailable` | Outcome `unavailable` — agent temporarily down | Retry later |
| 503 | `provider_unconfigured` | Chosen provider has no key on this platform | Choose another provider |
| 503 | `money_plane_unavailable` | Model routes not staged on this tier | Retry later |

## 13.3 Outcome → HTTP mapping

Execute routes classify an agent/tool **outcome** (`routes/_util.ts outcomeStatus`). A `success: false` engine result tagged with an outcome remaps to a caller-actionable status; an untagged failure stays `502 engine_error`.

| Outcome | HTTP | `code` |
|---|---|---|
| `needs_input` | 422 | `invalid_params` |
| `needs_connection` | 424 | `connection_required` |
| `needs_api_key` | 424 | `credential_required` |
| `unavailable` | 503 | `agent_unavailable` |
| `error` (or untagged) | 502 | `execution_failed` / `engine_error` |
| `needs_approval` | 409 | `approval_required` ([13.5](#135-trust-fence)) |

## 13.4 Rate limits

**Execution planes** (`orchestration/mcp-gateway/rate-limiter.ts`, a Durable Object; enforced in `lib/auth.ts`). Counted on **POST** executions only — catalog `GET`s are free. Per tier:

| Tier | per minute | per day |
|---|---|---|
| `free` | 30 | 1,000 |
| `pro` | 100 | 10,000 |
| `enterprise` | 300 | 100,000 |

A key with no tier is treated as `free`. A per-key `rate_limit` override is ignored on these planes (`lib/auth.ts` does not pass it to `checkRateLimit`); it applies only on `https://mcp.plungeai.com/v1`. Failed authentications are metered per client IP (`auth-fail`: 20/min, 500/day) to bound key brute-force. On a limit hit: `429 rate_limited` with a `Retry-After` header (seconds). A key is never shut out for longer than **ten minutes**: reaching the day limit pauses the key for 10 minutes (`Retry-After` ≤ 600), after which its day count starts over (2026-10-05; the minute limit waits out the minute as before). The per-tier numbers are the defaults: the operator can change them or switch the limits off (the failed-authentication meter stays on), see [2](/one-api/authentication).

**Money plane** (`inference/gateway/src/auth/rate-limit.ts`). Per **key**, atomic per-minute window (`RateLimiterDO`, KV soft-cap fallback): default **600 req/min** (`RATE_LIMIT_RPM_DEFAULT`) → `429 rate_limit_exceeded`. Separate from billing caps: `429 insufficient_quota` (monthly limit / trial allowance), `402 insufficient_balance` (empty wallet), `429 spend_cap_exceeded` (guardrail) — see [3.10](/one-api/models#310-guardrails), [3.13](/one-api/models#313-billing--metering).

## 13.5 Trust fence

Two statuses gate risky actions:

- **`403 refused`** — the agent/tool refused the action outright (policy). Do not retry unchanged.
- **`409 approval_required`** — a human must approve before the action runs. Approve out-of-band, then re-issue the request.

## 13.6 Limits: body caps, timeouts

- **Body cap (execution planes):** `MAX_REQUEST_SIZE`, default **1 MiB** (`routes/_util.ts` — `1_048_576` bytes). Enforced against `Content-Length` first, then streamed; over → `413 payload_too_large`. The models plane relies on the provider/runtime body limits.
- **Timeouts:** there is no gateway-imposed request timeout beyond the Cloudflare Workers runtime and each upstream's own. Streams have no idle timeout on the money/workflows plane; the agents plane sends `: OCEAN PROCESSING` keepalives ([4.3](/one-api/streaming#43-agents-plane-openai-shaped-stream)).
- **Storage errors** are scrubbed: a D1/SQLite failure surfaces as `502 upstream_error` "storage error" (the raw error is logged, never leaked); internal service-binding names are redacted from messages.

## 13.7 Retry guidance

- **429** — back off and honour `Retry-After`. Distinguish `rate_limited`/`rate_limit_exceeded` (throttle) from `insufficient_quota`/`spend_cap_exceeded` (billing — retrying won't help until the window resets or billing changes).
- **5xx** (`502`/`503`) — retry with exponential backoff; the models plane already retries `429`/`5xx` once per candidate and fails over ([3.3](/one-api/models#33-routing-models-sort-failover)).
- **4xx other than 429** — do **not** retry unchanged; fix the request. `needs_*` outcomes (`422`/`424`) are yours to resolve (params, connection, key).
- Always include the `x-request-id` when reporting an error.

## 13.8 Response headers

| Header | Plane | Meaning |
|---|---|---|
| `x-request-id` | both | Server-minted request id: the router's on the execution planes, the inference gateway's (= `error.request_id`) on the models plane. A client-sent `x-request-id` is never adopted — it comes back as `x-correlation-id`. |
| `x-ocean-router-id` | models | The router's own id for a models-plane call (the id `x-request-id` carried before the two were split). |
| `X-Error-Code` | execution (errors) | The error `code`, for header-only branching. |
| `X-Execution-Id` | execution | The run id on results, streams, and async pointers. |
| `x-correlation-id` | both | Echo of your `x-request-id` when you send one. A successful models-plane call without one carries the router's trace id here. |
| `x-generation-id` | models (`/api/v1`) | The `gen-…` completion id (= the body `id`), the id `GET /generation?id=` takes ([3.6](/one-api/models#36-response-headers)). |
| `x-ocean-dropped-params` | models | Request fields the routed provider shape could not take, comma-separated ([3.2](/one-api/models#32-request-fields)). |
| `x-cache` | models | `hit` / `miss` on cache-eligible chat requests ([3.11](/one-api/models#311-response-cache)). |
| `x-ocean-cost-usd` | models | The settled price in USD (6 decimals) of a non-stream `POST /v1/chat/completions`; `0.000000` on a response-cache hit. Absent on streams (use `usage.cost`) and on the execution planes (priced later). |
| `x-ocean-agent` · `x-ocean-tasks` · `x-ocean-version` | execution (`POST /v1/execute`) | What ran: the saved agent or building block (absent for an inline workflow), how many top-level tasks, and the snapshot (`<n>`, `live` or `inline`) ([16.9](/one-api/bots#169-the-one-door)). |
| `Server-Timing` | execution | `total;dur=<ms>` timing. |
| `X-Content-Type-Options` | execution | `nosniff` on every router response. |
| `Retry-After` | both | Seconds to wait after a `429`. |
| `Access-Control-Allow-Origin` (+ CORS) | both | `*`; both planes answer `OPTIONS` preflight and advertise the same methods (`GET, POST, PUT, PATCH, DELETE, OPTIONS`). |
| `Access-Control-Expose-Headers` | both | The headers a browser client may read: `x-request-id`, `x-ocean-router-id`, `x-execution-id`, `X-Error-Code`, `Retry-After`, `Server-Timing`, `x-cache`, `x-ocean-cost-usd`, `x-generation-id`, `x-correlation-id`, `x-ocean-dropped-params`, `x-ocean-agent`, `x-ocean-tasks`, `x-ocean-version`, `Content-Disposition`. |
| `Allow` | models | On a `405`: the one method the path serves, plus `OPTIONS`. |
| `Content-Type` | both | `application/json` \| `text/event-stream` \| negotiated `text/yaml`\|`text/markdown`\|`text/plain`. |

## 13.9 Verified examples

Unauthenticated execution-plane request → `401 unauthorized` with the `x-error-code` header:

```bash
curl -is "$API_BASE/v1/agents"
```

Unknown route → `404 not_found`, header `x-error-code: not_found`, plus `x-request-id`:

```bash
curl -is "$API_BASE/v1/nope"
```

Agent execute without a prompt → `400 missing_prompt`:

```bash
curl -s "$API_BASE/v1/agents/one-agent/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H "Content-Type: application/json" -d '{}'
```

Unknown agent id → `404 unknown_agent` (the trust/fence pre-dispatch check):

```bash
curl -s "$API_BASE/v1/agents/zzz-not-an-agent/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H "Content-Type: application/json" -d '{"prompt":"hi"}'
```
