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

# Text to Speech voices and options

The voice list and what each voice field means, how to pick a voice by language, and every request option with its type, range, default and effect.

This page is the lookup table for Text to Speech: what the voices endpoint
returns, how to choose a voice, and what each field of a generation request
does. It applies to both `POST /v1/text-to-speech/sync` and
`POST /v1/text-to-speech`, which accept the same body.

## Voices endpoint

`GET https://api.echonova.ai/v1/text-to-speech/voices`

The call takes no parameters. It returns every voice whose status is active,
newest first, in one response with no paging. `data.total` is the count.

| Field         | Type   | Always present                                  | Meaning                                                                                                  |
| ------------- | ------ | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `id`          | string | Yes                                             | The value to send as `voiceId`.                                                                          |
| `name`        | string | Yes                                             | The voice's identifier, for example `ae_ahmed`. Unique per voice.                                        |
| `displayName` | string | Yes                                             | The human label, for example `Ahmed`. Falls back to `name` when no label is set.                         |
| `language`    | string | Yes                                             | `ar` for Arabic voices, `en` for English voices.                                                         |
| `gender`      | string | Optional in the schema; every voice carries one | The gender label, for example `Male`.                                                                    |
| `previewUrl`  | string | No                                              | Declared in the API schema and absent from the response. Preview a voice in the portal under **Voices**. |

```json
{
  "data": {
    "items": [
      {
        "id": "66e5ef3a9c1d2b4f8a7e6c01",
        "name": "ae_ahmed",
        "displayName": "Ahmed",
        "language": "ar",
        "gender": "Male"
      }
    ],
    "total": 1
  },
  "success": true,
  "statusCode": 200,
  "errorMessage": null
}
```

:::tabs

```bash title="cURL"
curl https://api.echonova.ai/v1/text-to-speech/voices \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```python
import requests

response = requests.get(
    "https://api.echonova.ai/v1/text-to-speech/voices",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
arabic = [v for v in response.json()["data"]["items"] if v["language"] == "ar"]
```

```javascript
const response = await fetch('https://api.echonova.ai/v1/text-to-speech/voices', {
  headers: { Authorization: 'Bearer YOUR_API_KEY' },
});
const { data } = await response.json();
const arabic = data.items.filter((v) => v.language === 'ar');
```

:::

## Choosing a voice

Filter on `language` in your own code; the endpoint has no filters. `ar`
selects the Arabic voices and `en` the English ones.

The voice object does not include its dialect. The dialect is fixed by the
voice: you send `voiceId` and nothing else, and the audio comes out in that
voice's dialect. To see which dialect a voice speaks, open **Voices** in the
portal. Each card shows the voice's display name and its dialect. Match the
card to the API by `displayName`, or by `name` when no display name is set,
and use that voice's `id` in your request.

The dialects are `egyptian`, `emirati`, `saudi`, `kuwaiti`, `qatari`,
`omani`, `lebanese`, `syrian`, `iraqi`, `fusha`, `american`, `british` and
the legacy `english`. Which of them have voices at a given time can change;
the voices call is the source of truth. English voices report `language`
`en` whatever their dialect.

The set of voices is not fixed. A voice can be withdrawn: it drops out of
the list, and a request that names it gets `404 Voice not found`. Cache the
list for convenience, but refresh it when that error appears.

Choose a voice once and keep its `id` in configuration, with a path back to
the list when the id stops working.

**Recommended**

- Fetch the list at start-up, pick by `displayName` and `language`, keep the
  `id` in your configuration, and refetch on `404 Voice not found`.

**Less effective**

- Call `GET /v1/text-to-speech/voices` before every generation and take the
  first Arabic voice, whichever it is that day.

## Request options

Both generation endpoints take a JSON body with these fields. Leading and
trailing whitespace is trimmed from every string before validation. Unknown
fields are rejected.

| Option     | Type    | Required | Range                              | Default | Effect                                                                                                          |
| ---------- | ------- | -------- | ---------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------- |
| `text`     | string  | Yes      | 1 to 500 characters after trimming | none    | The text to speak. Every character is billed, spaces and line breaks included.                                  |
| `voiceId`  | string  | Yes      | An `id` from the voices call       | none    | Selects the voice and, through it, the language and dialect.                                                    |
| `speed`    | number  | No       | 0.5 to 2                           | 1       | Sets the reading pace. Lower is slower, higher is faster.                                                       |
| `cfg`      | number  | No       | 1 to 5                             | 2       | Classifier-free guidance scale. Leave it at the default unless you are tuning against your own listening tests. |
| `numSteps` | number  | No       | Exactly one of 16, 32, 48, 64, 128 | 48      | Diffusion step count. Only these five values are accepted; it is not a range.                                   |
| `tashkeel` | boolean | No       | `true` or `false`                  | `false` | Adds diacritics to the text before synthesis. See the note below.                                               |

An omitted `speed`, `cfg` or `numSteps` means "use the default", and the
job records the value that was used.

### About tashkeel

- `tashkeel` applies to **Fusha** voices only. On a voice of any other
  dialect the text is synthesised as written, without diacritics, and the
  call still succeeds.
- `tashkeel` takes effect on `POST /v1/text-to-speech/sync` only.
  `POST /v1/text-to-speech` accepts the field and synthesises the text as
  written. To get diacritised speech, use the synchronous endpoint.
- `text` on the job is the text you sent; the diacritised text is not
  returned.

### Example with every option

:::tabs

```bash title="cURL"
curl -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",
    "speed": 1,
    "cfg": 2,
    "numSteps": 48,
    "tashkeel": true
  }'
