Base URL https://api.echonova.ai

Developers

Text to Speech quickstart

List the voices, turn one Arabic sentence into audio with the synchronous endpoint, save the file, then run the same request as an asynchronous job and poll it.

In this tutorial you make four calls: list the voices, generate one Arabic sentence with the synchronous endpoint, save the audio file, then run the same sentence as an asynchronous job and poll it until it completes. At the end you have a working request for each path and know what each one returns.

What you need

  • An API key. See Get an API key. The key needs Access to Text to Speech, or no service restriction at all.
  • The base URL. The callout at the top of this page shows the host these samples call. See Base URL.
  • A workspace with credits. Every generation is charged before it runs. See Credits.
  • cURL with jq, Python 3 with requests, or Node 18 or later. The samples use YOUR_API_KEY where the secret goes.

Steps

Step 1: List the voices

Every generation needs a voiceId. This call returns every active voice, newest first, with no paging and no parameters:

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

The response is wrapped in the standard envelope. data.items holds the voices and data.total the count:

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

Pick a voice whose language is ar and keep its id. The rest of this tutorial calls it VOICE_ID. The fields are explained on Text to Speech voices and options.

Step 2: Generate speech synchronously

Send the text and the voice id to POST /v1/text-to-speech/sync. The call blocks until the audio is ready and returns a link to the file in the same response. The sample sentence is 41 characters of plain Arabic:

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"
  }'

The response:

{
  "data": {
    "jobId": "66e5f0c2a1b2c3d4e5f60718",
    "status": "completed",
    "audioUrl": "https://storage.example.com/tts/tts-output.wav?X-Amz-Signature=...",
    "durationSeconds": 3.4,
    "creditsConsumed": 41
  },
  "success": true,
  "statusCode": 200,
  "errorMessage": null
}

speed, cfg, numSteps and tashkeel were left out, so the defaults applied. The sentence has 41 characters, spaces included, so it costs 41 credits. See Credits for the rate.

Step 3: Save the audio

audioUrl is a signed link to a WAV file. Fetch it with a plain GET and no Authorization header:

# AUDIO_URL is data.audioUrl from Step 2
curl -o speech.wav "$AUDIO_URL"

Open speech.wav in any player to hear the sentence.

Treat audioUrl as a download instruction, not as an address to keep.

Recommended

  • Download the file when the response arrives, store it on your side, and serve it from there.

Less effective

  • Paste audioUrl into a page or a message that will be opened later, and find it expired.

The link expires. Download the file when you receive the response. If a link has expired, call GET /v1/text-to-speech/jobs/{jobId} with the jobId from the response; every call returns a fresh link.

Step 4: Run the same request asynchronously and poll

POST /v1/text-to-speech takes the same body but returns at once with a job id. The audio is produced in the background. Send the request:

curl -X POST https://api.echonova.ai/v1/text-to-speech \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "الطقس اليوم جميل والسماء صافية في المدينة",
    "voiceId": "VOICE_ID"
  }'

The response carries the job id and its status, which is processing once the job is accepted:

{
  "data": {
    "jobId": "66e5f0c2a1b2c3d4e5f60718",
    "status": "processing"
  },
  "success": true,
  "statusCode": 200,
  "errorMessage": null
}

Poll GET /v1/text-to-speech/jobs/{jobId} until status is completed or failed. Each poll counts against the rate limit of 100 requests per 60 seconds per key on that endpoint, so wait a few seconds between calls:

# JOB_ID is data.jobId from the request above
while true; do
  STATUS=$(curl -s https://api.echonova.ai/v1/text-to-speech/jobs/$JOB_ID \
    -H "Authorization: Bearer YOUR_API_KEY" | jq -r '.data.status')
  echo "$STATUS"
  if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then break; fi
  sleep 3
done
curl -s https://api.echonova.ai/v1/text-to-speech/jobs/$JOB_ID \
  -H "Authorization: Bearer YOUR_API_KEY"

A completed job looks like this. Note that the job object names its id id, while the response that started it named it jobId:

