How to make SRT subtitles from a video with an API

Send your video to POST /transcribe with output_format set to srt and word_timestamps set to false. Once the job finishes, save the returned string as a .srt file. If you want subtitles in another language, pass translate_to. This guide walks through the request, what the SRT output looks like, how to build clean cues from word timings, and how to generate WebVTT or burn the subtitles into the video.

Get an API key Quickstart Supported languages

Last updated: · Markdown

The request

Two calls: start the job, then poll it. The result of a finished srt job is a string, not JSON. This version needs only curl and jq:

# 1. Start the job. "word_timestamps": false gives one cue per segment.
JOB=$(curl -s https://app.kolwrite.com/transcribe \
  -H "x-api-key: $KOLWRITE_API_KEY" -H "Content-Type: application/json" \
  -d '{"audio_url": "https://example.com/talk.mp3", "model": "oracle", "output_format": "srt", "word_timestamps": false}' \
  | jq -r .jobId)

# 2. Poll until it is done and save the result string as a .srt file.
while true; do
  REPLY=$(curl -s "https://app.kolwrite.com/transcribe/$JOB" -H "x-api-key: $KOLWRITE_API_KEY")
  STATUS=$(echo "$REPLY" | jq -r .status)
  if [ "$STATUS" = "COMPLETED" ]; then echo "$REPLY" | jq -r .result > talk.srt; break; fi
  if [ "$STATUS" = "FAILED" ]; then echo "$REPLY" | jq -r .error; break; fi
  sleep 5
done

You can pass a video file directly and the API will read its audio track. Upload local files up to 100 MiB through POST /upload first. For long videos, extract the audio with ffmpeg before uploading. The Python helper in the video editing guide handles both steps.

What the SRT contains

Setting "word_timestamps": false gives you one cue per segment, like a standard subtitle file:

1
00:00:01,000 --> 00:00:03,750
Hello everyone and welcome to the show.

2
00:00:05,000 --> 00:00:07,750
Today we talk about editing video from a transcript.

By default, word timestamps are turned on. The API outputs a separate cue for every word, repeating the full segment text and wrapping the active word in a yellow <font> tag. This creates a read-along highlight instead of ordinary subtitles:

1
00:00:01,000 --> 00:00:01,350
<font color="yellow">Hello</font> everyone and welcome to the show.

2
00:00:01,400 --> 00:00:01,750
Hello <font color="yellow">everyone</font> and welcome to the show.

Set "word_timestamps": false if you want standard subtitles, or build your own cues from the word timings in the JSON output as shown below.

Build custom cues from word timings

Ask for output_format: "json" and group the words into cues yourself. Keep each cue to at most 42 characters and 6 seconds, and start a new one after a pause. That gives you control over line length, which matters when burning captions into video.

srt_tools.py

# srt_tools.py — neat subtitle cues from word timings
def srt_time(seconds):
    ms = round(seconds * 1000)
    h, ms = divmod(ms, 3_600_000)
    m, ms = divmod(ms, 60_000)
    s, ms = divmod(ms, 1000)
    return f"{h:02}:{m:02}:{s:02},{ms:03}"


def cues_from_words(words, max_chars=42, max_seconds=6.0, max_gap=0.8):
    """Group timed words into subtitle cues: (start, end, text)."""
    cues, line = [], []
    for word in words:
        text = word["word"].strip()
        if not text:
            continue
        if line:
            length = sum(len(w["word"]) + 1 for w in line) + len(text)
            if (length > max_chars
                    or word["end"] - line[0]["start"] > max_seconds
                    or word["start"] - line[-1]["end"] > max_gap):
                cues.append(line)
                line = []
        line.append({"start": word["start"], "end": word["end"], "word": text})
    if line:
        cues.append(line)
    return [(c[0]["start"], c[-1]["end"], " ".join(w["word"] for w in c)) for c in cues]


