Theme
Speech-to-Text (STT)
Upload audio for transcription, speaker labels, timing data, history, editing, and export.
Transcribe audio
Upload an audio file for speech-to-text transcription.
Headers
| Header | Required | Value |
|---|---|---|
Authorization | yes | Bearer <VOICELAB_API_KEY> |
Idempotency-Key | yes | A UUID in standard 36-character form |
Content-Type | yes | multipart/form-data with the generated boundary |
Example idempotency key: 550e8400-e29b-41d4-a716-446655440000.
Multipart fields
| Field | Required | Value |
|---|---|---|
audio | yes | One audio file |
language | yes | uz, en, or ru |
include_speakers | no | true or false; default: false |
Upload limits
| Limit | Value |
|---|---|
| Formats | MP3, WAV, M4A/AAC, OGG/Opus, WebM/Opus, or FLAC |
| Synchronous duration | 0.5 to 30 seconds |
| Synchronous file size | 10 MiB maximum |
The API inspects the actual container and codec. A filename or browser MIME type does not prove that a file is valid.
Request examples
All examples send meeting.mp3 with language=en and include_speakers=false. Generate a new UUID for each new request.
bash
curl -fS 'https://api.voicelab.uz/v1/stt' \
-H "Authorization: Bearer $VOICELAB_API_KEY" \
-H 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000' \
-F 'audio=@meeting.mp3;type=audio/mpeg' \
-F 'language=en' \
-F 'include_speakers=false'python
import os
import requests
with open("meeting.mp3", "rb") as audio_file:
response = requests.post(
"https://api.voicelab.uz/v1/stt",
headers={
"Authorization": f"Bearer {os.environ['VOICELAB_API_KEY']}",
"Idempotency-Key": "550e8400-e29b-41d4-a716-446655440000",
},
files={"audio": ("meeting.mp3", audio_file, "audio/mpeg")},
data={"language": "en", "include_speakers": "false"},
timeout=120,
)
response.raise_for_status()
print(response.json()["transcript"])ts
import { readFile } from "node:fs/promises";
const form = new FormData();
const audio = new Uint8Array(await readFile("meeting.mp3"));
form.append("audio", new Blob([audio], { type: "audio/mpeg" }), "meeting.mp3");
form.append("language", "en");
form.append("include_speakers", "false");
const response = await fetch("https://api.voicelab.uz/v1/stt", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VOICELAB_API_KEY}`,
"Idempotency-Key": "550e8400-e29b-41d4-a716-446655440000",
},
body: form,
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result.transcript);java
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public final class Main {
private static void addField(
ByteArrayOutputStream body, String boundary, String name, String value
) throws Exception {
body.write(("--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n"
+ value + "\r\n").getBytes(StandardCharsets.UTF_8));
}
public static void main(String[] args) throws Exception {
String boundary = "VoiceLabBoundary" + System.currentTimeMillis();
ByteArrayOutputStream body = new ByteArrayOutputStream();
body.write(("--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"audio\"; filename=\"meeting.mp3\"\r\n"
+ "Content-Type: audio/mpeg\r\n\r\n").getBytes(StandardCharsets.UTF_8));
body.write(Files.readAllBytes(Path.of("meeting.mp3")));
body.write("\r\n".getBytes(StandardCharsets.UTF_8));
addField(body, boundary, "language", "en");
addField(body, boundary, "include_speakers", "false");
body.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.voicelab.uz/v1/stt"))
.header("Authorization", "Bearer " + System.getenv("VOICELAB_API_KEY"))
.header("Idempotency-Key", "550e8400-e29b-41d4-a716-446655440000")
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray()))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString()
);
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}Response
Short audio returns 200 OK:
json
{
"id": "stt_01J...",
"transcript": "Hello world.",
"language": "en",
"duration_ms": 1420,
"audio_available": true,
"audio_url": "https://storage.example/signed-url...",
"segments": [
{
"ordinal": 1,
"start_ms": 120,
"end_ms": 1320,
"text": "Hello world.",
"words": [
{"ordinal": 1, "start_ms": 120, "end_ms": 520, "text": "Hello"},
{"ordinal": 2, "start_ms": 540, "end_ms": 1320, "text": "world."}
]
}
],
"speakers": [],
"speakers_available": false,
"request_id": "req_01J..."
}audio_url is optional and short-lived. The API returns it only when private source-audio storage is configured.
The API returns word timing when the provider supplies valid alignment. If words is absent, use the segment's start_ms and end_ms. Do not calculate word timing by dividing a segment.
With include_speakers=true, segments can contain labels such as speaker_1, and speakers contains local display metadata. Labels apply only to that transcription and do not identify people across recordings.
Long audio
When the durable worker is enabled, the same POST /v1/stt endpoint accepts audio longer than 30 seconds and files up to 500 MiB. It returns:
http
202 Acceptedjson
{
"id": "stt_01J...",
"status": "queued",
"request_id": "req_01J..."
}Poll GET /v1/stt/transcriptions/{id} until status is completed or the response reports a failure. If long-audio processing is disabled, the API returns 503 long_stt_unavailable.
Retry safely with idempotency
- Use a new UUID for different audio, language, or fields.
- After a timeout, retry the same file and fields with the same UUID. The API replays the existing result or queued job without processing or charging again.
- Reusing a UUID with different audio or fields returns
409 idempotency_key_reused.
json
{
"message": "Use a new Idempotency-Key for a different request.",
"error": {"code": "idempotency_key_reused"},
"request_id": "req_01J..."
}Read transcription history
List transcriptions
List saved transcriptions with cursor pagination.
Requires stt:read. The list limit is 10. Treat next_cursor as opaque and send it unchanged.
bash
curl -fS 'https://api.voicelab.uz/v1/stt/transcriptions?limit=10&cursor=NEXT_CURSOR' \
-H "Authorization: Bearer $VOICELAB_API_KEY"python
import os
import requests
response = requests.get(
"https://api.voicelab.uz/v1/stt/transcriptions",
headers={"Authorization": f"Bearer {os.environ['VOICELAB_API_KEY']}"},
params={"limit": 10, "cursor": "NEXT_CURSOR"},
timeout=30,
)
response.raise_for_status()
print(response.json())ts
const url = new URL("https://api.voicelab.uz/v1/stt/transcriptions");
url.searchParams.set("limit", "10");
url.searchParams.set("cursor", "NEXT_CURSOR");
const response = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.VOICELAB_API_KEY}` },
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());java
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
public final class Main {
public static void main(String[] args) throws Exception {
String cursor = URLEncoder.encode("NEXT_CURSOR", StandardCharsets.UTF_8);
var request = HttpRequest.newBuilder(URI.create(
"https://api.voicelab.uz/v1/stt/transcriptions?limit=10&cursor=" + cursor))
.header("Authorization", "Bearer " + System.getenv("VOICELAB_API_KEY"))
.GET()
.build();
var response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}json
{
"data": [
{
"id": "stt_01J...",
"title": "meeting",
"language": "en",
"duration_ms": 1420,
"audio_available": true,
"created_at": "2026-08-16T12:00:00Z"
}
],
"next_cursor": null,
"request_id": "req_01J..."
}Get a transcription
Read a complete transcription and its metadata.
The response contains the transcript, ordered segments, word timing, local speaker labels, revision, and optional signed source-audio URL.
bash
curl -fS 'https://api.voicelab.uz/v1/stt/transcriptions/stt_01J...' \
-H "Authorization: Bearer $VOICELAB_API_KEY"python
import os
import requests
response = requests.get(
"https://api.voicelab.uz/v1/stt/transcriptions/stt_01J...",
headers={"Authorization": f"Bearer {os.environ['VOICELAB_API_KEY']}"},
timeout=30,
)
response.raise_for_status()
print(response.json())ts
const response = await fetch(
"https://api.voicelab.uz/v1/stt/transcriptions/stt_01J...",
{ headers: { Authorization: `Bearer ${process.env.VOICELAB_API_KEY}` } },
);
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public final class Main {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder(URI.create(
"https://api.voicelab.uz/v1/stt/transcriptions/stt_01J..."))
.header("Authorization", "Bearer " + System.getenv("VOICELAB_API_KEY"))
.GET()
.build();
var response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}Rename, edit, and delete
Rename a transcription
Rename a saved transcription.
Requires stt:write.
json
{"title":"Customer interview"}bash
curl -fS -X PATCH 'https://api.voicelab.uz/v1/stt/transcriptions/stt_01J...' \
-H "Authorization: Bearer $VOICELAB_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"title":"Customer interview"}'python
import os
import requests
response = requests.patch(
"https://api.voicelab.uz/v1/stt/transcriptions/stt_01J...",
headers={"Authorization": f"Bearer {os.environ['VOICELAB_API_KEY']}"},
json={"title": "Customer interview"},
timeout=30,
)
response.raise_for_status()
print(response.json())ts
const response = await fetch(
"https://api.voicelab.uz/v1/stt/transcriptions/stt_01J...",
{
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.VOICELAB_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ title: "Customer interview" }),
},
);
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public final class Main {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder(URI.create(
"https://api.voicelab.uz/v1/stt/transcriptions/stt_01J..."))
.header("Authorization", "Bearer " + System.getenv("VOICELAB_API_KEY"))
.header("Content-Type", "application/json")
.method("PATCH", HttpRequest.BodyPublishers.ofString(
"{\"title\":\"Customer interview\"}"))
.build();
var response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}200 OK:
json
{
"id": "stt_01J...",
"title": "Customer interview",
"updated_at": "2026-08-16T12:05:00Z",
"request_id": "req_01J..."
}Edit a transcription
Atomically replace the editable transcript document.
Send the current revision from the GET response:
json
{
"revision": 1,
"title": "Customer interview",
"segments": [
{
"ordinal": 1,
"start_ms": 120,
"end_ms": 1320,
"text": "Hello world.",
"speaker": "speaker_1"
}
],
"speakers": [
{"id": "speaker_1", "display_name": "Speaker 1"}
]
}bash
curl -fS -X PUT 'https://api.voicelab.uz/v1/stt/transcriptions/stt_01J.../editor' \
-H "Authorization: Bearer $VOICELAB_API_KEY" \
-H 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"revision": 1,
"title": "Customer interview",
"segments": [
{"ordinal": 1, "start_ms": 120, "end_ms": 1320, "text": "Hello world.", "speaker": "speaker_1"}
],
"speakers": [{"id": "speaker_1", "display_name": "Speaker 1"}]
}
JSONpython
import os
import requests
document = {
"revision": 1,
"title": "Customer interview",
"segments": [{
"ordinal": 1,
"start_ms": 120,
"end_ms": 1320,
"text": "Hello world.",
"speaker": "speaker_1",
}],
"speakers": [{"id": "speaker_1", "display_name": "Speaker 1"}],
}
response = requests.put(
"https://api.voicelab.uz/v1/stt/transcriptions/stt_01J.../editor",
headers={"Authorization": f"Bearer {os.environ['VOICELAB_API_KEY']}"},
json=document,
timeout=30,
)
response.raise_for_status()
print(response.json())ts
const document = {
revision: 1,
title: "Customer interview",
segments: [{
ordinal: 1,
start_ms: 120,
end_ms: 1320,
text: "Hello world.",
speaker: "speaker_1",
}],
speakers: [{ id: "speaker_1", display_name: "Speaker 1" }],
};
const response = await fetch(
"https://api.voicelab.uz/v1/stt/transcriptions/stt_01J.../editor",
{
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.VOICELAB_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(document),
},
);
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public final class Main {
public static void main(String[] args) throws Exception {
String document = """
{
"revision": 1,
"title": "Customer interview",
"segments": [
{"ordinal": 1, "start_ms": 120, "end_ms": 1320,
"text": "Hello world.", "speaker": "speaker_1"}
],
"speakers": [{"id": "speaker_1", "display_name": "Speaker 1"}]
}
""";
var request = HttpRequest.newBuilder(URI.create(
"https://api.voicelab.uz/v1/stt/transcriptions/stt_01J.../editor"))
.header("Authorization", "Bearer " + System.getenv("VOICELAB_API_KEY"))
.header("Content-Type", "application/json")
.PUT(HttpRequest.BodyPublishers.ofString(document))
.build();
var response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}A stale revision returns 409 stt_edit_conflict. Reload and merge before retrying. For a historical result without timing segments, send transcript instead of segments and speakers.
Delete a transcription
Delete a transcription and retained source audio.
bash
curl -fS -X DELETE 'https://api.voicelab.uz/v1/stt/transcriptions/stt_01J...' \
-H "Authorization: Bearer $VOICELAB_API_KEY"python
import os
import requests
response = requests.delete(
"https://api.voicelab.uz/v1/stt/transcriptions/stt_01J...",
headers={"Authorization": f"Bearer {os.environ['VOICELAB_API_KEY']}"},
timeout=30,
)
response.raise_for_status()ts
const response = await fetch(
"https://api.voicelab.uz/v1/stt/transcriptions/stt_01J...",
{
method: "DELETE",
headers: { Authorization: `Bearer ${process.env.VOICELAB_API_KEY}` },
},
);
if (!response.ok) throw new Error(await response.text());java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public final class Main {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder(URI.create(
"https://api.voicelab.uz/v1/stt/transcriptions/stt_01J..."))
.header("Authorization", "Bearer " + System.getenv("VOICELAB_API_KEY"))
.DELETE()
.build();
var response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.discarding());
if (response.statusCode() >= 400) {
throw new RuntimeException("HTTP " + response.statusCode());
}
}
}Requires stt:write. A successful deletion returns 204 No Content.
Export TXT, JSON, SRT, or VTT
Export a transcript as TXT, JSON, SRT, or VTT.
The response includes Content-Disposition: attachment.
| Format | Contents |
|---|---|
txt | Plain transcript |
json | Transcript, language, duration, speakers, and segments |
srt | SubRip captions |
vtt | WebVTT captions |
SRT and VTT require stored timing segments. Without them, the API returns 409 stt_timestamps_not_available and does not fabricate timestamps.
bash
curl -fS 'https://api.voicelab.uz/v1/stt/transcriptions/stt_01J.../export?format=vtt' \
-H "Authorization: Bearer $VOICELAB_API_KEY" \
-o meeting.vttpython
import os
from pathlib import Path
import requests
response = requests.get(
"https://api.voicelab.uz/v1/stt/transcriptions/stt_01J.../export",
headers={"Authorization": f"Bearer {os.environ['VOICELAB_API_KEY']}"},
params={"format": "vtt"},
timeout=30,
)
response.raise_for_status()
Path("meeting.vtt").write_bytes(response.content)ts
import { writeFile } from "node:fs/promises";
const response = await fetch(
"https://api.voicelab.uz/v1/stt/transcriptions/stt_01J.../export?format=vtt",
{ headers: { Authorization: `Bearer ${process.env.VOICELAB_API_KEY}` } },
);
if (!response.ok) throw new Error(await response.text());
await writeFile("meeting.vtt", Buffer.from(await response.arrayBuffer()));java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
public final class Main {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder(URI.create(
"https://api.voicelab.uz/v1/stt/transcriptions/stt_01J.../export?format=vtt"))
.header("Authorization", "Bearer " + System.getenv("VOICELAB_API_KEY"))
.GET()
.build();
var response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() >= 400) {
throw new RuntimeException(new String(response.body()));
}
Files.write(Path.of("meeting.vtt"), response.body());
}
}STT errors
| Status | Code | Meaning |
|---|---|---|
400 | invalid_multipart, invalid_idempotency_key, invalid_pagination | Fix fields or cursor |
401 | invalid_api_key | API key is missing, expired, disabled, or invalid |
402 | insufficient_credits | Account does not have enough credits |
403 | insufficient_scope | Key permissions or allowed IP policy blocks the operation |
404 | stt_transcription_not_found | ID is unknown or belongs to another account |
409 | idempotency_key_reused, stt_edit_conflict, stt_timestamps_not_available | Resolve the state conflict |
413 | audio_too_large | Use a smaller file |
415 | unsupported_audio_format | Use a supported audio format |
422 | invalid_audio, unsupported_language, no_speech_detected, validation_error | Correct the request |
429 | rate_limited | Wait for Retry-After |
503 | stt_unavailable, long_stt_unavailable | Retry later |