Skip to content

Speech-to-Text (STT) ​

Transcribe audio with timestamps and optional speaker labels. Save, edit, and export the result.

October 9, 2026 update: 33 languages ​

VoiceLab STT now uses the new production checkpoint and accepts the 33 explicit language codes below for uploaded recordings and realtime STT. Use language=auto to detect the recording's primary language automatically. Existing endpoints, authentication, billing, and retry contracts are unchanged; there is no new model ID to send.

Supported languages ​

Languagelanguage code
Uzbekuz
Englishen
Russianru
Japaneseja
Chinese (Mandarin)zh
Spanishes
Frenchfr
Germande
Portuguesept
Italianit
Koreanko
Kazakhkk
Arabicar
Turkishtr
Tajiktg
Filipinotl
Malayms
Armenianhy
Hindihi
Georgianka
Nepaline
Hungarianhu
Persianfa
Finnishfi
Polishpl
Latvianlv
Danishda
Belarusianbe
Greekel
Ukrainianuk
Vietnamesevi
Thaith
Azerbaijaniaz

Send the code, not the country or language name: for Azerbaijani, use language=az, not Azerbaijan. The language field is still required: send auto for automatic detection or an explicit code for a known language. STT and TTS have separate catalogs: use ar, tl, and ne here, not TTS's arb, fil, and npi. Kyrgyz (ky) is not available in this STT checkpoint.

Availability follows the checkpoint's language tokens, not a recognition-quality certification. No new per-language recognition benchmark was required for this catalog expansion. Accuracy and word timing vary by language and recording; review important transcripts, especially noisy or mixed-language audio.

The public STT playground lists all 33 languages. Its prerecorded comparison samples are available only for Uzbek, English, and Russian. Other selections show a no-sample notice and a link to Studio for transcribing your own recording; they are not live demo results.

Automatic language detection ​

In Studio, choose Auto under Primary language. New users start with Auto; existing saved language choices are preserved. On the developer API, send -F 'language=auto' instead of a specific code. This works for uploaded STT jobs and "language":"auto" in a realtime STT start message.

Completed responses, history, and exports contain the detected code (for example "language":"zh"), never auto. Queued or failed jobs may retain the requested auto mode. Poll the original job and reuse its original request fields and idempotency key on retry; do not replace auto with the detected code.

Auto detects a primary language; it does not promise per-word language detection or reliable detection from silence, very short phrases, or heavily mixed-language audio. For a known language, an explicit code is still useful. Unsupported or missing detection results fail instead of silently choosing Uzbek. Failed jobs release their transcription credit reservation. Timestamp and speaker-label options work the same as with an explicit language.

Check credits before upload ​

GEThttps://api.voicelab.uz/v1/stt/preflight

Estimate required credits before sending audio. Requires stt:write.

bash
curl --fail-with-body 'https://api.voicelab.uz/v1/stt/preflight?duration_ms=60000' \
  -H "Authorization: Bearer $VOICELAB_API_KEY"

duration_ms is an integer estimate of the recording length, at least 500 ms and within the supported audio duration. Omission or 0 checks only the minimum 0.5-second cost. Optional idempotency_key can identify an existing upload on the same account; a recognized UUID returns estimated_credits: 0, so a retry can recover its existing result even when no credits remain.

200 OK returns can_upload: true, integer estimated_credits, and request_id. Insufficient balance returns 402 insufficient_credits; invalid duration returns 422 invalid_audio_duration. This check reserves no credits and does not guarantee that a later upload will be accepted. Processing uses the actual decoded duration. Keep the same UUID on retries of the original upload.

Transcribe audio ​

POSThttps://api.voicelab.uz/v1/stt

Upload an audio file for speech-to-text transcription.

Headers ​

HeaderRequiredValue
AuthorizationyesBearer <VOICELAB_API_KEY>
Idempotency-KeyyesA UUID in standard 36-character form
Content-Typeyesmultipart/form-data with the generated boundary
X-Audio-Duration-MsnoEstimated duration in milliseconds for the pre-upload credit check; actual decoded duration determines billing

Example idempotency key: 550e8400-e29b-41d4-a716-446655440000.

Multipart fields ​

FieldRequiredValue
audioyesOne audio file
languageyesauto to detect the primary language, or one of the 33 supported language codes
include_speakersnotrue or false; default: false

Upload limits ​

LimitValue
FormatsMP3, WAV, M4A/AAC, OGG/Opus, WebM/Opus, or FLAC
Minimum audio duration0.5 seconds
Upload size500 MiB maximum
Submission202 Accepted for all newly accepted files, including short clips

The API validates the audio container and codec, not just the filename or MIME type.

Request examples ​

All examples send meeting.mp3 with language=en and include_speakers=false. Submission requires stt:write; polling requires stt:read on the same account. Save a new UUID per upload and reuse it for retries. Python and TypeScript examples poll for the transcript. cURL and Java print a job ID; use it with Get a transcription.

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 time
import uuid
import requests

base = "https://api.voicelab.uz"
headers = {"Authorization": f"Bearer {os.environ['VOICELAB_API_KEY']}"}
request_key = str(uuid.uuid4())  # Save for retries of this exact file/options.
with open("meeting.mp3", "rb") as audio_file:
    response = requests.post(
        "https://api.voicelab.uz/v1/stt",
        headers={
            **headers,
            "Idempotency-Key": request_key,
        },
        files={"audio": ("meeting.mp3", audio_file, "audio/mpeg")},
        data={"language": "en", "include_speakers": "false"},
        timeout=120,
    )

