Base URL https://api.echonova.ai

Developers

Speech to Text formats and output

What the upload endpoints accept, which checks run before a job starts, every field the responses carry, speaker identification, the TXT, SRT and JSON downloads, and what durationSeconds means.

This page is the lookup table for the two Speech to Text upload endpoints, POST /v1/speech-to-text/sync and POST /v1/speech-to-text: what a request must satisfy, which audio decodes, every field the responses carry, and what the transcript does and does not include. It also covers speaker identification and GET /v1/speech-to-text/jobs/{jobId}/export, which downloads a finished job as TXT, SRT subtitles or JSON.

Accepted input

Both endpoints take one multipart/form-data part named file. The API runs these checks, in this order, before any credits are charged or any transcription starts:

Check Rule Rejection Status
Size 150 MB or less File too large 413
Part present A part named file exists File is required 400
Content type The part's Content-Type starts with audio/, compared case-insensitively Unsupported file type: <type>. Please upload an audio file. 400
Duration 10 minutes or less, on both endpoints Audio exceeds the 10-minute limit. 400

The size check runs while the body is being received, so an oversized upload is cut off before the other checks run. The duration check reads the file's metadata; when the duration cannot be read, the check passes and the file is accepted.

The <type> in the content-type rejection is whatever your client put on the part. application/octet-stream means the client guessed a generic type; text/plain means the part carried no Content-Type header at all. Set the type on the part yourself:

curl -X POST "https://api.echonova.ai/v1/speech-to-text/sync" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@audio.wav;type=audio/wav"
Container Content-Type to send
WAV audio/wav
MP3 audio/mpeg
OGG audio/ogg

Which audio decodes

The checks above read the type label, not the bytes. Decoding happens after them: the audio is resampled to 16 kHz mono, and the channels of a stereo file are averaged rather than one being kept.

WAV, FLAC, OGG and MP3 decode. Before you rely on M4A, MP4, 3GP, WebM or any other container, send a short test file. A file that cannot be decoded passes every check above and fails afterwards, in one of two ways:

Endpoint What you see Credits
POST /v1/speech-to-text/sync 503 with Transcription is temporarily unavailable. Please try again. The reason is not included. Refunded
POST /v1/speech-to-text The job ends as failed and its error field holds the reason, which starts with Audio could not be decoded. Supported: WAV, FLAC, OGG/Opus, MP3…. A job.failed webhook is sent. Refunded

Audio shorter than 50 ms is rejected with a message that starts with Audio is too short followed by the measured duration. This surfaces the same two ways.

To learn why a file fails, send it to POST /v1/speech-to-text: the job's error field carries the reason, and the synchronous endpoint does not.

Language

Both upload endpoints take an optional language form field. Send auto (default), ar or en. The value is trimmed and read without regard to case, so AR and en are accepted. Any other value is a 400 with the message language must be one of: auto, ar, en. Other extra form fields are ignored.

Value Use for
auto Letting the service decide. That is Arabic today, which also handles mixed Arabic/English and clean English speech.
ar Arabic and mixed Arabic/English speech.
en Speech that is all English. Arabic sent as en comes back garbled.

Speaker identification

Both upload endpoints take an optional identify_speakers form field. Send true to learn who speaks when: the finished job then also carries the transcript as timed segments, each tagged with a speaker, and a speakers list with each speaker's talk time. The default is false. The value is trimmed and read without regard to case; true, false, 1 and 0 are accepted, and an empty value counts as false. Any other value is a 400 with the message identify_speakers must be true or false. Asking for speakers does not change the charge.

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"

Speakers are named spk0, spk1 and so on, numbered in the order they first speak. The same id is used on every segment and in speakers, so you can count, colour or label the speakers yourself. The download turns the ids into Speaker 1, Speaker 2 and so on.

A segment:

Field Type Value
start number Where the piece starts, in seconds from the start of the audio.
end number Where the piece ends, in seconds from the start of the audio.
text string What was said in this piece. Numbers are written as in transcription.
speaker string or null The id of the speaker, such as spk0. null when this piece could not be given a speaker.

Segments are sorted by start. A speaker:

