2026-11-06 · 4 min read
How to Add Captions to Online Course Videos
If you sell or teach an online course, your videos are the product. Students watch them on phones, on lunch breaks, with the sound low or off, and a good share of them are not native speakers of your teaching language. Captions used to mean an editing pass per lesson, which is why so many course videos still ship without them. An API turns captioning into a step in your upload process instead of a project.
Why course videos specifically need captions
- Students rewatch lessons on mute and skim by reading ahead.
- Technical terms and names are easier to follow when they are on screen.
- Captioned previews and sample lessons convert better in silent feeds.
- Accessibility expectations for paid content keep rising.
The upload-pipeline approach
Instead of captioning lessons one by one in an editor, treat it like transcoding or thumbnailing: when a lesson video lands in your storage, submit it for captioning, and publish the captioned render. caption.sh takes a JSON POST with the video's source_url and returns a job_id, so the integration is a submit call and a status poll.
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"}'
Captioning is $0.10 per minute, prepaid. A ten-minute lesson costs a dollar, which makes captioning an entire course catalog realistic rather than a luxury.
Poll until the lesson is ready
Jobs are asynchronous. Submit, keep the job_id, and poll the status endpoint until the job is done or failed, with a timeout so a stuck job fails loudly. If you are backfilling a whole catalog and hit a 429, that is the per-key submission rate limit: 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. Save the captioned file to your own storage in the same step. The API documents no webhook surface, so the poll loop is the supported pattern.
Where creators actually plug this in
Most creators keep it simple: record, upload the raw file, submit the caption job, and swap in the captioned video on the course page. Because the captions are burned into the file, they work in every player, every embed, and every download without depending on each platform's caption support.