> For the full page index, fetch https://echonova.ai/llms.txt

# Errors

Every HTTP status and errorMessage the v1 API returns, when each one happens, what to do about it, and how asynchronous jobs report failure.

This page lists every error the `/v1` endpoints return: the HTTP status,
the exact `errorMessage`, the condition that raises it and the fix. It is for
the engineer writing the error branch of a client. Failures of asynchronous
jobs, which are not HTTP errors, are at the end.

## Error response shape

Every error is one JSON object with the same four fields as a successful
response. `success` is `false`, `data` is `null`, `statusCode` repeats the
HTTP status and `errorMessage` is the string quoted in the tables below.
Every error on a `/v1` path also carries `docs`, a link to this page. A `401`
links the [Authentication](/docs/developers/authentication) page instead.

```json
{
  "data": null,
  "statusCode": 400,
  "success": false,
  "errorMessage": "text must be shorter than or equal to 500 characters",
  "docs": "https://echonova.ai/docs/developers/errors"
}
```

When several validation rules fail on one request, their messages are joined
into one `errorMessage`, separated by a comma and a space. Match on the
status first and on the message second. The message is plain text, not a
code, and a change to one is listed on the
[Changelog](/docs/developers/changelog).

## Authentication

Raised before the endpoint runs. Every `401` carries the same message, so
the body does not say which of its five causes applied. The full check
order is on [Authentication](/docs/developers/authentication).

| Status | `errorMessage`                                                 | When                                                                                                                                             | Fix                                                              |
| ------ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| `401`  | `Unauthorized`                                                 | No `Authorization` header, a scheme other than `Bearer`, or a token that does not start with `ek_`.                                              | Send `Authorization: Bearer ek_...`.                             |
| `401`  | `Unauthorized`                                                 | No key matches the token: it is mistyped or unknown.                                                                                             | Check the key against its first 8 characters in the portal.      |
| `401`  | `Unauthorized`                                                 | The key was deleted.                                                                                                                             | Create a new key.                                                |
| `401`  | `Unauthorized`                                                 | The key is disabled.                                                                                                                             | Enable it under **Developers**, **API Keys**.                    |
| `401`  | `Unauthorized`                                                 | The account that owns the key is closed.                                                                                                         | Send `Authorization: Bearer ek_...` with an enabled key.         |
| `403`  | `Subscription not found`                                       | The workspace that owns the key has no subscription.                                                                                             | Check the workspace's plan in the portal.                        |
| `403`  | `Subscription expired or inactive`                             | The subscription is in a state other than active or trial.                                                                                       | Renew the subscription.                                          |
| `403`  | `This API key does not have access to Text-to-Speech services` | The key is restricted and Text to Speech is not among its services. The check runs on every `/v1/text-to-speech` path, including the voice list. | **Edit** the key and grant Text to Speech, or use another key.   |
| `403`  | `This API key does not have access to Speech-to-Text services` | The key is restricted and Speech to Text is not among its services.                                                                              | **Edit** the key and grant Speech to Text, or use another key.   |
| `403`  | `This API key does not have access to Sound Cleaning services` | The key is restricted and Sound Cleaning is not among its services.                                                                              | **Edit** the key and turn Sound Cleaning on, or use another key. |

## Rate limit, credit limit and balance

Two different errors use status `429`. Read `errorMessage` before you retry:
one clears with time, the other does not.

| Status | `errorMessage`                          | When                                                                                                                                                                                                                                                                                           | Fix                                                                                                       |
| ------ | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `429`  | `ThrottlerException: Too Many Requests` | The key made more than 100 requests to one endpoint in 60 seconds. A `Retry-After` header carries the seconds to wait. A limit of the same size is counted per client IP address, for requests the key check rejects only. It returns this same body, even when the key is missing or invalid. | Wait `Retry-After` seconds, then retry. See [Rate limits](/docs/developers/rate-limits).                  |
| `429`  | `Monthly spending limit exhausted`      | A call would take the key past its **Credit Limit** for this billing cycle. Raised before the call runs on every call that is charged: both Text to Speech endpoints, both Speech to Text endpoints and Sound Cleaning. Nothing ran. No `Retry-After` header.                                  | Raise or clear the limit on the key, or wait for the next cycle. See [Credits](/docs/developers/credits). |
| `402`  | `Insufficient subscription balance`     | The workspace balance cannot cover the charge for this call. Nothing ran and nothing was charged.                                                                                                                                                                                              | Add credits to the workspace.                                                                             |

