2026-11-06 · 4 min read
How to Add Captions to Meeting Recordings
Most meeting recordings are never rewatched, and the ones that are get scrubbed at 1.5x speed with the sound low. If someone missed the meeting, captions are the difference between a recording they can actually use and one they abandon in thirty seconds. Captioning recordings by hand makes no sense at company volume, so the realistic options are an API or nothing.
Why captioned meeting recordings get used
- People who missed the meeting can skim by reading instead of scrubbing blind.
- Crosstalk, accents, and bad microphones are easier to follow with text on screen.
- Captioned clips of decisions and demos can be shared in chat without extra editing.
- Burned-in captions play in any viewer, so nobody depends on a specific platform's caption toggle.
Submit a recording for captioning
caption.sh takes a JSON POST with the recording's source_url and returns a job_id. If your recordings live behind a login, the documented two-step signed-URL upload path covers files that are not publicly reachable. Captioning is $0.10 per minute, prepaid, so an hour-long meeting costs $6.
curl -X POST https://api.caption.sh/v1/videos \
-H "Authorization: Bearer $CAPTION_API_KEY" \
-H "Content-Type: application/json" \
-d '{"source_url": "https://example.com/video.mp4"}'
Poll for the captioned file
Jobs are asynchronous: submit, keep the job_id, and poll the status endpoint until the job reports done or failed. Bound the loop with a timeout so a problem surfaces instead of hanging. A 429 means the transcription throughput ceiling is reached; wait and retry.
import time
import requests
KEY = "your_api_key"
H = {"Authorization": f"Bearer {KEY}"}
job = requests.post(
"https://api.caption.sh/v1/videos",
headers=H,
json={"source_url": "https://example.com/video.mp4"},
).json()
job_id = job["job_id"]
# A 429 means the transcription throughput ceiling is reached. Wait and retry.
deadline = time.time() + 1800
status = None
while time.time() < deadline:
r = requests.get(f"https://api.caption.sh/v1/jobs/{job_id}", headers=H)
status = r.json()["status"]
if status in ("done", "error"):
break
time.sleep(10)
if status == "done":
r = requests.get(f"https://api.caption.sh/v1/jobs/{job_id}/result", headers=H, allow_redirects=False)
print(r.headers["Location"]) # signed URL; results are stored for 24 hours, then deleted
elif status == "error":
raise SystemExit("captioning failed")
else:
raise SystemExit("timed out waiting for the job")
The result endpoint redirects to a signed download URL, and results are stored for 24 hours, then deleted. Move the captioned file to your own storage in the completion step. The API documents no webhook surface, so polling is the supported integration.
A low-effort team workflow
The lightest version is: the recording lands in shared storage, a caption job is submitted automatically, and the captioned file is what gets linked in the meeting notes. Nobody has to remember to caption anything, and the recording is usable by everyone, not just the people with headphones and an hour to spare.