https://api.echonova.aiDevelopers
Synchronous and asynchronous calls
Which of the two call styles fits a request, how a job moves through its four statuses, and how to poll for the result.
Text to Speech and Speech to Text each expose the same two call styles. A synchronous call holds the connection open and returns the result in its response. An asynchronous call returns a job id at once and does the work in the background; you read the result from the job endpoint or receive it through a webhook. This page explains what each style does, when to use it, and how a job moves from start to finish.
The two styles
| Synchronous | Asynchronous | |
|---|---|---|
| Text to Speech | POST /v1/text-to-speech/sync |
POST /v1/text-to-speech |
| Speech to Text | POST /v1/speech-to-text/sync |
POST /v1/speech-to-text |
| Returns | jobId and status, with the result: audioUrl or transcription, durationSeconds and creditsConsumed. |
jobId and status only. |
| Connection | Open until the result is ready. | Closed as soon as the job is accepted. |
status in the response |
completed |
processing |
| Where the result is | In the response. | On GET .../jobs/{jobId}, or in a job.completed webhook. |
| Job events | None. Synchronous calls never emit a webhook event. | job.completed or job.failed. |
tashkeel |
Applied. | Accepted, and the text is synthesised as written. Use the synchronous endpoint for diacritics. |
The request body is the same for both Text to Speech endpoints, and the
multipart/form-data upload is the same for both Speech to Text endpoints.
The reference pages list every field:
Generate speech synchronously,
Generate speech from text,
Transcribe audio synchronously and
Transcribe audio file.
What both styles share
- The charge comes first. The API prices the request, charges the
workspace, and only then does the work. If the balance cannot cover it,
the call is rejected with
402andInsufficient subscription balancebefore anything runs. See Credits. - Every call creates a job. A synchronous call creates the job already
completed; an asynchronous call creates itpending. Both appear inGET /v1/text-to-speech/jobsorGET /v1/speech-to-text/jobs, newest first, and both can be read back fromGET .../jobs/{jobId}. Any key in the workspace with access to that product can read the job. - A failure is refunded. When a synchronous call fails, you get an error
response and the charge is refunded. When an asynchronous job fails, its
status becomes
failed,errorholds the reason, and the charge is refunded. - The same envelope. Every response is
{ "success", "statusCode", "errorMessage", "data" }and every successful call, includingPOST, returns HTTP200. See Developer API overview.
Choosing a style
Use the synchronous endpoint when all of these hold:
- You need the result inside the same request, for example to answer a chat message or to play a prompt on a call.
- The input is within the limit: audio of 10 minutes or less for Speech to Text, 500 characters for Text to Speech. Both endpoints of each service share the limit, so input size never forces the choice.
- Your client can keep an HTTP connection open for as long as the work takes. The API sets no timeout of its own on a synchronous call, so set your client timeout with the input size in mind.
Use the asynchronous endpoint in every other case:
- Batches. Submit many jobs, then poll them or let webhooks tell you when each one is done.
- Clients with a short HTTP timeout, or work you do not want to block on.
- Anything where you want a
job.completedorjob.failedevent delivered to your own endpoint instead of holding a connection.
Both styles cost the same credits for the same input. The choice is about how you receive the result, not about price.
Match the style to who is waiting. A person waiting on a reply wants one round trip; a batch wants a free connection and a webhook.
Recommended
- A chat reply:
POST /v1/text-to-speech/sync, result in the response. - Five hundred recordings overnight:
POST /v1/speech-to-textfor each, with a webhook onjob.completedandjob.failed.
Less effective
- A chat reply:
POST /v1/text-to-speech, then a poll loop that holds the user for the same time plus a polling interval. - Five hundred recordings overnight:
POST /v1/speech-to-text/syncin parallel with a 30-minute client timeout on every connection.
Job lifecycle
A job has four statuses: pending, processing, completed and failed.
An asynchronous job passes through them in this order:
POST accepted
|
v
pending --> processing --> completed
|
+---------> failed| Status | What it means | What the job object holds |
|---|---|---|
pending |
The job is charged and created, and is waiting to be accepted for processing. The POST returns only after the job leaves this state, so the only place you can see it is a job list fetched while the POST is still in flight. |
No result fields. |
processing |
The job is being worked on. | Text to Speech: no audioUrl and no durationSeconds. Speech to Text: durationSeconds holds an estimate read from the file, and transcription is missing. |
completed |
The result is ready. | Text to Speech: audioUrl and durationSeconds. Speech to Text: transcription and durationSeconds as measured. |
failed |
The job failed, or it timed out. The credits are refunded. | error with the reason. creditsConsumed still shows the amount that was charged and then refunded. |
Three rules hold for every job:
- A job ends once. A job cannot go from
completedtofailed, or be completed twice. - A job cannot stay
processingforever. A job that has beenprocessingfor longer than 10 minutes is markedfailedwitherrorset toJob timed out — AI server did not respond; the status changes within 15 minutes of thePOST. The credits are refunded. This timeout sends nojob.failedwebhook, so a poll loop, or a fallback poll after 15 minutes of silence, is the way to learn about it. - A rejection at submission is reported twice. If the job is refused
when the
POSTsubmits it, the job is markedfailed, the credits are refunded, and thePOSTitself returns an error, most often503withSpeech generation is temporarily unavailable. Please try again.orTranscription is temporarily unavailable. Please try again.. Thefailedjob stays in your job list.
A synchronous call has a shorter story: the job appears as completed when
the result is ready, and if the call fails no job is created at all. You
get the error response and the refund.
Polling a job
Read the job with GET /v1/text-to-speech/jobs/{jobId} or
GET /v1/speech-to-text/jobs/{jobId}, using the jobId from the POST
response. Stop when status is completed or failed.
curl https://api.echonova.ai/v1/text-to-speech/jobs/JOB_ID \
-H "Authorization: Bearer YOUR_API_KEY"import time
import requests
headers = {"Authorization": "Bearer YOUR_API_KEY"}
while True:
job = requests.get(
f"https://api.echonova.ai/v1/text-to-speech/jobs/{job_id}",
headers=headers,
).json()["data"]
if job["status"] in ("completed", "failed"):
break
time.sleep(3)
print(job["status"], job.get("audioUrl") or job.get("error"))const headers = { Authorization: 'Bearer YOUR_API_KEY' };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let job;
for (;;) {
const response = await fetch(`https://api.echonova.ai/v1/text-to-speech/jobs/${jobId}`, { headers });
job = (await response.json()).data;
if (job.status === 'completed' || job.status === 'failed') break;
await sleep(3000);
}
console.log(job.status, job.audioUrl ?? job.error);The job object names its id id, while the POST that started it returned
jobId. The fields:
| Field | Text to Speech job | Speech to Text job |
|---|---|---|
id |
Always | Always |
status |
Always | Always |
createdAt |
Always | Always |
creditsConsumed |
Always | Always |
text |
Always | Not present |
voiceId |
Always | Not present |
durationSeconds |
When completed |
Always; an estimate until completed |
audioUrl |
When completed |
Not present |
transcription |
Not present | When completed |
error |
When failed |
When failed |
language |
Not present | Always |
numbersAsDigits |
Not present | Always |
audioUrl is a signed link to a WAV file, and it expires. Every GET of
the job returns a fresh link, so poll again rather than storing the URL.
Fetch the file with a plain GET and no Authorization header.
Two things to keep in mind while polling:
- Every poll counts against the rate limit of 100 requests per 60 seconds per key on that endpoint. Wait a few seconds between polls. See Rate limits.
- Give the loop a deadline. Every job reaches
completedorfailedwithin 15 minutes of enteringprocessing.
The full pattern, with backoff on 429 and 5xx, is on
Retry requests safely.
Webhooks instead of polling
When an asynchronous job ends, the API sends job.completed or job.failed
to every enabled webhook in the workspace that subscribes to that event.
job.completed carries the result itself, audioUrl for Text to Speech and
transcription for Speech to Text, so you do not need to call the job
endpoint afterwards. job.failed carries error. Synchronous calls never
emit either event, and neither does the 10-minute timeout. See
Configure a webhook and
Webhook events.
Limits
| Limit | Value | Applies to |
|---|---|---|
text length |
1 to 500 characters | POST /v1/text-to-speech and POST /v1/text-to-speech/sync |
| Audio duration | 10 minutes | POST /v1/speech-to-text/sync and POST /v1/speech-to-text |
| File size | 150 MB | Both Speech to Text upload endpoints |
Time in processing |
10 minutes, then marked failed within 15 minutes of the POST |
Asynchronous jobs |
| Requests | 100 per 60 seconds per key per endpoint | Every endpoint, including polls |
FAQ
Does the asynchronous endpoint accept longer input?
No. Both Text to Speech endpoints take up to 500 characters, and both Speech to Text endpoints take up to 10 minutes of audio.
Can I get diacritics on an asynchronous job?
No. tashkeel takes effect on POST /v1/text-to-speech/sync only. The
asynchronous endpoint accepts the field and synthesises the text as
written.
Does a synchronous call show up in my job list?
Yes, already completed, with the same creditsConsumed, audioUrl or
transcription as the response.
Why did my job end as failed with no webhook?
It timed out after more than 10 minutes in processing, and the timeout
sends no event. Poll the job when 15 minutes have passed without one.
Is the result kept after the job ends?
Yes. GET .../jobs/{jobId} returns the transcript, or a fresh audioUrl,
until you delete the job with DELETE .../jobs/{jobId} or close your account.
Any key in the workspace with access to that product can delete its jobs. A job of another workspace answers 404.