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

# Changelog

The API version, what counts as a breaking change, and a dated list of every change to the v1 endpoints since their release.

This page is the record of what changed on the Developer API and when. Read
it before you upgrade a client, and check it when a request that used to
work starts to fail.

## Version

There is one API version, `v1`, and it is part of every path:
`/v1/text-to-speech`, `/v1/speech-to-text` and the endpoints under them.
There is no version header. A change that cannot be made inside `v1` under
the policy below ships under a new prefix, with `v1` kept running for a
published period.

## Breaking changes policy

A breaking change is one that makes a request that worked yesterday fail
today, or changes the meaning of a response a client already reads.

| Breaking                                                                                      | Not breaking                                                                   |
| --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| Removing or renaming an endpoint, a request field, a response field or a webhook event.       | Adding an endpoint.                                                            |
| Making an optional request field required.                                                    | Adding an optional request field.                                              |
| Lowering a limit: fewer characters, shorter audio, a smaller file, fewer requests per minute. | Raising a limit.                                                               |
| Changing the type of a field or the HTTP status of a response.                                | Adding a field to a response or to a webhook body.                             |
| Changing the authentication scheme or the key format.                                         | Adding a webhook event type. It is sent only to webhooks that subscribe to it. |
| Changing an existing `errorMessage` string.                                                   | Adding a new `errorMessage` for a new failure case.                            |

Two properties of the API follow from this table. First, unknown fields in a
JSON body or a query string are rejected with `400` and
`property <name> should not exist`, so a client must not send a field before
the entry below that introduces it. The two Speech to Text upload endpoints
read named form fields and ignore any other form field.
Second, a client should read the fields it needs from a response and ignore
the rest, because new fields will appear without notice.

Breaking changes are listed below with their date.

## Changes

### 2026-10-23

Added:

- Both Speech to Text upload endpoints, `POST /v1/speech-to-text` and
  `POST /v1/speech-to-text/sync`, take an optional `identify_speakers` form
  field, `true` or `false`. The default is `false`, so a request that does
  not send it gets the same transcript as before. A value other than `true`,
  `false`, `1` or `0` gets `400` with
  `identify_speakers must be true or false`.
- `identifySpeakers`, `segments` and `speakers` are now returned on the
  synchronous response and on the job returned by get and list. `segments`
  holds `start`, `end`, `text` and `speaker` (`spk0`, `spk1`, … or `null`);
  `speakers` holds `id` and `talkTime`. Jobs created before this date report
  `false`, `[]` and `[]`. The `job.completed` webhook body is unchanged.
