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

# Rate limits

How many requests one API key can make per minute, the headers that report the count, what a 429 looks like, and how to back off.

The API limits how many requests one key can make to one endpoint per 60
seconds.
This page gives the numbers, the headers that report where you stand, and the
exact `429` response so you can handle it in code.

## Limit

| Fact             | Value                                                                                                                                                                                  |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Requests allowed | 100                                                                                                                                                                                    |
| Window           | 60 seconds. Each request counts against the limit for 60 seconds after it is made.                                                                                                     |
| Scope            | One API key on one endpoint. Two keys have separate counts, and one key's calls to `/v1/text-to-speech` do not count against its calls to `/v1/speech-to-text/jobs`.                   |
| Block            | Once a key exceeds 100 requests on an endpoint, that endpoint rejects the key for 60 seconds. Rejected requests are not counted. When the block ends the count starts again from zero. |
| Applies to       | All fifteen `/v1` endpoints.                                                                                                                                                           |

`POST /v1/sound-cleaning` is also bounded by its upload, not only by the
request count: audio of 10 minutes or less, 150 MB or less, in MP3, WAV, FLAC
or OGG/Opus. See [Sound Cleaning quickstart](/docs/developers/clean-quickstart).

The count is per key, not per workspace. If several systems share a
workspace, give each its own key so a busy one does not block the others.

## Response headers

An accepted request returns three headers that tell you where the key stands
on that endpoint. A rejected request returns `Retry-After` instead.

| Header                  | When             | Meaning                                         |
| ----------------------- | ---------------- | ----------------------------------------------- |
| `X-RateLimit-Limit`     | Accepted request | The limit, `100`.                               |
| `X-RateLimit-Remaining` | Accepted request | Requests left in the window, `0` at the lowest. |
| `X-RateLimit-Reset`     | Accepted request | Seconds until the window resets.                |
| `Retry-After`           | Rejected request | Seconds until the block ends.                   |

## The 429 response

The rejected request gets HTTP `429` with this body:

```json
{
  "data": null,
  "statusCode": 429,
  "success": false,
  "errorMessage": "ThrottlerException: Too Many Requests"
}
```

A different `429` exists. When a call would take the key past
its credit limit, the body says `Monthly spending limit exhausted` and no
`Retry-After` header is sent. Waiting does not clear it; see
[Credits](/docs/developers/credits). Check `errorMessage` before you retry.

## What to do

- Read `Retry-After`, wait that many seconds, then retry. Do not retry sooner;
  requests made during the block are rejected and the block does not shorten.
- If you retry on other errors as well, use exponential backoff with jitter so
  a burst of failures does not turn into a burst of requests.
- Watch `X-RateLimit-Remaining` and slow down before it reaches `0`.
- Use one key per system. The limit is per key, so separate keys give
  separate windows.

## Stay under the limit while waiting for jobs

Polling is the usual way to hit the limit. Every poll is a request against
the job endpoint, whatever the `jobId`, so let the result come to you where
you can.

**Recommended**

- A webhook on `job.completed` and `job.failed`, which costs no requests, with
  one poll after 15 minutes of silence as a fallback.

**Less effective**

- A loop that polls `/v1/text-to-speech/jobs/{jobId}` every 500 milliseconds
  for every job in flight.

If you must poll, use a fixed interval of at least 3 seconds for Text to
Speech and 5 seconds for Speech to Text. See
[Retry requests safely](/docs/developers/retries-and-idempotency) for the
loop.

> [!NOTE]
> A second limit of the same size, 100 requests per 60 seconds per endpoint,
> is counted per client IP address. It counts only requests that the API key
> check rejects: a missing or invalid key (`401`), a key with no access to
> the service (`403`), or a workspace plan that is inactive or not found
> (`403`). A request with a valid key is never blocked by it. Valid keys are
> limited per key only. A blocked IP gets the `429` body above with
> `Retry-After`, even when it sends a bad key.

## FAQ

### Is the limit per workspace or per key?

Per key, and per endpoint. Two keys in one workspace have separate counts,
and one key's calls to `/v1/text-to-speech` do not count against its calls
to `/v1/speech-to-text/jobs`.

### Do rejected requests extend the block?

No. Rejected requests are not counted, and the block ends when
`Retry-After` says it does. They do not shorten it either.

### Do polls count?

Yes. Every `GET` on a job endpoint is a request against that endpoint's
limit.

### How do I tell this 429 from the spending-limit 429?

By `errorMessage`. This one says `ThrottlerException: Too Many Requests` and
carries `Retry-After`; the spending one says `Monthly spending limit
exhausted` and carries no header.

## Related

- [Authentication](/docs/developers/authentication)
- [Credits](/docs/developers/credits)
- [Developer API overview](/docs/developers/overview)
- [Get TTS job details](/docs/reference/tts-get-job)
- [Get STT job details](/docs/reference/stt-get-job)
- [Clean audio](/docs/reference/clean-create)