## Request validation

Raised by the body or query validation before any work runs. Nothing is
charged. Leading and trailing whitespace is trimmed from every string before
these rules are checked, and unknown fields are rejected.

| Status | `errorMessage`                                                                                                                                             | When                                                                                                                                                                          | Fix                                                                                                    |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `400`  | `text must be longer than or equal to 1 characters, text should not be empty`                                                                              | `text` is an empty string or only whitespace.                                                                                                                                 | Send at least one non-whitespace character.                                                            |
| `400`  | `text must be shorter than or equal to 500 characters, text must be longer than or equal to 1 characters, text should not be empty, text must be a string` | `text` is missing or `null`. Every rule on the field fails at once.                                                                                                           | Send `text` as a JSON string.                                                                          |
| `400`  | `text must be shorter than or equal to 500 characters, text must be longer than or equal to 1 characters, text must be a string`                           | `text` is a number, a boolean or an object.                                                                                                                                   | Send `text` as a JSON string.                                                                          |
| `400`  | `text must be shorter than or equal to 500 characters`                                                                                                     | `text` is over the limit on either Text to Speech endpoint.                                                                                                                   | Split the text into several calls.                                                                     |
| `400`  | `voiceId should not be empty, voiceId must be a string`                                                                                                    | `voiceId` is missing or `null`.                                                                                                                                               | Send an `id` from `GET /v1/text-to-speech/voices`.                                                     |
| `400`  | `voiceId should not be empty`                                                                                                                              | `voiceId` is an empty string.                                                                                                                                                 | Send an `id` from the voices call.                                                                     |
| `400`  | `speed must not be less than 0.5` or `speed must not be greater than 2`                                                                                    | `speed` is a number outside the range.                                                                                                                                        | Stay inside the range on [Text to Speech voices and options](/docs/developers/tts-voices-and-options). |
| `400`  | `cfg must not be less than 1` or `cfg must not be greater than 5`                                                                                          | `cfg` is a number outside the range `1` to `5`.                                                                                                                               | Send a `cfg` from `1` to `5`.                                                                          |
| `400`  | `speed must not be greater than 2, speed must not be less than 0.5, speed must be a number conforming to the specified constraints`                        | `speed` is not a JSON number, for example the string `"1"`.                                                                                                                   | Send a number, not a string.                                                                           |
| `400`  | `cfg must not be greater than 5, cfg must not be less than 1, cfg must be a number conforming to the specified constraints`                                | `cfg` is not a JSON number, for example the string `"1"`.                                                                                                                     | Send a number, not a string.                                                                           |
| `400`  | `numSteps must be one of the following values: 16, 32, 48, 64, 128`                                                                                        | `numSteps` is any other value.                                                                                                                                                | Send one of the five values.                                                                           |
| `400`  | `tashkeel must be a boolean value`                                                                                                                         | `tashkeel` is not `true` or `false`.                                                                                                                                          | Send a JSON boolean.                                                                                   |
| `400`  | `property <name> should not exist`                                                                                                                         | The body or query carries a field the endpoint does not define, for example a field from an older version.                                                                    | Remove the field.                                                                                      |
| `400`  | `offset must not be less than 0`                                                                                                                           | `offset` on a jobs list is negative.                                                                                                                                          | Send `0` or more.                                                                                      |
| `400`  | `offset must be an integer number`                                                                                                                         | `offset` is not a whole number.                                                                                                                                               | Send an integer.                                                                                       |
| `400`  | `limit must not be less than 1` or `limit must not be greater than 100`                                                                                    | `limit` on a jobs list is out of range.                                                                                                                                       | Send `1` to `100`.                                                                                     |
| `400`  | `limit must not be greater than 100, limit must not be less than 1, limit must be an integer number`                                                       | `limit` is not a number at all.                                                                                                                                               | Send an integer from `1` to `100`.                                                                     |
| `400`  | `Invalid identifier`                                                                                                                                       | `voiceId` is not a well-formed id, or `jobId` is not 24 hexadecimal characters.                                                                                               | Copy the id exactly as the API returned it.                                                            |
| `400`  | `Request body is not valid JSON`                                                                                                                           | The JSON body cannot be parsed, for example it has a trailing comma. It is rejected before the key is checked, so it has no Activity row.                                     | Send valid JSON.                                                                                       |
| `413`  | `Request body is larger than 100 KB`                                                                                                                       | A JSON body is over 100 KB. It is rejected before the key is checked, so it has no Activity row. File uploads have their own limit, see Uploads.                              | Send a smaller body.                                                                                   |
| `415`  | `Content-Type must be application/json`                                                                                                                    | A body was sent to `POST /v1/text-to-speech`, `POST /v1/text-to-speech/sync` or a widget ticket with another `Content-Type`. The key is checked first, so the call is logged. | Send `Content-Type: application/json`.                                                                 |