- `GET /v1/speech-to-text/jobs/{jobId}/export` downloads a completed job as
  `format=txt`, `srt` or `json`, with speaker labels in `lang=en` or `ar`.
  The body is the file, not the envelope. It adds the messages
  `format must be one of the following values: txt, srt, json`,
  `lang must be one of the following values: en, ar` and
  `Job is not completed`, all `400`. See
  [Speech to Text formats and output](/docs/developers/stt-formats-and-output#download-as-txt-srt-or-json).

### 2026-10-21

Breaking:

- A `jobId` that is not 24 hexadecimal characters now gets `400` with
  `Invalid identifier` on the `GET` and `DELETE` job routes. `DELETE` answered
  `404` for it before, and a 12-character id answered `404`.
- A wrong HTTP method on an existing `/v1` path now gets `405` with
  `Method not allowed` and an `Allow` header. It was `404`.

Changed:

- An unknown `/v1` route now gets `404` with `Route not found`. It was
  `Cannot GET …`.

Added:

- Every error on a `/v1` path carries `docs`, including `413` and the
  `400` for a body that is not valid JSON.
- The request log takes a `requestId` filter, and the Activity **Logs** tab has
  **Find by request ID**. **Audio processed** shows seconds under a minute.

### 2026-10-20

Added:

- Activity and the request log now record every call to `/v1` made with a
  known key, any method and any status. `GET` and `DELETE` calls get rows too.
  `GET` calls charge no credits.
- A JSON body over 100 KB returns `413` with `Request body is larger than 100 KB`.
  A body that is not valid JSON returns `400` with `Request body is not valid JSON`.
  Neither has an Activity row, because the key is not checked yet.
- A body sent to `POST /v1/text-to-speech`, `POST /v1/text-to-speech/sync` or a
  widget ticket with a `Content-Type` other than `application/json` returns
  `415` with `Content-Type must be application/json`.
- The add-endpoint dialog shows a new webhook's signing secret once, with a
  **Copy** button.

Changed:

- The per-IP limit now counts only requests the key check rejects. A valid key
  is never blocked by it and is limited per key only.
- `apiKeyName` in `spending.warning` and `spending.exhausted` is now the key's
  name, not its id.

### 2026-10-15

Added:

- Both Speech to Text upload endpoints, `POST /v1/speech-to-text` and
  `POST /v1/speech-to-text/sync`, take an optional `numbers_as_digits` form
  field, `true` or `false`. The default is `true`: spoken numbers come back as
  digits, as before. Send `false` to keep them as the words that were said.
  English transcripts are never converted either way.
- `numbersAsDigits` is now returned on the synchronous response and on the job
  returned by get and list. Jobs created before this date report `true`.

### 2026-10-11

Added:

- Sound Cleaning: `POST /v1/sound-cleaning`, `GET /v1/sound-cleaning/jobs` and
  `GET /v1/sound-cleaning/jobs/{jobId}`. The upload is synchronous, up to
  10 minutes and 150 MB, in MP3, WAV, FLAC or OGG/Opus, and costs 1,000 credits
  per minute. See [Sound Cleaning quickstart](/docs/developers/clean-quickstart).
- `clean` is a new value of a key's allowed services. A key with no service
  restriction can use Sound Cleaning at once. A key that lists services does
  not, until you **Edit** it in the portal and turn **Sound Cleaning** on;
  until then its Sound Cleaning calls get `403`.

Changed:

- Limits per request: `text` on both Text to Speech endpoints is capped at
  500 characters, and audio on both Speech to Text endpoints and on Sound
  Cleaning is capped at 10 minutes. The duration message on both Speech to
  Text endpoints is now `Audio exceeds the 10-minute limit.`
- Credits changed unit. Text to Speech costs 1 credit per character of
  `text`, spaces and line breaks included; Speech to Text costs 100
  credits per minute of audio, charged per second. See
  [Credits](/docs/developers/credits).
- `creditsConsumed`, `creditsUsed` and `creditLimit` keep their names and
  types; their values are in the new unit, whole credits (no decimals). Every key's **Credit Limit** was multiplied by 3,000 into the new unit, so a limit of 100 now reads 300,000.
- Credits are whole numbers. Each charge rounds to a whole credit (half up, at least 1), so `creditsConsumed` is never a decimal; a 1 second Speech to Text call is 2 credits, not 1.666667. `creditsUsed` is a whole number too.

### 2026-10-06

Added:

- Both Speech to Text upload endpoints, `POST /v1/speech-to-text` and
  `POST /v1/speech-to-text/sync`, take an optional `language` form field:
  `auto`, `ar` or `en`. The default is `auto`.
- `language` is now returned on the synchronous response and on the job
  returned by get and list. Jobs created before this date report `auto`.

Breaking:

- A `language` value that was ignored before, such as `en-US` or `arabic`,
  now gets `400` with `language must be one of: auto, ar, en`. Other extra
  form fields are still ignored.

### 2026-10-01

- Credit amounts in `/v1` responses and in the `spending.warning` and
  `spending.exhausted` webhooks are now decimal numbers with up to 6 decimal
  places. The field names are the same: `creditsConsumed`, `creditsUsed` and
  `creditLimit`. Parse them as floating-point numbers. `percentageUsed` is
  still an integer.
- Credits are charged exactly. A charge is no longer rounded up to a whole
  credit and has no minimum.
- 1 credit is now $0.095 of usage, and every service is priced at its
  provider cost plus 20%. See [Credits](/docs/developers/credits).

### 2026-09-06

Breaking:

- Both Text to Speech endpoints: the request fields `creativity`,
  `exaggeration` and `stability` were removed. A request that still sends
  one gets `400` with `property creativity should not exist`, or the same
  message for the other two.
- Both upload endpoints: an upload over the size cap is now rejected with
  `413` and `File too large` while the body is being received. Before, it
  was rejected with `400` and `Audio file exceeds the 50MB limit.` after the
  whole file had been read.
- Both upload endpoints: the duration rejection message changed. It was
  `Audio exceeds the 60-second limit. Use the async endpoint for longer audio.`
  on the synchronous endpoint and the same text with `1800-second` on the
  asynchronous one. It is now `Audio exceeds the 20-minute limit.` and
  `Audio exceeds the 1-hour limit.`

Limits raised:

- `POST /v1/text-to-speech/sync`: `text` from 500 to 5 000 characters. The
  asynchronous endpoint stays at 5 000, so both endpoints now share one cap.
  Earlier the same day the asynchronous cap was briefly 20 000; it is 5 000.
- `POST /v1/speech-to-text/sync`: audio duration from 1 minute to 20
  minutes.
- `POST /v1/speech-to-text`: audio duration from 30 minutes to 1 hour.
- Both upload endpoints: file size from 50 MB to 150 MB.

Added:

- Both Text to Speech endpoints: the optional fields `cfg` (1 to 5, default
  2), `numSteps` (16, 32, 64 or 128, default 32) and `tashkeel` (boolean,
  default `false`). `speed` (0.5 to 2, default 1) was kept. When a field is
  omitted the default applies, so a request without them behaves as before.
- Webhooks: the `job.completed` and `job.failed` events are now delivered
  when an asynchronous job started with an API key ends. Before this date
  the two events could be subscribed to but were not delivered.

### 2026-06-08

Breaking:

- Both upload endpoints: the duration rejection messages changed from
  `Audio exceeds 30 minute limit (got <seconds>s)` and
  `Sync STT supports up to 60s. Use the async endpoint for longer audio.` to
  `Audio exceeds the 1800-second limit. Use the async endpoint for longer audio.`
  and `Audio exceeds the 60-second limit. Use the async endpoint for longer audio.`
- Both upload endpoints: a request with no file part is rejected with
  `File is required` instead of `Audio file is required`.

### 2026-06-07

Initial release of `v1`:

- Text to Speech: `POST /v1/text-to-speech` with up to 5 000 characters,
  `POST /v1/text-to-speech/sync` with up to 500 characters,
  `GET /v1/text-to-speech/voices`, `GET /v1/text-to-speech/jobs` and
  `GET /v1/text-to-speech/jobs/{jobId}`.
- Speech to Text: `POST /v1/speech-to-text` with up to 30 minutes of audio,
  `POST /v1/speech-to-text/sync` with up to 1 minute, both up to 50 MB,
  `GET /v1/speech-to-text/jobs` and `GET /v1/speech-to-text/jobs/{jobId}`.
- Authentication with `Authorization: Bearer` and keys starting with `ek_`.
- A rate limit of 100 requests per 60 seconds per key on each endpoint.
- Request logs in the portal.
- Webhooks with the `spending.warning` and `spending.exhausted` events.

## FAQ

### How do I pin my client to a version?

Every path starts with `/v1`, and that prefix is the version. There is no
version header to send.

### What happens if I keep sending a removed field?

`400` with `property <name> should not exist`. Remove the field; the
request then behaves as it did before the field existed.

### Will a new response field break my client?

Not if the client reads the fields it needs and ignores the rest. Fields
are added without notice; removing or renaming one is a breaking change and
is listed here.

## Related

- [Developer API overview](/docs/developers/overview)
- [Authentication](/docs/developers/authentication)
- [Rate limits](/docs/developers/rate-limits)
- [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)
- [Errors](/docs/developers/errors)
