# Errors

Every failed request returns a JSON body with a `detail` field and a meaningful HTTP status:

```json
{ "detail": "invalid or expired API key" }
```

Check the status code first — it tells you whether to fix the request, top up, or retry.

| Status | Meaning | Retry? |
|--------|---------|--------|
| `400` | Malformed or incomplete request body | No — fix the request |
| `401` | Missing, invalid, or expired API key | No — fix the key |
| `402` | Out of credits | No — top up at [billing](/billing) |
| `403` | No `Authorization` header on a request that needs one | No — send your key |
| `404` | Unknown endpoint, voice, or job id | No — check the id |
| `409` | That upload was already submitted | No — submit the file again |
| `411` | Upload sent without a `Content-Length` | No — send the file, not a stream |
| `413` | File too large (100 MB upload, 1 GiB by URL) | No — submit it by URL, or split it |
| `422` | Body parsed, but a value failed validation | No — fix the value |
| `429` | Too many requests | Yes — back off and retry |
| `502` / `503` | A model server is restarting or saturated | Yes — retry in a moment |

## 401 — Bad or missing key

```json
{ "detail": "invalid or expired API key" }
{ "detail": "Invalid API key" }
{ "detail": "Authentication credentials not provided" }
```

The key didn't resolve to an account. Check that you're sending the whole key, that it hasn't been revoked, and that the header reached us — some HTTP clients drop `Authorization` when following a redirect.

## 402 — Out of credits

Speech:

```json
{ "detail": "insufficient_credits" }
```

Transcription refuses a submit when the balance is below its $0.01 minimum, and returns the shortfall so you can show it to a user:

```json
{
  "detail": {
    "error": "insufficient_funds",
    "message": "Insufficient funds. Need $0.01, have $0.00",
    "balance_microcents": 400000,
    "required_microcents": 1000000
  }
}
```

Top up at [billing](/billing); the same request then succeeds.

The full charge is made when a transcription starts processing, so a job longer than your balance covers ends `failed`, with the same message in its `error_message`.

To avoid hitting this at all, poll `GET /billing/balance` (see
[Overview](#overview)) and top up when `gated` turns true — the speech 402 above
carries no balance, so it tells you that you have run out but not by how much.

## 403 — No key on the request

```json
{ "detail": "human verification required" }
```

The request arrived with no `Authorization` header at all. Send your API key — see [Overview](#overview).

## 404 — Unknown voice

```json
{ "detail": "render failed: {\"detail\":\"unknown voice: maeve.<old-checkpoint>\"}" }
```

Voice ids are exact and include the serving checkpoint's suffix, which changes when we ship a new model. Don't hard-code one from an old integration — read the current id from `GET /voices` and pass it straight through.

A `404 {"detail": "Not Found"}` with no further text means the path itself doesn't exist: check the service prefix (`/vui`, `/akro`) and the version segment.

## 400 / 422 — Bad request body

```json
{ "detail": "voice and input required" }
```

`400` means a required field was absent; `422` means a field was present but out of range or the wrong type (the body then lists the offending fields). Both are deterministic — retrying without changing the request will fail identically.

## 429 — Rate limited

Back off and retry. Exponential backoff starting around one second is plenty; a burst that trips this will usually clear within a few seconds.

## 502 / 503 — Capacity

```json
{ "detail": "generation unavailable" }
```

A model server is restarting or every worker is busy. These are transient: retry the identical request after a short pause rather than treating it as a bad request.

## WebSocket errors

The speech WebSocket reports failures two ways.

A **render** that fails leaves the socket open and sends an error frame — send another `speak` when you've fixed the input:

```json
{ "type": "error", "code": "insufficient_credits", "message": "insufficient credits" }
```

A **connection** that isn't authenticated is closed immediately with code `1008` (policy violation), before any `speak` is read. The key is checked when the socket opens, so a credential sent inside the first message is too late — put it on the handshake as the `Authorization` header, or on the URL as `?token=`.

## Cold starts

The GPU fleet powers down when idle. The first request after a quiet period is held at the gateway while a machine comes up — expect up to ~30 seconds, then normal latency for subsequent calls. Set your client timeout accordingly; a short timeout will look like a hang or a `502` when the request would have succeeded.
