https://api.echonova.aiDevelopers
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. |
{
"data": {
"items": [
{
"id": "66e5ef3a9c1d2b4f8a7e6c01",
"name": "ae_ahmed",
"displayName": "Ahmed",
"language": "ar",
"gender": "Male"
}
],
"total": 1
},
"success": true,
"statusCode": 200,
"errorMessage": null
}curl https://api.echonova.ai/v1/text-to-speech/voices \
-H "Authorization: Bearer YOUR_API_KEY"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"]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
displayNameandlanguage, keep theidin your configuration, and refetch on404 Voice not found.
Less effective
- Call
GET /v1/text-to-speech/voicesbefore 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
tashkeelapplies to Fusha voices only. On a voice of any other dialect the text is synthesised as written, without diacritics, and the call still succeeds.tashkeeltakes effect onPOST /v1/text-to-speech/synconly.POST /v1/text-to-speechaccepts the field and synthesises the text as written. To get diacritised speech, use the synchronous endpoint.texton the job is the text you sent; the diacritised text is not returned.
Example with every option
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
}'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())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.
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 | 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. |
| 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. The request rate 429 is
on 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.