Transcription
Transcribe audio and video files to text with SRT subtitles using the Kolbo API.
Transcribe audio and video files to text with word-level SRT subtitles. Supports both URL input and direct file upload.
No model selection — transcription always uses the built-in STT engine. There is no model parameter.
All generation endpoints accept an optional
project_idbody field that routes the output into a specific project. See Projects.
Endpoint
POST /api/v1/transcribeAccepts both application/json (for URL-based input) and multipart/form-data (for file uploads).
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
audio_url | string | No* | URL of an audio file to transcribe. One source per request — this endpoint takes a single string, not an array |
video_url | string | No* | URL of a video file (audio is extracted automatically). Treated exactly like audio_url; takes precedence over it when both are sent |
file_url | string | No* | URL of any audio or video file. Lowest precedence — used only when neither video_url nor audio_url is sent, and otherwise handled identically. One difference: if it points at a video stored under a Kolbo /temp/ path, that source object is deleted after the transcription finishes |
file | file | No* | Audio or video file upload (multipart field name file, one file). Max 500 MB |
language | string | No | Output language, e.g. "en", "he", "es". It is not a source-language hint: the engine always auto-detects the spoken language, and when the detected language differs, the transcript text, the .txt file, the SRT and the word-by-word SRT are all translated into this language (works with or without generate_srt; a no-op when it equals the detected language). Accepts ISO 639-1 or 639-3 codes, region-tagged codes such as "en-US" (the base code is used), and a few English names ("hebrew", "english", "arabic", "spanish", "french", "german", "russian", "chinese", "japanese"). Omit — or send "auto", "auto-detect", "detect" or "none" — to keep the detected language. If translation fails, the job still completes in the original language |
diarize | boolean | No | Detect distinct speakers, so the speaker-grouped text in the result labels them Speaker 1, Speaker 2, … (default false) |
tag_audio_events | boolean | No | Tag non-speech events (laughter, applause, music) (default false) |
remove_punctuation | boolean | No | Strip punctuation from the transcript and word chunks (default false) |
generate_srt | boolean | No | Produce SRT + word-by-word SRT files (default true). Only an explicit false (boolean or the string "false") disables it |
words_per_line | number | No | SRT: max words per line. Clamped 1–18 (default 12) |
lines_per_subtitle | number | No | SRT: max lines per cue. Clamped 1–4 (default 2) |
stretch_captions | boolean | No | SRT: extend a cue's end to the next cue's start so subtitles stay on screen continuously. Only small gaps are closed — a real pause is left intact. Default true; only an explicit false / "false" disables it. The same value is applied to the word-by-word SRT |
srt_options | object | string | No | JSON object (or JSON string) of { wordsPerLine, linesPerSubtitle }. Takes precedence over the flat words_per_line / lines_per_subtitle fields. A malformed JSON string is ignored silently |
project_id | string | No | Route the transcription into a specific project. Omit for the default "API Generations" project |
session_id | string | No | Append to an existing transcription session (e.g. the session_id returned by an earlier call) instead of the default API session. Access is checked against that session's project, which then replaces project_id |
*At least one of audio_url, video_url, file_url, or file must be provided — otherwise the request returns 400 Provide audio_url, video_url, or upload a file. For multipart/form-data uploads, send the options above as additional form fields; boolean flags are accepted as the strings "true" / "false".
Sources and formats
- URLs must be public. Direct file links work, and Google Drive, Dropbox and Loom share links are converted to downloads automatically. Private/internal network addresses are rejected. A URL source may be up to 4 GB (direct uploads stay capped at 500 MB), and the download has a 5-minute timeout.
- The URL is downloaded before the
POSTreturns, so a large URL source makes the submit call itself slow; a download failure comes back as400 VIDEO_DOWNLOAD_FAILED. - Supported types: video
mp4,webm,mov/quicktime,avi,mkv,flv,wmv; audiomp3/mpeg,wav,ogg,m4a,aac,flac,webm. Anything else returns400 UNSUPPORTED_FILE_TYPE.
POST /v1/transcribe has a raised rate limit of 100 requests/minute per account (vs. 30/min for most other generation endpoints) so you can fire a whole folder of files at once. Rejected requests do not count against the window. Each account can also have at most 60 transcriptions processing at once (a URL submit counts its own record toward that cap); beyond it the POST returns 429 CONCURRENT_LIMIT_REACHED until one finishes.
Examples
From Audio URL
curl -X POST https://api.kolbo.ai/api/v1/transcribe \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{"audio_url": "https://example.com/podcast-episode.mp3"}'From Video URL
curl -X POST https://api.kolbo.ai/api/v1/transcribe \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{"video_url": "https://example.com/interview.mp4"}'File Upload
curl -X POST https://api.kolbo.ai/api/v1/transcribe \
-H "X-API-Key: kolbo_live_..." \
-F "[email protected]"With Subtitle Formatting
curl -X POST https://api.kolbo.ai/api/v1/transcribe \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{
"audio_url": "https://example.com/interview.mp3",
"diarize": true,
"words_per_line": 6,
"lines_per_subtitle": 2,
"stretch_captions": false
}'Translated Subtitles
language translates the output into that language when it differs from the detected speech: the transcript text, the .txt file, the SRT and the word-by-word SRT. Only the timing array words stays in the original spoken language.
curl -X POST https://api.kolbo.ai/api/v1/transcribe \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://example.com/hebrew-interview.mp4",
"language": "en"
}'Response
This endpoint is asynchronous and fire-and-forget: the POST returns as soon as the job is queued and never contains a transcript.
Polling is the only completion mechanism. Kolbo never calls you back: there is no webhook, no callback_url field on this request body, and no route delivers outbound notifications. Socket.IO events are the web app's internal transport and are not part of the API contract — API-key generations are registered so the shared progress emitter drops their events, and the few emit sites that bypass that check are undocumented, unversioned and unsafe to build on. After the POST, loop against GET /api/v1/generate/{generation_id}/status until state is completed, failed or cancelled — the transcript record carries a cancelled status of its own even though this endpoint has no cancel route. Full contract: Polling & Cancellation.
Generation Started
{
"success": true,
"generation_id": "68f2c1a9b4e5d6f7a8b9c0d1",
"type": "transcription",
"model": "auto",
"credits_charged": null,
"poll_url": "/v1/generate/68f2c1a9b4e5d6f7a8b9c0d1/status",
"poll_interval_hint": 8,
"session_id": "68f2c1a9b4e5d6f7a8b9c0d2",
"project_id": "68f2c1a9b4e5d6f7a8b9c0d3"
}| Field | Type | Notes |
|---|---|---|
generation_id | string | Mongo ObjectId. This is what you poll with. |
type | string | Always "transcription" |
model | string | Always the literal "auto" — this endpoint takes no model parameter |
credits_charged | null | Always null on this endpoint — transcription is billed per minute of audio once the job finishes |
poll_url | string | The status path without the /api prefix. Prepend https://api.kolbo.ai/api. |
poll_interval_hint | number | Suggested seconds between polls — 8 here |
session_id / project_id | string | Where the transcription lives in the Kolbo app |
Completed Status
GET /api/v1/generate/{generation_id}/status
{
"success": true,
"generation_id": "68f2c1a9b4e5d6f7a8b9c0d1",
"type": "transcription",
"state": "completed",
"progress": 100,
"result": {
"text": "Speaker 1 [0:00 - 0:12]:\nHello and welcome to today's episode...",
"words": [
{ "text": "Hello", "start": 0.0, "end": 0.42 },
{ "text": "and", "start": 0.42, "end": 0.6 }
],
"srt_url": "https://media.kolbo.ai/stt/.../subtitles.srt",
"txt_url": "https://media.kolbo.ai/stt/.../transcript.txt",
"word_by_word_srt_url": "https://media.kolbo.ai/stt/.../words.srt",
"srt_content": "1\n00:00:00,000 --> 00:00:03,500\nHello and welcome to today's episode\n\n",
"duration": 245.8,
"audio_url": "https://media.kolbo.ai/uploaded-audio/.../episode-42.mp3",
"model": null,
"created_at": "2026-07-20T14:20:00Z"
}
}The output is result.text — this family has no urls array. The transcript is also available as downloadable files:
| Result field | Type | Notes |
|---|---|---|
text | string | The full transcript, grouped into speaker turns as Speaker N [m:ss - m:ss]: followed by that turn's text (a single-speaker recording is one Speaker 1 block). If the job fell back to the secondary engine it is plain unlabeled text. For the plain transcript without labels, download txt_url |
words | array | Timing items { text, start, end } (seconds). Always in the original spoken language, even when language translated the rest. Empty array when no timing data was stored |
srt_url | string | null | null when generate_srt was disabled |
txt_url | string | null | Plain-text download |
word_by_word_srt_url | string | null | Word-level timing. null when generate_srt was disabled. |
srt_content | string | null | The complete SRT inline, so you can process subtitles without a download |
duration | number | null | Source length in seconds |
audio_url | string | null | The stored copy of your source file |
model | null | Always null for transcription — the record does not persist a model identifier |
created_at | string (ISO 8601) |
result appears only under state: "completed". There is no partial read: the transcript is written internally while state is still processing (progress can already read 99–100), and the job only flips to completed once the files are stored. Branch on state, never on progress.
A job that produces no transcript fails rather than completing empty: silent audio fails with "No speech detected…", and when generate_srt is on but no SRT file could be produced the job fails too.
credits_used is returned, credits_breakdown is not. This pipeline's credit ledger rows are not linked to the generation id you poll with, so the completed status reports credits_used from the charge stored on the transcription itself and omits credits_breakdown. Transcription is billed — per minute of audio (duration rounded up to the next whole minute), at the credit rate of elevenlabs/scribe-v2 in GET /api/v1/models?type=stt (or elevenlabs/scribe-v2-srt when generate_srt is on).
POST /v1/generate/{id}/cancel does not work for transcription — it always returns 404. Transcription jobs are not in the cancellable registry. Let the job finish or ignore the result.
Failure
A failed transcription is still an HTTP 200 with success: true — the failure is in state, with the real message in error (e.g. the provider's rejection). Every failure.* sub-field except message is null on this endpoint. Credits are deducted only after a transcript exists, so a failed job costs nothing. See Polling & Cancellation.
A job still processing more than 30 minutes after it started is marked failed ("Transcription timed out or stuck in processing") the next time you submit a transcription.
Submission errors
Errors raised before the job starts come back from the POST as { "success": false, "error": "...", "code": "..." } with the HTTP status below.
| Status | code | Cause |
|---|---|---|
| 400 | — | No audio_url, video_url, file_url or file (Provide audio_url, video_url, or upload a file) |
| 400 | VIDEO_DOWNLOAD_FAILED | The URL could not be downloaded (not public, blocked address, timeout, too large) |
| 400 | UNSUPPORTED_FILE_TYPE | The file or downloaded URL is not one of the supported types above |
| 400 | FILE_TOO_LARGE | The source exceeds the size limit. An upload over 500 MB is rejected before the handler runs, so that response is { "status": false, "message": "File too large", "code": "FILE_TOO_LARGE" } |
| 403 | INSUFFICIENT_CREDITS | Balance below the minimum needed to start (at least one minute at the current per-minute rate) |
| 403 | NO_SUBSCRIPTION | The paying account has no subscription record |
| 429 | CONCURRENT_LIMIT_REACHED | The account's limit of 60 transcriptions processing at once is reached |
| 429 | — | More than 100 POST /v1/transcribe calls in a minute. This one is emitted by the rate limiter, so its body is different: { "status": false, "error", "message", "limit", "window_seconds", "retryAfter" } — honor retryAfter (seconds) |
project_id / session_id access errors use the shared codes in Errors and Limits.
JavaScript Example — URL Input
const KOLBO_API_KEY = "kolbo_live_..."; // Replace with your API key
const BASE_URL = "https://api.kolbo.ai";
async function transcribe(body) {
const response = await fetch(`${BASE_URL}/api/v1/transcribe`, {
method: "POST",
headers: {
"X-API-Key": KOLBO_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const started = await response.json();
if (!started.success) throw new Error(started.error);
const status = await pollUntilDone(started.generation_id, started.poll_interval_hint);
if (status.state !== "completed") throw new Error(status.error || status.state);
return status.result;
}
const TERMINAL = new Set(["completed", "failed", "cancelled"]);
// Minimal loop. A production client should also handle transient HTTP errors and
// an overall timeout — see /docs/developer-api/polling-and-cancellation.
async function pollUntilDone(generationId, intervalSeconds) {
while (true) {
await new Promise((r) => setTimeout(r, intervalSeconds * 1000));
const status = await fetch(
`${BASE_URL}/api/v1/generate/${generationId}/status`,
{ headers: { "X-API-Key": KOLBO_API_KEY } }
).then((r) => r.json());
if (TERMINAL.has(status.state)) return status;
}
}
async function main() {
// Transcribe from audio URL
const result = await transcribe({
audio_url: "https://example.com/podcast.mp3",
});
console.log("Full text:", result.text); // the transcript — no urls array here
console.log("Duration:", result.duration, "seconds");
console.log("SRT file:", result.srt_url);
console.log("Word-level SRT:", result.word_by_word_srt_url);
}
main().catch(console.error);JavaScript Example — File Upload
const KOLBO_API_KEY = "kolbo_live_..."; // Replace with your API key
const BASE_URL = "https://api.kolbo.ai";
async function transcribeFile(filePath) {
// Node.js: use fs to read the file
const fs = await import("fs");
const path = await import("path");
const formData = new FormData();
formData.append("file", new Blob([fs.readFileSync(filePath)]), path.basename(filePath));
const response = await fetch(`${BASE_URL}/api/v1/transcribe`, {
method: "POST",
headers: { "X-API-Key": KOLBO_API_KEY },
body: formData,
});
const started = await response.json();
if (!started.success) throw new Error(started.error);
// pollUntilDone() from the URL-input example above — a file upload returns the
// same envelope and is polled the same way.
const status = await pollUntilDone(started.generation_id, started.poll_interval_hint);
if (status.state !== "completed") throw new Error(status.error || status.state);
return status.result;
}
async function main() {
const result = await transcribeFile("./recording.mp3");
console.log("Transcript:", result.text);
console.log("SRT download:", result.srt_url);
}
main().catch(console.error);Python Example — URL Input
import requests
import time
KOLBO_API_KEY = "kolbo_live_..." # Replace with your API key
BASE_URL = "https://api.kolbo.ai"
HEADERS = {"X-API-Key": KOLBO_API_KEY}
# Transcribe from audio URL
response = requests.post(
f"{BASE_URL}/api/v1/transcribe",
headers=HEADERS,
json={"audio_url": "https://example.com/podcast.mp3"},
)
started = response.json()
if not started.get("success"):
raise Exception(started.get("error", "Request failed"))
TERMINAL = {"completed", "failed", "cancelled"}
def poll_until_done(generation_id, interval_seconds):
"""Minimal loop. Add transient-error retries and an overall timeout for
production — see /docs/developer-api/polling-and-cancellation."""
while True:
time.sleep(interval_seconds)
status = requests.get(
f"{BASE_URL}/api/v1/generate/{generation_id}/status",
headers=HEADERS,
).json()
if status["state"] in TERMINAL:
return status
status = poll_until_done(started["generation_id"], started["poll_interval_hint"])
if status["state"] != "completed":
raise Exception(status.get("error", status["state"]))
result = status["result"]
print("Full text:", result["text"]) # the transcript — no urls array here
print("Duration:", result["duration"], "seconds")
print("SRT file:", result["srt_url"])
print("Word-level SRT:", result["word_by_word_srt_url"])Python Example — File Upload
import requests
KOLBO_API_KEY = "kolbo_live_..." # Replace with your API key
BASE_URL = "https://api.kolbo.ai"
HEADERS = {"X-API-Key": KOLBO_API_KEY}
# Upload a local file
with open("recording.mp3", "rb") as f:
started = requests.post(
f"{BASE_URL}/api/v1/transcribe",
headers=HEADERS,
files={"file": ("recording.mp3", f)},
).json()
if not started.get("success"):
raise Exception(started.get("error", "Request failed"))
# poll_until_done() from the URL-input example above — a file upload returns the
# same envelope and is polled the same way.
status = poll_until_done(started["generation_id"], started["poll_interval_hint"])
if status["state"] != "completed":
raise Exception(status.get("error", status["state"]))
result = status["result"]
print("Transcript:", result["text"])
print("SRT download:", result["srt_url"])
print("Text download:", result["txt_url"])Tips
- Audio is automatically extracted from video files -- no preprocessing needed
- The
srt_contentfield contains the raw SRT text inline, useful if you want to process subtitles without downloading the file - The
word_by_word_srt_urlprovides word-level timing, ideal for karaoke-style subtitles or precise editing - Maximum file size is 500 MB for direct uploads and 4 GB for URL sources
- Use
words(inline{ text, start, end }timings) when you need word timing without downloading the word-by-word SRT - Poll at the cadence in
poll_interval_hint(8 seconds) - Batching is expected: the endpoint allows 100 requests/minute per account
Related
Speech and Sound
Text-to-speech, sound effects, and custom voices
Media Library
Upload a local file first and transcribe it by URL
Projects
Route transcriptions into a project with project_id
Errors and Limits
Error codes, rate limits, and retry guidance
Polling and Cancellation
The state machine, the reference poll loop, and failures