response.raise_for_status()
job_id = response.json()["id"]  # Persist this ID to resume polling later.
deadline = time.monotonic() + 900
while time.monotonic() < deadline:
    poll = requests.get(f"{base}/v1/stt/transcriptions/{job_id}", headers=headers, timeout=30)
    poll.raise_for_status()
    result = poll.json()
    if result["status"] == "completed":
        print(result["transcript"])
        break
    if result["status"] == "failed":
        raise RuntimeError(result.get("error", "Transcription failed"))
    time.sleep(max(1, min(10, int(poll.headers.get("Retry-After", "1")))))
else:
    raise TimeoutError(f"Still pending: {job_id}; resume polling this ID, do not resubmit.")
ts
import { readFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";

const base = "https://api.voicelab.uz";
const headers = { Authorization: `Bearer ${process.env.VOICELAB_API_KEY}` };
const requestKey = randomUUID(); // Save for retries of this exact file/options.
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: {
    ...headers,
    "Idempotency-Key": requestKey,
  },
  body: form,
  signal: AbortSignal.timeout(120_000),
});

if (!response.ok) throw new Error(await response.text());
const { id } = await response.json(); // Persist to resume polling later.
const deadline = Date.now() + 900_000;
let completed = false;
while (Date.now() < deadline) {
  const poll = await fetch(`${base}/v1/stt/transcriptions/${encodeURIComponent(id)}`, {
    headers, signal: AbortSignal.timeout(30_000),
  });
  if (!poll.ok) throw new Error(await poll.text());
  const result = await poll.json();
  if (result.status === "completed") {
    console.log(result.transcript);
    completed = true;
    break;
  }
  if (result.status === "failed") throw new Error(JSON.stringify(result.error ?? result));
  const retryAfter = Number(poll.headers.get("Retry-After") ?? "1");
  const seconds = Number.isFinite(retryAfter) ? Math.max(1, Math.min(10, retryAfter)) : 1;
  await new Promise(resolve => setTimeout(resolve, seconds * 1000));
}
if (!completed) throw new Error(`Still pending: ${id}; resume polling this ID, do not resubmit.`);
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;
import java.time.Duration;
import java.util.UUID;

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", UUID.randomUUID().toString()) // Persist for retries.
        .header("Content-Type", "multipart/form-data; boundary=" + boundary)
        .timeout(Duration.ofSeconds(120))
        .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()); // 202 acknowledgement: poll its id, not transcript.
  }
}

Submission response ​

All newly accepted recordings return 202 Accepted with a job ID:

json
{"id":"stt_01J...","status":"queued","request_id":"req_01J..."}

Poll GET /v1/stt/transcriptions/{id} using the same account's stt:read key. Respect Retry-After when present; otherwise poll about once per second with a bounded client deadline. A polling 200 OK can still mean queued or processing. Only read transcript after status=completed; handle status=failed as a job failure, even when the polling HTTP status is 200.

bash
curl -fS "https://api.voicelab.uz/v1/stt/transcriptions/$STT_JOB_ID" \
  -H "Authorization: Bearer $VOICELAB_API_KEY"

Persist id before polling. If your client deadline expires, resume polling that same ID later instead of submitting duplicate work. Processing time depends on duration, diarization and queue/provider load, not only file size.

Completed polling response ​

json
{
  "id": "stt_01J...",
  "status": "completed",
  "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.

The new checkpoint preserves segment and word timing across supported scripts, including Chinese text without spaces. When word timing is incomplete or invalid, the original caption is retained instead of inventing word boundaries. Speaker assignment uses overlap and bounded boundary recovery; ambiguous speech can still be unassigned. Do not treat a missing speaker label as a new person or promise perfect speaker recognition.

Retry safely with idempotency ​

  • Use a new UUID for different audio, language, or fields.
  • After an upload timeout, retry the same file and fields with the same UUID. The API identifies the existing job without processing or charging again. Even a completed POST replay is an acknowledgement; GET the result by ID.
  • 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 ​

GEThttps://api.voicelab.uz/v1/stt/transcriptions?limit=10&cursor=<opaque>

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 ​

GEThttps://api.voicelab.uz/v1/stt/transcriptions/{transcription_id}

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 ​

PATCHhttps://api.voicelab.uz/v1/stt/transcriptions/{transcription_id}

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 ​

PUThttps://api.voicelab.uz/v1/stt/transcriptions/{transcription_id}/editor

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"}]
}
JSON
python
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 ​

DELETEhttps://api.voicelab.uz/v1/stt/transcriptions/{transcription_id}

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 ​

GEThttps://api.voicelab.uz/v1/stt/transcriptions/{transcription_id}/export?format=json

Export a transcript as TXT, JSON, SRT, or VTT.

The response includes Content-Disposition: attachment.

FormatContents
txtPlain transcript
jsonTranscript, language, duration, speakers, and segments
srtSubRip captions
vttWebVTT captions

SRT and VTT require timing segments. Without them, the API returns 409 stt_timestamps_not_available.

bash
curl -fS 'https://api.voicelab.uz/v1/stt/transcriptions/stt_01J.../export?format=vtt' \
  -H "Authorization: Bearer $VOICELAB_API_KEY" \
  -o meeting.vtt
python
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 ​

StatusCodeMeaning
400invalid_multipart, invalid_idempotency_key, invalid_paginationFix fields or cursor
401invalid_api_keyAPI key is missing, expired, disabled, or invalid
402insufficient_creditsAccount does not have enough credits
403insufficient_scopeKey permissions or allowed IP policy blocks the operation
404stt_transcription_not_foundID is unknown or belongs to another account
409idempotency_key_reused, stt_edit_conflict, stt_timestamps_not_availableResolve the state conflict
413audio_too_largeUse a smaller file
415unsupported_audio_formatUse a supported audio format
422invalid_audio, unsupported_language, no_speech_detected, validation_errorCorrect the request
429rate_limitedWait for Retry-After
503stt_unavailableRetry later with the same idempotency key