## Uploads

Raised on `POST /v1/speech-to-text`, `POST /v1/speech-to-text/sync` and
`POST /v1/sound-cleaning` while the file is received or checked. Nothing is
charged. The `language`, `numbers_as_digits` and `identify_speakers` rows
apply to the two Speech to Text endpoints only.

| Status | `errorMessage`                                                   | When                                                                                                                                                                         | Fix                                                                                                                                                      |
| ------ | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `413`  | `File too large`                                                 | The upload is over 150 MB. The request is cut off before the file is read.                                                                                                   | Re-encode at a lower bitrate or as mono.                                                                                                                 |
| `400`  | `Multipart: Boundary not found`                                  | The `Content-Type` header says `multipart/form-data` but carries no boundary. This happens when a client sets the header by hand instead of letting its form encoder set it. | Remove the hand-written header and let the client set it with the boundary.                                                                              |
| `400`  | `Unexpected file field - <name>`                                 | The file part has a name other than `file`.                                                                                                                                  | Name the part `file`.                                                                                                                                    |
| `400`  | `File is required`                                               | The form has no file part, or the request was not multipart at all, for example a JSON body or a raw file body.                                                              | Send a multipart form with one part named `file`.                                                                                                        |
| `400`  | `Unsupported file type: <type>. Please upload an audio file.`    | The part's `Content-Type` does not start with `audio/`. `<type>` is what your client sent; `text/plain` means it sent none.                                                  | Set the part's type, for example `audio/wav` or `audio/mpeg`. See [Speech to Text formats and output](/docs/developers/stt-formats-and-output).          |
| `400`  | `Audio exceeds the 10-minute limit.`                             | The file's duration is over the limit on a Speech to Text or Sound Cleaning endpoint.                                                                                        | Split the recording into parts of 10 minutes or less.                                                                                                    |
| `400`  | `Could not read the audio length. Try another file.`             | The file is empty or could not be decoded as audio.                                                                                                                          | Check that the file plays, or export it again in a common format.                                                                                        |
| `503`  | `Audio processing is temporarily unavailable. Please try again.` | Too many uploads were being measured at once, or measuring timed out. Nothing was charged.                                                                                   | Retry after a short wait.                                                                                                                                |
| `400`  | `language must be one of: auto, ar, en`                          | The `language` form field holds something other than `auto`, `ar` or `en`, ignoring case and surrounding spaces, for example `en-US` or `arabic`.                            | Send `auto`, `ar` or `en`, or leave the field out. See [Speech to Text formats and output](/docs/developers/stt-formats-and-output#language).            |
| `400`  | `numbers_as_digits must be true or false`                        | The `numbers_as_digits` form field is not `true`, `false`, `1` or `0`.                                                                                                       | Send `true` or `false`, or leave the field out.                                                                                                          |
| `400`  | `identify_speakers must be true or false`                        | The `identify_speakers` form field is not `true`, `false`, `1` or `0`.                                                                                                       | Send `true` or `false`, or leave the field out. See [Speech to Text formats and output](/docs/developers/stt-formats-and-output#speaker-identification). |

## Sound Cleaning

Raised on `POST /v1/sound-cleaning`, in addition to the upload errors above.

| Status | `errorMessage`                                                                 | When                                                                                              | Fix                                                                 |
| ------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `400`  | `Unsupported audio format. Sound cleaning accepts MP3, WAV, FLAC or OGG/Opus.` | The file extension or its `Content-Type` is not one of the four formats, or the two do not match. | Send MP3, WAV, FLAC or OGG/Opus with a matching extension and type. |
| `400`  | `targetLufs must not be greater than -6`                                       | A tuning field is outside its range. The message names the field and the bound.                   | Stay inside the minimum and maximum listed for the field.           |
| `503`  | `Sound cleaning is temporarily unavailable. Please try again.`                 | The cleaner could not be reached. The charge is refunded.                                         | Retry once.                                                         |

## Not found

| Status | `errorMessage`        | When                                                                                                                                 | Fix                                                              |
| ------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| `404`  | `Route not found`     | No endpoint has this path. The message never repeats the path. Not logged in Activity.                                               | Compare the path, with its `/v1` prefix, to the endpoint's page. |
| `405`  | `Method not allowed`  | The path exists, but not for this method. An `Allow` header lists the methods it takes, such as `GET, HEAD`. Not logged in Activity. | Send the request with a method from `Allow`.                     |
| `404`  | `Voice not found`     | No active voice has this `voiceId`. A voice that is withdrawn drops out of the list.                                                 | List the voices again and pick one.                              |
| `404`  | `TTS Job not found`   | The job id is unknown, or the job belongs to another workspace or was made in the portal.                                            | Use a `jobId` from this workspace's API jobs.                    |
| `404`  | `STT Job not found`   | Same, for Speech to Text jobs.                                                                                                       | Use a `jobId` from this workspace's API jobs.                    |
| `404`  | `Clean Job not found` | Same, for Sound Cleaning jobs.                                                                                                       | Use a `jobId` from this workspace's API jobs.                    |

The same three `404` messages come from `GET` and `DELETE` on a job, and
`STT Job not found` also from downloading one. A key
deletes the API jobs of its workspace, whichever key created them, so a job of another workspace answers `404`.

## Deleting jobs

Raised by `DELETE /v1/text-to-speech/jobs/{jobId}`,
`DELETE /v1/speech-to-text/jobs/{jobId}` and
`DELETE /v1/sound-cleaning/jobs/{jobId}`.

| Status | `errorMessage`                                     | When                                                                             | Fix                                              |
| ------ | -------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------ |
| `400`  | `Job is still processing`                          | The job is pending or processing. Only a completed or failed job can be deleted. | Wait until the job has finished, then delete it. |
| `503`  | `The stored file could not be deleted. Try again.` | The job's stored audio could not be removed. The job is kept.                    | Retry the delete after a short wait.             |

## Downloading transcripts

Raised by [`GET /v1/speech-to-text/jobs/{jobId}/export`](/docs/reference/stt-export-job), in addition to the
authentication, rate-limit and service errors, `Invalid identifier`,
`property <name> should not exist` and `STT Job not found` above.
Downloading is not charged. A success is the file itself; an error is the
usual error object.

| Status | `errorMessage`                                               | When                                                                                               | Fix                                                                                                                |
| ------ | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `400`  | `format must be one of the following values: txt, srt, json` | The `format` query parameter is missing or is not `txt`, `srt` or `json`, for example `vtt`.       | Send `format=txt`, `format=srt` or `format=json`.                                                                  |
| `400`  | `lang must be one of the following values: en, ar`           | The `lang` query parameter is something other than `en` or `ar`.                                   | Send `lang=en` or `lang=ar`, or leave it out for `en`.                                                             |
| `400`  | `Job is not completed`                                       | The job is `pending`, `processing` or `failed`. Only a completed job has a transcript to download. | Wait for the `job.completed` webhook, or poll `GET /v1/speech-to-text/jobs/{jobId}` until `status` is `completed`. |

## Widget tickets

Raised by `POST /v1/widgets/{publicId}/tickets`. Minting a ticket is free.
Nothing is charged.

| Status | `errorMessage`                                             | When                                                                                  | Fix                                                                            |
| ------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `400`  | `ttlSeconds must not be less than 60`                      | `ttlSeconds` is outside 60 to 900 or not a whole number. The message names the bound. | Send a whole number of seconds from 60 to 900, or leave the field out for 300. |
| `400`  | `channel must be one of the following values: voice, chat` | `channel` is any other value.                                                         | Send `voice` or `chat`, or leave the field out.                                |
| `400`  | `user.id must be shorter than or equal to 128 characters`  | `user.id` is over 128 characters.                                                     | Send a `user.id` of 128 characters or fewer.                                   |
| `400`  | `user.id must be longer than or equal to 1 characters`     | `user.id` is an empty string.                                                         | Send a non-empty `user.id`, or leave the field out.                            |
| `400`  | `user.id must not contain control characters`              | `user.id` holds a control character such as a newline or tab.                         | Remove control characters from `user.id`.                                      |
| `400`  | `user.name must be shorter than or equal to 80 characters` | `user.name` is over 80 characters.                                                    | Send a `user.name` of 80 characters or fewer.                                  |
| `403`  | `This API key does not have access to Agents`              | The key is restricted and Agents is not among its services.                           | Edit the key and turn Agents on, or use another key.                           |
| `404`  | `widget_not_found`                                         | No widget has this `publicId`, or the widget belongs to another workspace.            | Copy the `publicId` from the widget settings of an agent in this workspace.    |
| `409`  | `widget_not_private`                                       | The widget is in public mode, which needs no ticket.                                  | Switch the widget to private mode in its settings.                             |
| `409`  | `widget_disabled`                                          | The widget is turned off, so a ticket could never start a session.                    | Turn the widget on in its settings.                                            |

## Service errors

Speech generation and transcription run during both synchronous calls and
start during both asynchronous calls. When the work is not accepted, the
charge is refunded and the request fails with one of these.

| Status | `errorMessage`                                                    | When                                                                                                                                                                                                    | Fix                                                                                                                           |
| ------ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `503`  | `Speech generation is temporarily unavailable. Please try again.` | Speech generation was unavailable for the request. On `POST /v1/text-to-speech` the job is listed as `failed` and no webhook is sent.                                                                   | Retry after a short wait.                                                                                                     |
| `503`  | `Transcription is temporarily unavailable. Please try again.`     | Transcription was unavailable for the request, or, on `POST /v1/speech-to-text/sync`, the file could not be decoded. On `POST /v1/speech-to-text` the job is listed as `failed` and no webhook is sent. | Retry once. If the same file fails every time, send it to `POST /v1/speech-to-text`, whose job carries the reason in `error`. |
| `400`  | `Cloned voices work with instant generation only for now.`        | A cloned voice was sent to `POST /v1/text-to-speech`, which does not support it.                                                                                                                        | Use `POST /v1/text-to-speech/sync` for cloned voices.                                                                         |
| `400`  | A free-text reason, at most 300 characters                        | The request was rejected with a reason of its own. Rare.                                                                                                                                                | Read the message and fix the request.                                                                                         |
| `500`  | `Something went wrong`                                            | An error the API did not classify.                                                                                                                                                                      | Check the request. Retry after a short wait; contact support if it persists.                                                  |
| `503`  | `Database unavailable`                                            | A transient outage inside the API.                                                                                                                                                                      | Retry after a short wait.                                                                                                     |
| `504`  | `Database timed out`                                              | A transient timeout inside the API.                                                                                                                                                                     | Retry after a short wait.                                                                                                     |

## Failures on asynchronous jobs

An asynchronous job that fails after it was accepted does not return an
HTTP error. The job's `status` becomes `failed`, its `error` field holds the
reason, the credits are refunded, and in most cases a `job.failed` webhook
is sent. `creditsConsumed` keeps the number that was charged.

| `error` on the job                          | When                                                                                                                                      | `job.failed` webhook |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| A reason in plain text                      | The job failed and a reason was given.                                                                                                    | Sent.                |
| `AI server generation failed`               | A Text to Speech job failed with no reason given.                                                                                         | Sent.                |
| `AI server transcription failed`            | A Speech to Text job failed with no reason given.                                                                                         | Sent.                |
| `Job timed out — AI server did not respond` | The job stayed in `processing` for more than 10 minutes without a result. The status changes to `failed` within 15 minutes of the `POST`. | Not sent.            |
| The `errorMessage` of the `503` above       | The job was not accepted at submission. The `POST` itself returned the `503`.                                                             | Not sent.            |

The timeout sends no webhook, so a client that waits only for
`job.completed` or `job.failed` never hears about it. Poll
`GET /v1/text-to-speech/jobs/{jobId}` or
`GET /v1/speech-to-text/jobs/{jobId}` when no event has arrived 15 minutes
after the `POST`. Event bodies are on
[Webhook events](/docs/developers/webhooks-events).

## Code samples

Read the status, then `errorMessage`. Retry only the rate-limit `429`, after
`Retry-After` seconds, and the `503` and `504` errors. Which retries are
free and which create a second job is on
[Retry requests safely](/docs/developers/retries-and-idempotency).

:::tabs

```bash title="cURL"
curl -s -o response.json -w "%{http_code}" \
  -X POST "https://api.echonova.ai/v1/text-to-speech/sync" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "", "voiceId": "VOICE_ID"}'
# prints 400; response.json holds "text must be longer than or equal to 1 characters, text should not be empty"
```

```python
import time
import requests

response = requests.post(
    "https://api.echonova.ai/v1/text-to-speech/sync",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={"text": "", "voiceId": "VOICE_ID"},
)
body = response.json()
if not body["success"]:
    message = body["errorMessage"]
    if response.status_code == 429 and "Retry-After" in response.headers:
        time.sleep(int(response.headers["Retry-After"]))
        # retry the request
    elif response.status_code in (503, 504):
        time.sleep(5)
        # retry the request
    else:
        raise RuntimeError(f"{response.status_code}: {message}")
```

```javascript
const response = await fetch('https://api.echonova.ai/v1/text-to-speech/sync', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ text: '', voiceId: 'VOICE_ID' }),
});
const body = await response.json();
if (!body.success) {
  const retryAfter = response.headers.get('Retry-After');
  if (response.status === 429 && retryAfter) {
    await new Promise((r) => setTimeout(r, Number(retryAfter) * 1000));
    // retry the request
  } else if (response.status === 503 || response.status === 504) {
    await new Promise((r) => setTimeout(r, 5000));
    // retry the request
  } else {
    throw new Error(`${response.status}: ${body.errorMessage}`);
  }
}
```

:::

## FAQ

### Which errors are safe to retry?

The rate-limit `429`, after `Retry-After` seconds, and every `503` and
`504`. A `400`, `401`, `403`, `404` or `402` returns the same answer until
you change the request, the key or the balance. Which retries create a
second job is on
[Retry requests safely](/docs/developers/retries-and-idempotency).

### Is anything charged when I get an error?

No. Every error listed above is raised before the charge, or the charge is
refunded before the error is returned.

### Why are there two 429 responses?

One is the request rate limit and clears with time; it carries
`Retry-After`. The other is the key's **Credit Limit** and clears only when
you raise the limit or the billing cycle turns; it carries no header. Tell
them apart by `errorMessage`.

### Why does every 401 say the same thing?

`Unauthorized` covers a missing or malformed header, an unknown key, a
deleted key, a disabled key and a closed account. Check the header first,
then the key's **Status** under **Developers**, **API Keys**.

### Can I match on errorMessage in code?

Yes, after matching on the status. The messages are plain text and any
change to one is listed on the [Changelog](/docs/developers/changelog).

## Related

- [Authentication](/docs/developers/authentication)
- [Rate limits](/docs/developers/rate-limits)
- [Credits](/docs/developers/credits)
- [Text to Speech voices and options](/docs/developers/tts-voices-and-options)
- [Speech to Text formats and output](/docs/developers/stt-formats-and-output)
- [Webhook events](/docs/developers/webhooks-events)
- [Retry requests safely](/docs/developers/retries-and-idempotency)
- [Read the request log](/docs/developers/request-logs)