Field Type Value
id string The speaker id the segments use.
talkTime number How long this speaker talks, in seconds.

identifySpeakers on the job records what you asked for, not what came back. Speaker identification may not be available to you yet. Until it is, a job made with identify_speakers=true completes with its full transcript, and segments and speakers are empty. A recording whose speakers could not be told apart comes back the same way. To know whether a job has speaker data, check that segments is not empty, not identifySpeakers. Jobs created before the field existed read false, [] and [].

The job.completed webhook carries the transcription, not the segments or the speakers. Get the job, or download it, to read them.

Response fields

Every response is wrapped in the standard envelope; the fields below are inside data. A successful POST returns HTTP 200.

Synchronous transcription

POST /v1/speech-to-text/sync returns the finished job:

Field Type Value
jobId string The id of the job the call created.
status string completed.
transcription string The transcript.
durationSeconds number or null Length of the audio in seconds, with decimals.
creditsConsumed number Credits charged for the call.
language string The language the transcription was requested with.
numbersAsDigits boolean Whether spoken numbers were written as digits.
identifySpeakers boolean Whether the call was made with identify_speakers=true.
segments array The transcript in timed pieces, each with its speaker. Empty unless the job has speaker data. See Speaker identification.
speakers array The speakers heard, in order of first appearance. Empty unless the job has speaker data.
{
  "jobId": "66e3f2a1c9d4b8a1f0e12345",
  "status": "completed",
  "transcription": "مرحبا بكم في إيكونوڤا",
  "durationSeconds": 15.51,
  "creditsConsumed": 26,
  "language": "ar",
  "numbersAsDigits": true,
  "identifySpeakers": false,
  "segments": [],
  "speakers": []
}

Asynchronous transcription

POST /v1/speech-to-text returns only the job id and its state:

Field Type Value
jobId string The id to poll.
status string processing once the file is accepted.

Job

GET /v1/speech-to-text/jobs/{jobId} returns one job, and GET /v1/speech-to-text/jobs returns items, a list of these, plus total, the number of API jobs in the workspace. Lists are sorted newest first and paged with offset (0 or more, default 0) and limit (1 to 100, default 20).

Field Type Value
id string The job id.
status string pending, processing, completed or failed.
transcription string The transcript. Present only once the job is completed; absent before that and on a failed job.
durationSeconds number or null See below.
creditsConsumed number Credits charged when the job was created. Unchanged by a refund; a failed job keeps the number it was charged.
error string Present only on a failed job. It holds the reason the job failed; a job that stayed processing for more than 10 minutes carries Job timed out — AI server did not respond.
language string The language the transcription was requested with; auto for jobs created before the field existed.
numbersAsDigits boolean Whether spoken numbers were written as digits; true for jobs created before the field existed.
identifySpeakers boolean Whether the job was made with identify_speakers=true; false for jobs created before the field existed.
segments array The transcript in timed pieces, each with its speaker. Empty unless the job has speaker data, and always empty before the job is completed.
speakers array The speakers heard, in order of first appearance. Empty unless the job has speaker data.
createdAt string ISO 8601 timestamp of when the job was created.
{
  "id": "66e3f2a1c9d4b8a1f0e12346",
  "status": "completed",
  "transcription": "السلام عليكم، كيف حالك؟ بخير، الحمد لله.",
  "durationSeconds": 6.4,
  "creditsConsumed": 11,
  "language": "ar",
  "numbersAsDigits": true,
  "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 }
  ],
  "createdAt": "2026-09-15T10:21:04.512Z"
}

Any API key in the workspace with Speech to Text access can list, get and delete its jobs, even jobs made by a key that was later deleted. The job endpoint returns 404 with STT Job not found for a job of another workspace or one made in the portal.

What durationSeconds is

durationSeconds is the length of the audio in seconds. Where the number comes from depends on the endpoint and the job's state:

Where Source of the value
Synchronous response The length measured while transcribing. If none was measured, the duration read from the file's metadata, or the size-based estimate below, rounded to a whole second.
Asynchronous job while pending or processing An estimate read from the file's metadata before the job started. When the metadata carries no duration, the file size in bytes divided by 16 000, rounded up.
Asynchronous job once completed The length measured while transcribing, replacing the estimate.

