Base URL https://api.echonova.ai

Developers

Speech to Text quickstart

Transcribe a short audio file in one call, read the transcript and its duration, run a long file as an asynchronous job and poll it, then download it as subtitles with speaker labels.

Speech to Text turns an audio file into a transcript. This tutorial makes one synchronous call for a short file, reads the two fields you will use most, runs a long file as an asynchronous job that you poll until it finishes, and then downloads a job as subtitles that say who is speaking.

What you need

  • An API key. See Get an API key. If the key restricts services, it needs Access on Speech to Text.
  • The base URL. See Base URL. The callout at the top of this page shows the host these samples call.
  • Credits on the workspace. Each call is charged before it runs. See Credits.
  • An audio file of 10 minutes or less and 150 MB or smaller. Both the synchronous and the asynchronous endpoint take the same limits.

Steps

Step 1: Transcribe a short file synchronously

Send the file as the file part of a multipart/form-data request to POST https://api.echonova.ai/v1/speech-to-text/sync. The call waits for the transcript and returns it in the response.

Add the optional language field: auto (default), ar or en.

curl -X POST "https://api.echonova.ai/v1/speech-to-text/sync" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@audio.mp3;type=audio/mpeg" \
  -F "language=ar"

The API checks the Content-Type of the file part, not the file extension, and it must start with audio/. Set it explicitly as the samples do. Without it, cURL sends application/octet-stream, Python requests sends no type at all, which the API reads as text/plain, and a Node Blob with no type is sent as application/octet-stream. All three are rejected with 400. Use audio/wav for WAV, audio/mpeg for MP3 and audio/ogg for OGG.

Name the type on the part, not on the request. The file extension is not read.

Recommended

-F "file=@audio.wav;type=audio/wav"

Less effective

-F "file=@audio.wav" -H "Content-Type: audio/wav"

Step 2: Read the transcript and the duration

A successful call returns HTTP 200 and the standard envelope. The result is in data:

{
  "success": true,
  "statusCode": 200,
  "errorMessage": null,
  "data": {
    "jobId": "66e3f2a1c9d4b8a1f0e12345",
    "status": "completed",
    "transcription": "مرحبا بكم في إيكونوڤا",
    "durationSeconds": 15.51,
    "creditsConsumed": 26,
    "language": "ar"
  }
}
Field Type What it holds
jobId string The id of the job the call created. It is already completed, and you can read it again from GET /v1/speech-to-text/jobs/{jobId}.
status string Always completed on a successful synchronous call.
transcription string The transcript.
durationSeconds number or null The length of the audio as measured, in seconds, with decimals.
creditsConsumed number The credits charged for this call, a whole number.
language string The language the transcription was requested with.
numbersAsDigits boolean Whether spoken numbers came back as digits. It is true unless you sent numbers_as_digits=false.
identifySpeakers boolean Whether you sent identify_speakers=true. See Step 4.
segments array The transcript in timed pieces with a speaker on each. Empty unless the job has speaker data.
speakers array The speakers heard and their talk time. Empty unless the job has speaker data.

The sample leaves out numbersAsDigits, identifySpeakers, segments and speakers, which the response and the job also carry; see Speech to Text formats and output. transcription is the text you store or show. durationSeconds tells you how much audio was processed. Credits are charged per second of audio, 100 credits a minute, so durationSeconds and creditsConsumed move together; the rates are on the Credits page.

Step 3: Transcribe a file asynchronously

When you do not want to hold a connection open, or want a webhook when the job ends, send it to POST https://api.echonova.ai/v1/speech-to-text instead. The request is the same multipart/form-data upload, with the same optional language field; the limit is 10 minutes of audio, as on the synchronous endpoint. The call returns as soon as the file is accepted:

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

Then poll GET https://api.echonova.ai/v1/speech-to-text/jobs/{jobId} until status is completed or failed:

curl -X POST "https://api.echonova.ai/v1/speech-to-text" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@audio.mp3;type=audio/mpeg" \
  -F "language=ar"

# Repeat until data.status is "completed" or "failed"
curl "https://api.echonova.ai/v1/speech-to-text/jobs/JOB_ID" \
  -H "Authorization: Bearer YOUR_API_KEY"

A finished job looks like this:

{
  "success": true,
  "statusCode": 200,
  "errorMessage": null,
  "data": {
    "id": "66e3f2a1c9d4b8a1f0e12346",
    "status": "completed",
    "transcription": "مرحبا بكم في إيكونوڤا",
    "durationSeconds": 312.4,
    "creditsConsumed": 521,
    "language": "ar",
    "createdAt": "2026-09-15T10:21:04.512Z"
  }
}

While the job is processing, transcription is not present and durationSeconds holds an estimate read from the file. When the job completes, transcription appears and durationSeconds is replaced by the measured length. When it fails, error appears with the reason and the credits are refunded. Until they are set, transcription and error are absent from the object, not null.

Instead of polling, register a webhook endpoint in the portal. The job.completed event carries the transcription and durationSeconds, so you do not need a second call. It does not carry segments or speakers; get the job for those. See Configure a webhook and Webhook events.

Step 4: Identify speakers and download subtitles

Add identify_speakers=true to the upload from Step 3. When the job is completed, download it from GET https://api.echonova.ai/v1/speech-to-text/jobs/{jobId}/export with format=srt. The download is the file itself, not the JSON envelope.

curl -X POST "https://api.echonova.ai/v1/speech-to-text" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@call.mp3;type=audio/mpeg" \
  -F "language=ar" \
  -F "identify_speakers=true"

