https://api.echonova.aiDevelopers
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 page instead.
{
"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.
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.
| 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. |
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. |
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. |
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. |
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. |
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. |
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, 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.
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.
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"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}")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.
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.