Credits are charged per second of audio, 100 credits a minute. The charge is computed from the metadata duration, or the size-based estimate, before the job starts, and it is rounded to a whole credit, at least 1. The measured durationSeconds is usually the same length, but creditsConsumed is always the number actually charged; use it, not durationSeconds, when you reconcile spend. The rate is on the Credits page.

Write files with a proper header so the duration can be read. A file with no readable duration is priced from its size, and that charge is not corrected afterwards.

Recommended

  • audio/wav, audio/mpeg or audio/ogg files written by an encoder, with a complete header and a known duration.

Less effective

  • A raw stream captured from a socket and uploaded as audio/wav with no header, priced at one second per 16 000 bytes whatever its real length.

What the transcript includes

The transcript is one string, and the duration is one number. A job made with identify_speakers=true that has speaker data also carries the transcript as timed segments with a speaker on each; see Speaker identification. No response carries word timestamps or confidence scores. Only the language you requested comes back, as language on the synchronous response and on a job. If you need timestamps for a job without speaker data, split the audio on your side before you upload it and keep the offsets.

Download as TXT, SRT or JSON

GET /v1/speech-to-text/jobs/{jobId}/export returns a completed job as a file. Unlike the other endpoints, a success is the file itself, not the { data, ... } envelope. An error is still the usual error object, so check the status before you save the body. The endpoint reference is Download a completed STT job.

Query parameter Required Values
format Yes txt, srt or json.
lang No en (default) or ar, the language of the speaker labels: Speaker 1 or المتحدث 1. It does not translate the transcript.
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
format Content-Type Body
txt text/plain; charset=utf-8 Exactly the job's transcription.
srt application/x-subrip; charset=utf-8 SubRip subtitles, built as described below.
json application/json; charset=utf-8 The transcript, the speakers and the segments, shown below.

Every download carries Content-Disposition: attachment and a file name made from the uploaded file's name, with the audio extension swapped for the format: call.mp3 downloads as call.srt. A name that is not plain ASCII, such as an Arabic one, also comes as an RFC 5987 filename* in UTF-8, next to an ASCII filename for older clients. Browser code that calls the API from another origin can read the header.

Content-Disposition: attachment; filename="transcript.srt"; filename*=UTF-8''%D9%85%D9%83%D8%A7%D9%84%D9%85%D8%A9.srt

Any key in the workspace with Speech to Text access can download its API jobs, as with GET /v1/speech-to-text/jobs/{jobId}. Only a completed job has a file; wait for job.completed or poll the job first.

SRT

The cues are built from the job's segments:

  • Each cue lasts at most 7 seconds and holds at most two lines of at most 42 characters. Characters are counted as a reader counts them, not as bytes, so Arabic gets the same room as English.
  • A longer segment is split at sentence and clause punctuation first (. ! ? ؟ ، , ; ؛ … :), then between words. A word is never split, and the text is never changed except for spaces and line breaks.
  • The cues of one segment share its time in proportion to their length in characters. There are no word timings, so cue times inside a segment are estimates: close enough to read along, not exact to the frame. The cues start and end where the segment does and never overlap.
  • When a segment has a speaker, each of its cues starts with the speaker's label in brackets: [Speaker 1] with lang=en, [المتحدث 1] with lang=ar. Speakers are numbered 1, 2, 3 in the order they first speak. The label counts toward the line length.
  • A job with no segments, because speakers were not asked for or not identified, downloads as one cue from 00:00:00,000 to its durationSeconds, holding the whole transcript.
  • The file is UTF-8 with \n line endings, HH:MM:SS,mmm timestamps and a blank line between cues.

Three segments, the last one 12.7 seconds long, download with lang=en as:

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

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

3
00:00:06,900 --> 00:00:11,425
[Speaker 2] عندي سؤال عن الفاتورة الأخيرة،

4
00:00:11,425 --> 00:00:15,659
[Speaker 2] المبلغ أعلى من الشهر الماضي.

