Skip to content
Console

Speech

Transcription

Copy page

Turn speech into text — POST /v1/stt for short audio, streamed segments for long recordings, POST /v1/transcriptions for large files.

There are two ways to transcribe, both on the stt scope:

POST /v1/stt POST /v1/transcriptions
Answer the text, in the response a generation id, then poll
Input the audio in the request body an audio file in your asset library
Size up to 25 MB up to the library’s audio limit (Limits)
Streaming stream=true: segments as they are transcribed no
Stored nothing the full result, as JSON, in your library

Short audio — a voice note, a spoken command — goes to /v1/stt. Anything long, or anything you want to keep, goes to /v1/transcriptions.

Send the audio as a file part with the options as form fields:

curl
curl https://api.lumavox.ai/v1/stt \
-H "Authorization: Bearer $API_KEY" \
-F file=@meeting.mp3 \
-F language=en

Or send the audio itself as the body, with the options as query parameters:

curl
curl "https://api.lumavox.ai/v1/stt?language=en&timestamps=true" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: audio/mpeg" \
--data-binary @meeting.mp3
Response
{
"text": "Thanks everyone for joining. Let's start with the roadmap.",
"language_code": "eng",
"language_probability": 0.98,
"duration_seconds": 4.2,
"model": "scribe_v2",
"words": null
}

