2026-11-01 · 6 min read

Captions for Live Event Recordings: From Raw Stream to Watchable Replay

A recorded talk or panel is some of the best content an event produces, and most of it gets watched the way everything else gets watched: muted, in a feed, in pieces. Captions on the recording are what make that watching possible. The blocker is length. An hour of footage is an hour of someone's time in a captioning tool, unless the captioning is automated.

The API shape

caption.sh is built for this shape of work. You POST the recording's URL to /v1/videos with an options object, get back a job_id, and poll GET /v1/jobs/{job_id} until it reports done. The rendered MP4 has the captions burned in, so it plays with subtitles everywhere, including players that do not support caption tracks. Pricing is $0.10 per minute of video, prepaid, and failed renders are not charged.

import requests, time

KEY = "YOUR_CAPTION_API_KEY"
H = {"Authorization": f"Bearer {KEY}"}
API = "https://api.caption.sh/v1"

def caption_recording(source_url, out_path):
    r = requests.post(f"{API}/videos", headers=H,
                      json={"source_url": source_url, "options": {}},
                      timeout=30)
    if r.status_code == 402:
        raise RuntimeError("out of credits, top up first")
    r.raise_for_status()
    job_id = r.json()["job_id"]

    deadline = time.time() + 60 * 60
    while time.time() < deadline:
        time.sleep(15)
        job = requests.get(f"{API}/jobs/{job_id}", headers=H, timeout=30).json()
        if job["status"] == "done":
            break
        if job["status"] == "error":
            raise RuntimeError(f"render failed: {job}")
    else:
        raise RuntimeError("render did not finish in time")

    mp4 = requests.get(f"{API}/jobs/{job_id}/result", headers=H, timeout=60)
    open(out_path, "wb").write(mp4.content)

Working with long files

  • Long recordings take longer to render, so give the polling loop a real deadline instead of an infinite wait. The example above allows an hour and exits on done, error, or timeout.
  • A 429 means the transcription throughput ceiling is reached. If you caption several sessions from one event, a 429 on submit means pause and retry, not resubmit from scratch.
  • Results are stored for 24 hours and then deleted. Download promptly into your own storage.
  • If the recording is not on a public URL, use the documented upload flow: the submit response includes a signed upload URL, you PUT the file to it, and the job starts when the upload finishes.

If your events team publishes replays after every event, this turns captioning from a per-event editing task into a step in the publish script. Submit when the raw recording lands, poll, download, upload the captioned version wherever the replay lives.

Back to all articles