Base URL https://api.echonova.ai

Developers

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"

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.