Quickstart
Turn an audio file into a subtitle file with the API, from key to download.
This page takes one audio file all the way to an SRT file. You need an API key and a file. Audio-only files work and upload much faster than video.
export SUBTITLE_AI_KEY=sk_live_...
export API=https://app.subtitle.ai/api/v1The steps
Create the job
Tell the API about the file. durationSeconds is required because billing uses it. Read it from the file, for example with ffprobe -v error -show_entries format=duration -of csv=p=0 episode.wav.
curl -s $API/jobs \
-H "Authorization: Bearer $SUBTITLE_AI_KEY" \
-H "Content-Type: application/json" \
-d '{
"filename": "episode.wav",
"contentType": "audio/wav",
"sizeBytes": 52428800,
"durationSeconds": 1520.4,
"settings": { "language": "nl" }
}'You get 201 Created with the job id, an editorUrl and the upload details:
{
"id": "6a5f3c0e9b1d4e2a7c8b9d01",
"status": "uploading",
"name": "episode",
"editorUrl": "https://app.subtitle.ai/editor/6a5f3c0e9b1d4e2a7c8b9d01",
"upload": {
"method": "PUT",
"url": "https://…",
"headers": { "Content-Type": "audio/wav", "Content-Length": "52428800" },
"expiresAt": "2026-10-02T11:00:00.000Z"
}
}Upload the file
PUT the file to upload.url, with the same Content-Type and the exact file size. The upload starts transcription. The URL expires after one hour.
curl -X PUT --upload-file episode.wav \
-H "Content-Type: audio/wav" \
"UPLOAD_URL"Wait until it is ready
Poll the job every 10 to 30 seconds until status is ready or failed.
curl -s $API/jobs/JOB_ID -H "Authorization: Bearer $SUBTITLE_AI_KEY"Download the subtitles
curl -s "$API/jobs/JOB_ID/subtitles?format=srt" \
-H "Authorization: Bearer $SUBTITLE_AI_KEY" -o episode.srtThe same flow in code
Node 18 or later, and ffprobe on your path. Run it as node subtitle.mjs episode.wav.
import { readFile, stat, writeFile } from 'node:fs/promises';
import { basename } from 'node:path';
import { execFileSync } from 'node:child_process';
const API = 'https://app.subtitle.ai/api/v1';
const auth = { Authorization: `Bearer ${process.env.SUBTITLE_AI_KEY}` };
const file = process.argv[2];
async function call(path, options = {}) {
const response = await fetch(`${API}${path}`, { ...options, headers: { ...auth, ...options.headers } });
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
return response;
}
const { size } = await stat(file);
const durationSeconds = Number(
execFileSync('ffprobe', ['-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', file]),
);
// 1. Create the job.
const job = await (
await call('/jobs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: basename(file),
contentType: 'audio/wav', // match your file: audio/mpeg, audio/mp4, video/mp4 ...
sizeBytes: size,
durationSeconds,
}),
})
).json();
// 2. Upload the media. fetch sets Content-Length from the body.
const put = await fetch(job.upload.url, {
method: 'PUT',
headers: { 'Content-Type': job.upload.headers['Content-Type'] },
body: await readFile(file),
});
if (!put.ok) throw new Error(`Upload failed: ${put.status}`);
// 3. Wait until it is ready.
let status;
do {
await new Promise((resolve) => setTimeout(resolve, 15_000));
status = await (await call(`/jobs/${job.id}`)).json();
console.log(status.status);
} while (!['ready', 'failed'].includes(status.status));
if (status.status === 'failed') throw new Error(status.error);
// 4. Download the subtitles.
const srt = await (await call(`/jobs/${job.id}/subtitles?format=srt`)).text();
await writeFile('subtitles.srt', srt);
console.log(`Saved subtitles.srt. Review it at ${job.editorUrl}`);Python 3.9 or later with requests, and ffprobe on your path. Run it as python subtitle.py episode.wav.
import os
import subprocess
import sys
import time
import requests
API = "https://app.subtitle.ai/api/v1"
AUTH = {"Authorization": f"Bearer {os.environ['SUBTITLE_AI_KEY']}"}
path = sys.argv[1]
size = os.path.getsize(path)
duration = float(
subprocess.check_output(
["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", path]
)
)
# 1. Create the job.
response = requests.post(
f"{API}/jobs",
headers=AUTH,
json={
"filename": os.path.basename(path),
"contentType": "audio/wav", # match your file: audio/mpeg, audio/mp4, video/mp4 ...
"sizeBytes": size,
"durationSeconds": duration,
},
)
response.raise_for_status()
job = response.json()
# 2. Upload the media. requests sets Content-Length from the body.
with open(path, "rb") as media:
put = requests.put(
job["upload"]["url"],
headers={"Content-Type": job["upload"]["headers"]["Content-Type"]},
data=media,
)
put.raise_for_status()
# 3. Wait until it is ready.
while True:
time.sleep(15)
status = requests.get(f"{API}/jobs/{job['id']}", headers=AUTH).json()
print(status["status"])
if status["status"] in ("ready", "failed"):
break
if status["status"] == "failed":
raise SystemExit(status["error"])
# 4. Download the subtitles.
srt = requests.get(f"{API}/jobs/{job['id']}/subtitles", params={"format": "srt"}, headers=AUTH)
srt.raise_for_status()
with open("subtitles.srt", "wb") as out:
out.write(srt.content)
print(f"Saved subtitles.srt. Review it at {job['editorUrl']}")Then, hand it to a person
Open the job's editorUrl to review it in the editor. The job appears in the project list of the person who created the API key, or of the organization's longest-standing owner if they have left. Their corrections are included the next time you download.
Next
- Create a job lists every setting you can send.
- Results covers transcripts, formats and time offsets.
- Errors, limits and billing covers what can go wrong.