> Fetch https://askmiles.ai/llms.txt first: it indexes every public Miles capability. This file is the markdown twin of https://askmiles.ai/docs/chat-api/errors.

# Errors

Every status the Chat API returns, with its type, code, cause and fix, in the OpenAI error envelope.

## The envelope

Every error, including a 413 raised before the body is parsed and an error mid-stream, is JSON in the OpenAI shape:

```json
{"error": {"message": "api key revoked or unknown", "type": "invalid_request_error", "code": "invalid_api_key"}}
```

A 401 also carries `WWW-Authenticate: Bearer realm="askmiles", error="invalid_token"`, so an OAuth-aware SDK's retry logic behaves the same whichever bearer type it sent. A 429 carries `Retry-After`. Nothing here is cached: every error response is `no-store`.

The one error that does not wear the envelope is a plain-text 403 from Cloudflare, returned before Miles sees the request when the User-Agent is a stock `Python-urllib`. Use a client library.

## The table

| Status | `type` | `code` or message | Cause | Fix |
|---|---|---|---|---|
| 400 | `invalid_request_error` | `request body must be a JSON object` | The body was not an object | Send `{"model": "miles", "messages": [...]}` |
| 400 | `invalid_request_error` | names the field | A role other than `system`/`user`/`assistant`, an image or audio part, a malformed `messages` entry | Remove the part; only text content is read |
| 401 | `invalid_request_error` | `invalid_api_key` | The key was revoked or never existed, or the OAuth token expired | Mint a new key, or refresh the token |
| 401 | `invalid_request_error` | `invalid_token` | The bearer is not a Miles credential, or lacks `miles:chat` | Send a personal key or a `miles:chat` token |
| 403 | `invalid_request_error` | `connections_paused` | The account paused its AI app connections | The user turns them back on at Settings → Connected AI Apps; nothing to re-mint |
| 413 | `invalid_request_error` | request too large | The body exceeded the size cap | Trim history; only the last 26 messages are read anyway |
| 429 | `rate_limit_exceeded` | `rate limited, retry after Ns` | The per-minute window is full | Wait `Retry-After`; safe to retry |
| 429 | `rate_limit_exceeded` | `concurrency limited, retry after 2s` | Another turn is running on this grant, or the service is at its shared ceiling | Reduce parallelism, then retry |
| 429 | `insufficient_quota` | `usage limit reached` | The month's allowance is spent | Wait for the month, or the account changes plan |
| 500 | `server_error` | `the pipeline failed to answer` | Miles could not produce an answer | Retry once; if it repeats, the outage is on Miles' side |
| 504 | `timeout_error` | `the request took too long to answer` | A non-streaming turn passed 90 seconds | Retry with `stream: true`, which has no turn timeout |

Retrying a 429 or 504 is safe: the endpoint holds no state, so nothing was half-written. A 400, 401 or 403 will return the same answer until the cause changes.

## Mid-stream

If a stream has already opened, an error arrives as one final `data:` event whose body is the envelope above, followed by `data: [DONE]`. The HTTP status of the stream is already 200 by then; read the event.

## Catalog and MCP errors

The keyless catalog API has its own shape, `{"error": "message"}`; see [Catalog errors](https://askmiles.ai/docs/catalog/errors). MCP tool refusals arrive as `isError` results with a structured code; see [Quotas and refusals](https://askmiles.ai/docs/mcp/quotas).