Any audio or video container the provider reads is accepted, and nothing is converted on the way. The content type must be audio/*, video/* or application/octet-stream; anything else is a 415.

The same options work as form fields or query parameters.

Option Default What it does
model scribe_v2 The transcription model. scribe_v2 is the one you can name.
language detected An ISO 639-1 or 639-3 code, such as en or fa. Set it when you know it: detection is good, and a wrong guess costs the whole transcript.
diarize false Label who is speaking. Each word gains a speaker_id.
num_speakers provider decides The most speakers to look for, 1 to 32, when diarize is on.
tag_audio_events false Mark laughter, applause and the like, as entries with type: audio_event.
timestamps false Return each word with its start_seconds and end_seconds.
stream false Answer as newline-delimited JSON — see streaming.

language_code in the answer is not what you sent. It is always ISO 639-3, so en comes back as eng.

Word timings are off by default because the list is far larger than the text — an hour of speech is about ten thousand words. Without them words is null; with timestamps=true it is a list of:

A word
{"text": "Thanks", "start_seconds": 0.12, "end_seconds": 0.46, "type": "word", "speaker_id": null}

start_seconds and end_seconds count from the start of the audio. type is word, the spacing between words, or an audio_event with tag_audio_events; speaker_id is set with diarize.

Charged per minute of audio, on duration_seconds — the provider’s own measurement, which the response shows you so you can check the charge. A request the provider fails is not charged.

With stream=true the answer is application/x-ndjson: one JSON object per line. The audio is cut at pauses, the pieces are transcribed several at a time — the platform’s speech.stt.stream_concurrency (Limits) — and each piece’s text arrives as soon as it is its turn — so a long recording shows its first lines in seconds rather than at the end.

curl
curl "https://api.lumavox.ai/v1/stt?stream=true&language=en" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: audio/wav" \
--data-binary @lecture.wav \
--no-buffer
Response (one object per line)
{"type":"segment","index":0,"duration_seconds":28.4,"reason":"silence"}
{"type":"segment","index":1,"duration_seconds":30.0,"reason":"max"}
{"type":"segment","index":2,"duration_seconds":6.1,"reason":"end"}
{"type":"transcript","index":0,"text":"Good morning. Today we look at…","duration_seconds":28.4,"reason":"silence","language_code":"eng","language_probability":0.99,"words":null}
{"type":"transcript","index":1,"text":"…which is why the second law…","duration_seconds":30.0,"reason":"max","language_code":"eng","language_probability":0.99,"words":null}
{"type":"error","index":2,"code":"internal","message":"That segment could not be transcribed (TimeoutError)."}
{"type":"done","text":"Good morning. Today we look at… …which is why the second law…","segments":3,"failed":1,"duration_seconds":58.4,"language_code":"eng"}

The four kinds of line:

type Fields Meaning
segment index, duration_seconds, reason A piece was cut. reason is silence (cut at a pause), max (cut at 30 seconds, at the quietest moment) or end (the last piece).
transcript index, text, duration_seconds, reason, language_code, language_probability, words One piece’s text. words is null without timestamps=true.
error index, code, message One piece failed; the rest carry on. code is an error code: provider_failed when the provider refused it, internal otherwise.
done text, segments, failed, duration_seconds, language_code The end: the whole transcript joined, how many pieces failed, and how long the transcribed pieces were together.
  • Transcripts arrive in index order, not in the order they finish. A transcript out of order is not a transcript.
  • Every line has every field of its kind, null where there is nothing: failed is 0 when no piece failed.
  • The done line carries the whole text, so a client may ignore every line before it.
  • A piece with no speech in it is dropped, so silence costs nothing.
  • No done line means the stream broke. The status was already 200 by then; treat a stream without done as incomplete.

Streaming needs WAV or raw PCM. Finding a pause means reading samples, and the API never decodes audio. Send:

  • a WAV of 16-bit mono integer PCM, at any rate from 8 to 192 kHz — the rate is read from the header; or
  • raw 16-bit little-endian mono PCM, as audio/pcm, audio/l16 or audio/x-pcm, which is read as 24 kHz.

Anything else with stream=true is a 415 that says what it found. Send it without stream, or convert it first.

For a long recording, or one you want to keep with its word timings, upload it to your library and queue the transcription.

1. Upload the file — ask for an upload URL, PUT the file to it, then confirm:

curl
curl https://api.lumavox.ai/v1/assets/upload-url \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"content_type": "audio/mpeg"}'
# → {"asset_id": "ast_…", "url": "https://…", "expires_at": "2026-10-24T09:45:00.000Z"}
curl -X PUT "<url>" -H "Content-Type: audio/mpeg" --data-binary @interview.mp3
curl https://api.lumavox.ai/v1/assets/uploaded \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"asset_id": "ast_…", "content_type": "audio/mpeg", "name": "interview.mp3"}'

Wait until GET /v1/assets/{asset_id} says ready. Assets has the details.

2. Queue the transcription:

curl
curl https://api.lumavox.ai/v1/transcriptions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"asset_id": "ast_a1B2c3D4e5F6g7H8", "language": "en", "diarize": true, "timestamps": true}'

It answers 202 with a generation (gen_…). The body takes asset_id, folder_id (where the result is filed) and the same model, language, diarize, num_speakers, tag_audio_events and timestamps as /v1/stt, as JSON.

3. Poll GET /v1/transcriptions/{generation_id} every couple of seconds until status is succeeded or failed:

curl
curl https://api.lumavox.ai/v1/transcriptions/gen_a1B2c3D4e5F6g7H8 \
-H "Authorization: Bearer $API_KEY"
Response (abridged)
{
"id": "gen_a1B2c3D4e5F6g7H8",
"status": "succeeded",
"capability": "stt",
"transcript": {
"text": "Thanks for having me. So, where do I start…",
"language_code": "eng",
"language_probability": 0.99,
"duration_seconds": 1834.6,
"word_count": 5120,
"asset_id": "ast_q1W2e3R4t5Y6u7I8",
"content_type": "application/json",
"size_bytes": 912345,
"url": "https://…"
}
}
  • transcript.text is the whole transcript, inline.
  • transcript.url is a short-lived link to the provider’s full response as JSON — word timings and speaker labels included, where you asked for them.
  • That JSON is kept in your library as transcript.asset_id. Fetch it from there later; the url expires.

The recording stays in your library, so transcribing it again with other options costs no second upload.

Charged when accepted, as an estimate: the asset’s measured length, or an estimate from its size. When the job finishes, the charge is corrected to the provider’s own measurement — transcript.duration_seconds is the figure you paid for. A transcription that fails is refunded in full. See Queued jobs and Credits and usage.

The common envelope is described in Errors and limits. The ones specific to transcription:

Status Endpoint When
422 /v1/transcriptions The asset is not audio, or its upload failed; or folder_id is not one of your folders.
400 /v1/stt The multipart body could not be read.
402 both The organisation is out of credits — or, for /v1/transcriptions, the job’s estimate would take its balance past the grace buffer. Nothing is charged.
404 /v1/transcriptions The asset or the generation is unknown or not yours.
409 /v1/transcriptions The asset is still being processed. Poll it until it is ready.
413 /v1/stt The audio is over 25 MB. Use /v1/transcriptions, whose limit is the library’s for an audio upload (limits.uploads.audio_bytes on Limits).
415 /v1/stt Not audio; or, with stream=true, not WAV or raw PCM, or a WAV that is not 16-bit mono.
422 both An option failed validation, or one the endpoint does not know was sent — or, on /v1/stt, no audio was sent, a multipart body had no file part, or a WAV header could not be read. issues names the field.
429 both Over your organisation’s rate limit.
502 /v1/stt The provider failed. Nothing is charged; safe to retry.
503 both Transcription, or the storage it reads from, is not set up on this deployment.