```

```python
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",
        "speed": 1,
        "cfg": 2,
        "numSteps": 48,
        "tashkeel": True,
    },
)
print(response.json())
```

```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',
    speed: 1,
    cfg: 2,
    numSteps: 48,
    tashkeel: true,
  }),
});
console.log(await response.json());
```

:::

## Output

Both endpoints produce one audio file per call and expose it as `audioUrl`:
in the synchronous response directly, and on the job object once the
asynchronous job is `completed`.

| Fact               | Value                                                                                                                                                                                               |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| What `audioUrl` is | A signed link to the audio file. It expires. `GET /v1/text-to-speech/jobs/{jobId}` returns a fresh link on every call.                                                                              |
| File format        | WAV. The file name in the link ends with `.wav`. No request option chooses the format.                                                                                                              |
| `durationSeconds`  | The length of the audio in seconds. The synchronous response carries `0` when the length could not be measured; the job object of an asynchronous job omits the field until the job is `completed`. |
| `creditsConsumed`  | The credits charged for this call.                                                                                                                                                                  |

## Credits

A call is priced from the `text` field before the job starts: every character of `text`, spaces and line breaks included, at 1 credit per character. The
workspace balance is charged before generation and refunded if the job fails.
Rates, worked examples and the credit limit per key are on
[Credits](/docs/developers/credits).

## Limits

| Limit               | Value                                                                                | Applies to                                                   |
| ------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------ |
| `text`              | 1 to 500 characters after trimming                                                   | `POST /v1/text-to-speech/sync` and `POST /v1/text-to-speech` |
| `speed`             | 0.5 to 2                                                                             | Both endpoints                                               |
| `cfg`               | 1 to 5                                                                               | Both endpoints                                               |
| `numSteps`          | 16, 32, 48, 64 or 128                                                                | Both endpoints                                               |
| Voices per response | All active voices, no paging                                                         | `GET /v1/text-to-speech/voices`                              |
| Requests per key    | 100 per 60 seconds on each endpoint; see [Rate limits](/docs/developers/rate-limits) | All calls                                                    |

The two endpoints share one text limit. The difference between them is how
you receive the result, not how much text they take.

## Errors

Every error uses the standard envelope with `success: false` and the message
in `errorMessage`. Validation failures list every failing rule in one
message, separated by commas.

| Status | `errorMessage`                                                                | When                                                                                                                                                                            | What to do                                                             |
| ------ | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| 400    | `text must be longer than or equal to 1 characters, text should not be empty` | `text` is an empty string or only whitespace. When `text` is missing, `text must be shorter than or equal to 500 characters` is prepended and `text must be a string` appended. | Send at least one non-whitespace character.                            |
| 400    | `text must be shorter than or equal to 500 characters`                        | `text` is over the limit.                                                                                                                                                       | Split the text into several calls.                                     |
| 400    | `voiceId should not be empty`                                                 | `voiceId` is an empty string. When it is missing, `voiceId must be a string` is appended.                                                                                       | Send an `id` from the voices call.                                     |
| 400    | `speed must not be less than 0.5`, `speed must not be greater than 2`         | `speed` is out of range. The same pattern applies to `cfg` with 1 and 5.                                                                                                        | Stay inside the range.                                                 |
| 400    | `speed must be a number conforming to the specified constraints`              | `speed` or `cfg` is not a number.                                                                                                                                               | Send a JSON number, not a string.                                      |
| 400    | `numSteps must be one of the following values: 16, 32, 48, 64, 128`           | `numSteps` is not one of the five values.                                                                                                                                       | 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 carries a field the endpoint does not know.                                                                                                                            | Remove it.                                                             |
| 400    | `Invalid identifier`                                                          | `voiceId` is not a well-formed id.                                                                                                                                              | Copy the `id` exactly as the voices call returned it.                  |
| 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.                                  |
| 402    | `Insufficient subscription balance`                                           | The workspace balance cannot cover the charge.                                                                                                                                  | Add credits to the workspace. See [Credits](/docs/developers/credits). |
| 403    | `This API key does not have access to Text-to-Speech services`                | The key restricts services and Text to Speech is not among them.                                                                                                                | Grant **Access** to **Text to Speech** on the key.                     |
| 404    | `Voice not found`                                                             | No active voice has this `voiceId`.                                                                                                                                             | List the voices again and pick an active one.                          |
| 429    | `Monthly spending limit exhausted`                                            | The key's credit limit for this cycle would be exceeded.                                                                                                                        | Raise or clear the limit on the key.                                   |
| 503    | `Speech generation is temporarily unavailable. Please try again.`             | Speech generation was unavailable for the request. The charge is refunded.                                                                                                      | Retry after a short wait.                                              |

Authentication failures, `401` and the other `403`, are listed on
[Authentication](/docs/developers/authentication). The request rate `429` is
on [Rate limits](/docs/developers/rate-limits).

## FAQ

### How do I know which dialect a voice speaks?

Open **Voices** in the portal; each card shows the dialect. The API voice
object carries `language` only.

### Why does tashkeel do nothing on my job?

Either the voice is not **Fusha**, or you used `POST /v1/text-to-speech`.
Diacritics are added on `POST /v1/text-to-speech/sync` with a Fusha voice.

### Can I preview a voice through the API?

No. `previewUrl` is absent from the response. Preview voices in the portal
under **Voices**, or generate a short sentence with
`POST /v1/text-to-speech/sync`.

### What do cfg and numSteps change?

Both tune the synthesis. Leave them at their defaults unless you are
comparing outputs in your own listening tests; the accepted values are in
the table above, and anything outside them is rejected with `400`.

### Can I get MP3 or OGG?

No. The audio is WAV. Convert it on your side if you need another format.

## Related

- [Generate speech synchronously](/docs/reference/tts-generate-sync)
- [Generate speech from text](/docs/reference/tts-generate-async)
- [List available voices](/docs/reference/tts-list-voices)
- [Text to Speech quickstart](/docs/developers/tts-quickstart)
- [Credits](/docs/developers/credits)
- [Errors](/docs/developers/errors)
- [Developer API overview](/docs/developers/overview)