5
00:00:15,659 --> 00:00:19,600
[Speaker 2] هل يمكنك أن تشرح لي السبب؟

JSON

The JSON download keeps the segments as they are on the job, not split into cues, and adds the labels:

Field Type Value
id string The job id.
fileName string The uploaded file's name.
durationSeconds number or null As on the job.
language string or null The language the transcription was requested with.
text string The transcript, the same as transcription.
speakers array id, label and talkTime of each speaker, numbered as in the SRT. Empty when the job has no speaker data.
segments array start, end, speaker, speakerLabel and text of each segment. speaker and speakerLabel are null when unknown.
{
  "id": "66e3f2a1c9d4b8a1f0e12346",
  "fileName": "call.mp3",
  "durationSeconds": 19.6,
  "language": "ar",
  "text": "السلام عليكم، كيف حالك؟ بخير، الحمد لله. عندي سؤال عن الفاتورة الأخيرة، المبلغ أعلى من الشهر الماضي. هل يمكنك أن تشرح لي السبب؟",
  "speakers": [
    {
      "id": "spk0",
      "label": "Speaker 1",
      "talkTime": 3.74
    },
    {
      "id": "spk1",
      "label": "Speaker 2",
      "talkTime": 14.8
    }
  ],
  "segments": [
    {
      "start": 0.38,
      "end": 4.12,
      "speaker": "spk0",
      "speakerLabel": "Speaker 1",
      "text": "السلام عليكم، كيف حالك؟"
    },
    {
      "start": 4.3,
      "end": 6.4,
      "speaker": "spk1",
      "speakerLabel": "Speaker 2",
      "text": "بخير، الحمد لله."
    },
    {
      "start": 6.9,
      "end": 19.6,
      "speaker": "spk1",
      "speakerLabel": "Speaker 2",
      "text": "عندي سؤال عن الفاتورة الأخيرة، المبلغ أعلى من الشهر الماضي. هل يمكنك أن تشرح لي السبب؟"
    }
  ]
}

Download errors

Status errorMessage When
400 format must be one of the following values: txt, srt, json format is missing or holds another value, such as vtt.
400 lang must be one of the following values: en, ar lang holds something other than en or ar.
400 Job is not completed The job is pending, processing or failed.
400 Invalid identifier jobId is not 24 hexadecimal characters.
400 property <name> should not exist The query carries a parameter other than format and lang.
404 STT Job not found The job id is unknown, or the job belongs to another workspace or was made in the portal.

The authentication and rate-limit errors of every endpoint apply too; they are on Errors. Downloading is not charged.

Numbers

Spoken Arabic numbers are written as digits by default: a phone number said as صفر واحد واحد خمسة comes back as 0115, and مية وخمسين جنيه as 150 جنيه. Words that are also ordinary Arabic stay words, so واحد صاحبي ("a friend of mine") is not turned into 1.

To keep the numbers exactly as they were said, send numbers_as_digits=false with the upload. English transcripts are never converted. The job reports the setting it was made with as numbersAsDigits.

FAQ

Why did a bad file give me 503 on one endpoint and a failed job on the other?

Both endpoints refund the charge. The synchronous endpoint reports every transcription failure as 503 without a reason; the asynchronous job carries the reason in error. Use POST /v1/speech-to-text when you need to know why.

Is a rejected upload charged?

No. Every check in the table above runs before any credits are charged.

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.

Can I get timestamps or speaker labels?

Send identify_speakers=true with the upload. A job with speaker data carries segments, each with a start, an end and a speaker, and GET /v1/speech-to-text/jobs/{jobId}/export?format=srt turns them into subtitles with speaker labels. There are no word timestamps.

Why are segments and speakers empty when I sent identify_speakers=true?

Speaker identification may not be available to you yet, or the speakers in the recording could not be told apart. The transcript is complete either way, and the SRT download holds one cue for the whole file.

Can I download WebVTT subtitles?

No. The download formats are txt, srt and json; format=vtt is a 400.

Why is durationSeconds different from what I was charged for?

The charge is computed before the job starts, from the file's metadata or its size. durationSeconds on a completed job is the length measured during transcription. Reconcile spend on creditsConsumed.