def cues_from_result(result):
    """Cues for a whole JSON result; a segment without word times becomes one cue."""
    cues = []
    for segment in result["segments"]:
        if segment.get("words"):
            cues += cues_from_words(segment["words"])
        elif segment["text"].strip():
            cues.append((segment["start"], segment["end"], segment["text"].strip()))
    return cues


def write_srt(cues, path):
    with open(path, "w", encoding="utf-8") as out:
        for number, (start, end, text) in enumerate(cues, 1):
            out.write(f"{number}\n{srt_time(start)} --> {srt_time(end)}\n{text}\n\n")

cues_from_result(result) pulls (start, end, text) tuples for the full result, falling back to segment times if the model didn't return word times. Then write_srt(cues, "talk.srt") writes the file to disk.

Subtitles in another language

Pass a language code in translate_to and pick an Oracle model (oracle_mini is the cheapest one with translation). The cue text comes back in the target language. Keep word_timestamps set to false so you get one cue per segment.

{
  "audio_url": "https://example.com/lecture-he.mp4",
  "model": "oracle_mini",
  "language": "he",
  "translate_to": "en",
  "output_format": "srt",
  "word_timestamps": false
}

If you need several languages for the same video, run a separate job for each target language. Treat machine translation as a draft and check names, numbers, and specific terms before publishing.

WebVTT, ASS, and other formats

The API returns json, txt or srt only. Convert SRT to WebVTT for an HTML5 <video> track with ffmpeg:

ffmpeg -i video.srt video.vtt

ffmpeg converts between most subtitle formats the same way: just change the output file extension.

Burn in or attach the subtitles

Burned into the video frames:

ffmpeg -i edited.mp4 -vf "subtitles=edited.srt" -c:a copy burned.mp4

Attached as an MP4 subtitle track that viewers can toggle on and off:

ffmpeg -i edited.mp4 -i edited.srt -c copy -c:s mov_text -metadata:s:s:0 language=eng soft.mp4

YouTube Studio and most video players and editors take .srt files directly.

Hebrew and Arabic subtitles

  • The API returns plain Unicode text. Right-to-left layout is handled by the video player or by ffmpeg's subtitle renderer, which renders both Hebrew and Arabic properly as long as the font includes the glyphs.
  • Pick a font that covers the script and check a single rendered frame before encoding the whole video. In our tests, Arial rendered Hebrew fine, but missed a glyph on one Arabic word, so use a different font for Arabic.
ffmpeg -i video.mp4 -vf "subtitles=video.he.srt:force_style='FontName=Arial,FontSize=24'" -c:a copy burned.mp4
  • If a line ends with punctuation or Latin characters like a brand name or a number, players without proper bidirectional text support may put them on the wrong side. Test on the players your viewers actually use.

Pricing

JobModelCost
45-minute lecture, subtitlesoracleUS$1.08
The same lecture, translated subtitlesoracle_miniUS$0.72
One hour of clear audio, subtitlesbaseUS$0.48

Billed by the second with no minimum. Full rates: /llms/pricing.md.

Questions and answers

How do I get an SRT file from a video?

Send the video URL to POST /transcribe with output_format: "srt" and word_timestamps: false. Poll the job until it finishes, then save the result string as a .srt file.

Does the API return WebVTT?

No, only json, txt, or srt. You can convert an SRT file to WebVTT with ffmpeg -i video.srt video.vtt.

Why does my SRT have one cue per word?

Word timestamps are enabled by default, so the SRT output has a separate cue for every word with the active word highlighted. Pass word_timestamps: false if you want one cue per segment, or build the cues yourself from the JSON.

Can I get subtitles in another language?

Yes. Set translate_to to the target language code and use an Oracle model. The subtitle cues will come back translated.

What is the difference between captions and subtitles?

Subtitles transcribe or translate speech for viewers who can hear the audio, while captions also include non-speech sound descriptions for deaf or hard-of-hearing viewers. The API gives you the timed speech text; if you need full captions, you will need to add the sound descriptions yourself.