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.