# Once GET /v1/speech-to-text/jobs/JOB_ID shows "completed"
curl "https://api.echonova.ai/v1/speech-to-text/jobs/JOB_ID/export?format=srt&lang=en" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -o call.srt

The completed job lists the speakers and the transcript in timed segments. Speakers are spk0, spk1 and so on, in the order they first speak:

{
  "identifySpeakers": true,
  "segments": [
    { "start": 0.38, "end": 4.12, "text": "السلام عليكم، كيف حالك؟", "speaker": "spk0" },
    { "start": 4.3, "end": 6.4, "text": "بخير، الحمد لله.", "speaker": "spk1" }
  ],
  "speakers": [
    { "id": "spk0", "talkTime": 3.74 },
    { "id": "spk1", "talkTime": 2.1 }
  ]
}

The SRT file labels each cue with its speaker. lang=ar writes [المتحدث 1] instead of [Speaker 1]:

1
00:00:00,380 --> 00:00:04,120
[Speaker 1] السلام عليكم، كيف حالك؟

2
00:00:04,300 --> 00:00:06,400
[Speaker 2] بخير، الحمد لله.

Send format=txt for the transcript alone, or format=json for the transcript, the labelled speakers and the segments in one file. Cue rules, file names and the JSON shape are on Speech to Text formats and output, and every parameter and error is on Download a completed STT job.

Speaker identification may not be available to you yet. Until it is, a job made with identify_speakers=true still completes with its transcript, and segments and speakers are empty. The SRT download then holds one cue for the whole file. Check that segments is not empty before you rely on speaker labels.

What happens next

  • Every call, synchronous or asynchronous, creates a job. List the API jobs of the workspace at GET /v1/speech-to-text/jobs, newest first, 20 per page by default. Any key in the workspace with Speech to Text access sees them, whichever key created them.
  • Credits are charged to the workspace before transcription starts and refunded if the job fails. The key's own usage counter is updated after the call. See Credits.
  • An asynchronous job that stays processing for more than 10 minutes is marked failed with the error Job timed out — AI server did not respond, within 15 minutes of the POST. The credits are refunded. This timeout sends no job.failed webhook, so a client that listens for webhooks should also poll a job that has been silent for 15 minutes.

Limits

Limit Value Applies to
Audio duration 10 minutes Both upload endpoints
File size 150 MB Both upload endpoints
Content-Type of the file part Must start with audio/ Both upload endpoints
limit query parameter 1 to 100, default 20 GET /v1/speech-to-text/jobs
offset query parameter 0 or more, default 0 GET /v1/speech-to-text/jobs
Requests per key See Rate limits Every endpoint
format query parameter txt, srt or json GET /v1/speech-to-text/jobs/{jobId}/export
Subtitle cue At most 7 seconds and two lines of 42 characters SRT download

Troubleshooting

400 Unsupported file type: application/octet-stream. Please upload an audio file.

The file part was sent without an audio/ content type. The type named in the message is what your client sent; text/plain means it sent none. Set the part's type as shown in Step 1.

400 language must be one of: auto, ar, en

The language field held something else. The value is trimmed and read without regard to case, so AR and en are accepted. Send auto, ar or en, or leave the field out.

400 File is required

The request had no part named file. Check the field name and that the request is multipart/form-data.

400 Audio exceeds the 10-minute limit.

The file is longer than 10 minutes. Both upload endpoints give the same message; split longer recordings.

413 File too large

The upload is over 150 MB. The request is cut off before the file is read. Re-encode the audio at a lower bitrate or as mono.

503 Transcription is temporarily unavailable. Please try again.

Transcription was unavailable, or the file could not be decoded; the synchronous endpoint reports both with this one message. The charge is refunded. Retry once. If the same file fails every time, send it to POST /v1/speech-to-text: that job ends as failed with the reason in its error field. The containers that decode are listed on Speech to Text formats and output.

400 identify_speakers must be true or false

The identify_speakers field held something other than true, false, 1 or 0. Send true or false, or leave the field out.

400 Job is not completed

The export was asked for a job that is pending, processing or failed. Wait for job.completed, or poll the job until status is completed, then download it. A failed job has no transcript to download.

404 STT Job not found

The jobId does not exist, or it belongs to another workspace or was made in the portal. A malformed id returns 400 Invalid identifier.

403 This API key does not have access to Speech-to-Text services

The key grants Access to at least one service, and Speech to Text is not among them. Edit the key in the portal or use another key.

402 Insufficient subscription balance

The workspace cannot cover the charge for this file. Nothing ran. Add credits or use a shorter file.

FAQ

Which language is the transcript in?

Send language: auto (default) lets the service decide. That is Arabic today, which also handles mixed Arabic/English and clean English speech. Use ar for Arabic and mixed speech. Use en only for speech that is all English. Arabic sent as en comes back garbled. Any other value is a 400.

Do I get word timestamps or speaker labels?

Speaker labels, yes: send identify_speakers=true and the job carries timed segments with a speaker on each, as in Step 4. Word timestamps, no. See Speech to Text formats and output for the full list of fields and what is not returned.

Can I transcribe a 30-minute recording?

Not in one call. Both endpoints reject anything over 10 minutes before transcription starts, so split the recording and send each part.

Is the synchronous call also a job?

Yes. It creates a job in the completed state and returns its jobId. The job appears in GET /v1/speech-to-text/jobs with the same transcription, durationSeconds and creditsConsumed.

Can I upload the same file twice?

Yes. Each upload is a new job and a new charge, whatever the file.