{
  "data": {
    "id": "66e5f0c2a1b2c3d4e5f60718",
    "status": "completed",
    "text": "الطقس اليوم جميل والسماء صافية في المدينة",
    "voiceId": "66e5ef3a9c1d2b4f8a7e6c01",
    "audioUrl": "https://storage.example.com/tts/tts-output.wav?X-Amz-Signature=...",
    "durationSeconds": 3.4,
    "creditsConsumed": 41,
    "createdAt": "2026-09-15T10:21:04.512Z"
  },
  "success": true,
  "statusCode": 200,
  "errorMessage": null
}

While the job is pending or processing, audioUrl and durationSeconds are not in the object. When it is failed, error holds the reason and the credits are refunded to the workspace. A job that stays processing for longer than 10 minutes is marked failed within 15 minutes of the POST, so a poll loop always ends. Save the audio exactly as in Step 3.

Instead of polling, register a webhook endpoint and receive job.completed or job.failed with the same audioUrl. See Configure a webhook and Webhook events. Synchronous calls never emit these events.

What you get

  • One audio file per call, reachable through a signed audioUrl for a limited time. The file stays attached to the job; GET /v1/text-to-speech/jobs/{jobId} returns a fresh link on every call.
  • A job for both paths. The synchronous call creates it already completed; the asynchronous call moves it through pending, processing and then completed or failed.
  • creditsConsumed on every job: the exact number of credits charged for that text. A failed job is refunded.
  • GET /v1/text-to-speech/jobs lists the API jobs of the workspace, newest first, 20 per page by default and 100 at most. Any key in the workspace with Text to Speech access sees them, whichever key created them, and a key without that access gets 403. GET /v1/text-to-speech/jobs/{jobId} returns 404 for a job of another workspace.

Limits

Limit Value Applies to
text length 1 to 500 characters, after leading and trailing whitespace is trimmed Both endpoints
speed 0.5 to 2, default 1 Both endpoints
cfg 1 to 5, default 2 Both endpoints
numSteps One of 16, 32, 48, 64, 128, default 48 Both endpoints
Requests 100 per 60 seconds per key on each endpoint All calls, including polls
Jobs per page limit 1 to 100, default 20; offset 0 or more, default 0 GET /v1/text-to-speech/jobs

The full option table is on Text to Speech voices and options.

Troubleshooting

400 text must be shorter than or equal to 500 characters

The text field is over the limit. Split the text and make one call per part, or use the asynchronous endpoint for each part.

400 Invalid identifier

voiceId or jobId is not a valid id. Use the id value exactly as the voices call or the job response returned it.

404 Voice not found

No active voice has that id. List the voices again; a voice can be withdrawn.

404 TTS Job not found

The job id is unknown, or the job belongs to another workspace or was made in the portal. Any API key in the workspace with Text to Speech access can read its API jobs.

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. Edit the key in the portal and grant Access to Text to Speech.

402 Insufficient subscription balance

The workspace balance cannot cover the charge for this text. Nothing was generated. See Credits.

429 Monthly spending limit exhausted

The key's own credit limit for this billing cycle would be exceeded. Raise or clear the limit on the key. This is not the request rate limit; that 429 carries a Retry-After header and is explained on Rate limits.

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.

The job ends as failed

error on the job holds the reason, or AI server generation failed when none was given. The credits are refunded. Retry the request; if it fails again with the same text, shorten it or try another voice.

The job ends as failed with Job timed out

The job stayed processing for more than 10 minutes without a result. It is marked failed with error set to Job timed out — AI server did not respond, within 15 minutes of the POST, and the credits are refunded. Submit the request again.

FAQ

Which endpoint should I use in your live app?

The synchronous one when a person is waiting for the audio, and the asynchronous one with a webhook for batches. See Synchronous and asynchronous calls.

It expires, so download the file as soon as you have the link. Every call to GET /v1/text-to-speech/jobs/{jobId} returns a fresh one.

Can I choose the audio format?

No. The file is WAV.

Do I pay for a failed job?

No. The charge is refunded when the job fails, and creditsConsumed keeps the amount that was charged and refunded.

Can I send English text?

Use a voice whose language is en, when the voices call lists one. The voice sets the language; text takes any string up to 500 characters.