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

# Errors

> Every error carries a stable type, a specific code, and a request id you can quote to support.

Every non-2xx response from the API returns a JSON body with a structured
`error` object and a `request_id`. Branch on `error.type` or `error.code` —
never on the human-readable `message`, which is written for people and may be
reworded at any time.

```json theme={null}
{
  "error": {
    "type": "not_found_error",
    "code": "scenario_not_found",
    "message": "No scenario with that id.",
    "param": "scenario_id",
    "doc_url": "https://docs.waterr.ai/api-reference/errors#scenario_not_found"
  },
  "request_id": "req_4f1c8a2b9d3e5f6071829a3b4c5d6e7f",
  "status": "error",
  "message": "No scenario with that id."
}
```

<Note>
  The top-level `status` and `message` fields are retained for backwards
  compatibility with earlier integrations. New code should read `error`.
</Note>

## Fields

<ParamField body="error.type" type="string">
  The broad error class. **Stable for the life of `v1`** — safe to switch on
  permanently. See the table below for the complete set.
</ParamField>

<ParamField body="error.code" type="string">
  The specific reason. New codes may be introduced within an existing `type`,
  so treat an unrecognised code as its `type` rather than as a failure to
  parse.
</ParamField>

<ParamField body="error.message" type="string">
  A human-readable explanation. Display it, log it, but do not parse it.
</ParamField>

<ParamField body="error.param" type="string">
  Present when the error is attributable to one request parameter — the field
  to highlight in a form or fix in a payload.
</ParamField>

<ParamField body="error.doc_url" type="string">
  A deep link to the section of this page describing the code.
</ParamField>

<ParamField body="request_id" type="string">
  Unique per request, also returned as the `X-Request-Id` response header —
  on **every** response, including successful ones. Include it in any support
  request; it is how we find your call in our logs.
</ParamField>

## Error types

| HTTP | `error.type` | Meaning |
| - | - | - |
| 400, 402, 405, 413, 415, 422 | `invalid_request_error` | The request was malformed, incomplete, or not something the resource supports. Fix the request and retry. |
| 401 | `authentication_error` | Missing, malformed, revoked, or expired credentials. Retrying without changing the key will not help. |
| 403 | `permission_error` | The key is valid but not entitled to this resource or action. Common with read-only OAuth tokens attempting a write. |
| 404, 410 | `not_found_error` | No such resource, or it is no longer available. Also returned instead of a 403 where confirming existence would leak information across tenants. |
| 409 | `conflict_error` | The request conflicts with the current state of the resource. |
| 429 | `rate_limit_error` | You are over a published limit. See [Rate limits](/api-reference/rate-limits). |
| 5xx | `api_error` | Something failed on our side. Safe to retry with backoff. Quote the `request_id`. |

## Handling errors

Retry `api_error` (5xx) and `rate_limit_error` (429) with exponential backoff.
Never blind-retry a `4xx` other than 429 — the request will fail identically
every time.

<CodeGroup>
  ```python Python theme={null}
  import time, httpx

  def call(client, method, url, **kwargs):
      for attempt in range(5):
          r = client.request(method, url, **kwargs)
          if r.status_code < 400:
              return r.json()

          body = r.json()
          err = body.get("error", {})

          if r.status_code == 429:
              time.sleep(int(r.headers.get("Retry-After", 2 ** attempt)))
              continue
          if r.status_code >= 500:
              time.sleep(2 ** attempt)
              continue

          raise RuntimeError(
              f"{err.get('code')}: {err.get('message')} "
              f"(request_id={body.get('request_id')})"
          )

      raise RuntimeError("exhausted retries")
  ```

  ```javascript Node theme={null}
  async function call(url, init = {}, attempt = 0) {
    const res = await fetch(url, init);
    if (res.ok) return res.json();

    const body = await res.json();

    if (res.status === 429) {
      const wait = Number(res.headers.get("Retry-After") || 2 ** attempt);
      await new Promise((r) => setTimeout(r, wait * 1000));
      return call(url, init, attempt + 1);
    }
    if (res.status >= 500 && attempt < 4) {
      await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
      return call(url, init, attempt + 1);
    }

    throw Object.assign(new Error(body.error?.message ?? "request failed"), {
      code: body.error?.code,
      type: body.error?.type,
      requestId: body.request_id,
    });
  }
  ```
</CodeGroup>

## Common codes

### `unauthenticated`

**401.** No `Authorization` header, or the token is not a recognised `wai_`
key or JWT. Check you are sending `Authorization: Bearer wai_...` and that the
key has not been revoked in
[Settings → API Keys](https://waterr.ai/settings?tab=api-keys).

### `permission_denied`

**403.** The credential is valid but not allowed here. The most common cause
is an OAuth access token (`wai_oat_`) attempting a write — those are read-only
outside the MCP tool surface. Use a `wai_` API key for writes.

### `not_found`

**404.** No resource with that id is visible to your key. Note that a resource
belonging to another workspace also returns `not_found`, not `permission_denied`
— we don't confirm the existence of records you can't see.

### `invalid_request`

**400.** The body or query string failed validation. When the problem is one
field, `error.param` names it.

### `payload_too_large`

**413.** The request body exceeded the 50 MB limit. Split large transcript or
file payloads across multiple calls.

### `rate_limited`

**429.** Over a published limit. Honour the `Retry-After` header. Full detail
in [Rate limits](/api-reference/rate-limits).

### `internal_error`

**500.** An unhandled failure on our side. Retry with backoff; if it persists,
open a support request with the `request_id`.

## Getting help

Include the `request_id` — from the body or the `X-Request-Id` header — in
every support conversation. It resolves in seconds what a description of the
symptoms resolves in days.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.