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

# Errors

> One error envelope with stable, machine-readable codes.

Every Apollo API error — any surface, any status — renders as the same
envelope:

```json theme={"dark"}
{
  "error": {
    "code": "agents.not_found",
    "message": "Agent 6a344d186d3160971ec62eb6 was not found",
    "request_id": "req_8f14e45f",
    "details": [
      { "field": "agentId", "message": "no agent with this id" }
    ]
  }
}
```

<ResponseField name="error.code" type="string" required>
  The stable, machine-readable code your integration branches on. Codes are
  append-only: new codes may be added, existing ones are never removed or
  repurposed.
</ResponseField>

<ResponseField name="error.message" type="string" required>
  A human-readable explanation for logs and debugging. **Not part of the
  contract** — the wording can change at any time, so never parse it.
</ResponseField>

<ResponseField name="error.request_id" type="string">
  Correlation id for the request. Include it when contacting support.
</ResponseField>

<ResponseField name="error.details" type="array">
  Optional structured sub-errors — for validation failures, one entry per
  offending field (`field`, `message`).
</ResponseField>

## Error codes

Codes come in two levels. Every status has a **generic code**, and some
failures carry a **namespaced code** that is more specific (e.g.
`agents.not_found` rather than `not_found`). Always keep a default branch —
the set is open and grows over time.

| HTTP status | Generic code       | When                                                    |
| ----------- | ------------------ | ------------------------------------------------------- |
| `400`       | `bad_request`      | The request was malformed.                              |
| `401`       | `unauthorized`     | Missing or invalid credentials.                         |
| `403`       | `forbidden`        | Authenticated, but not allowed to act on this resource. |
| `404`       | `not_found`        | The resource doesn't exist.                             |
| `409`       | `conflict`         | The request conflicts with current state.               |
| `422`       | `validation_error` | A field failed validation — see `details`.              |
| `429`       | `rate_limited`     | Too many requests — back off and retry.                 |
| `5xx`       | `internal_error`   | Something went wrong on our side. Retry with backoff.   |

## Handling errors

Branch on `error.code`, fall back on the status class:

<CodeGroup>
  ```javascript Node.js theme={"dark"}
  const response = await fetch(url, options);

  if (!response.ok) {
    const { error } = await response.json();

    switch (error.code) {
      case "unauthorized":
        // refresh the access token and retry
        break;
      case "validation_error":
        error.details?.forEach(({ field, message }) =>
          console.error(`${field}: ${message}`)
        );
        break;
      case "rate_limited":
        // back off and retry
        break;
      default:
        console.error(`${error.code}: ${error.message} (${error.request_id})`);
    }
  }
  ```

  ```python Python theme={"dark"}
  response = requests.post(url, headers=headers, json=body)

  if not response.ok:
      error = response.json()["error"]

      if error["code"] == "unauthorized":
          ...  # refresh the access token and retry
      elif error["code"] == "validation_error":
          for detail in error.get("details") or []:
              print(f"{detail.get('field')}: {detail['message']}")
      elif error["code"] == "rate_limited":
          ...  # back off and retry
      else:
          print(f"{error['code']}: {error['message']} ({error.get('request_id')})")
  ```
</CodeGroup>

## Exceptions to the envelope

Two places deliberately speak a different error dialect:

* **The token endpoint** (`POST /management/v1/auth/token`) follows the OAuth
  2.0 convention: `{ "error": "invalid_grant", "error_description": "..." }`.
  See [Authentication](/api/authentication).
* **Streaming transports** deliver errors in-stream: SSE as an `error` event,
  WebSocket as a `type: "error"` envelope. The connection stays open where
  recovery is possible. See [WebSocket](/api/messaging/websocket).
