2026-11-07 · 4 min read

A Caption API for SaaS Onboarding Videos

Onboarding videos carry a lot of weight: they are the walkthrough a new user gets when nobody is there to walk them through. They get watched at desks, on phones, in shared offices, often muted. If your onboarding videos only work with sound on, a chunk of new users never actually get onboarded. Captioning them by hand is fine for one video and quietly breaks when you ship product updates every week.

What captions fix in onboarding

  • New users can follow setup steps on mute while they work in another tab.
  • Feature names and UI labels stay visible as text instead of being misheard.
  • Non-native speakers get a much easier first week with the product.
  • Burned-in text has no selectable caption track for the viewer to toggle or resize, so where accessibility requirements apply, pair it with the player's own closed captions.
  • A captioned video plays the same in your app, docs, and help center without depending on each player's caption support.

Captioning as a publish step

The clean setup treats captioning like a build step: record or update the walkthrough, export it, submit it for captioning, and the captioned render is what ships to the help center and in-app. caption.sh accepts a JSON POST with a source_url and returns a job_id, so the whole thing is a submit call and a 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. Onboarding videos are short, so keeping every walkthrough captioned costs a few dollars per product update.

Poll for the finished video

Jobs are asynchronous: submit, keep the job_id, and poll until the job is done or failed, with a timeout. The transcription throughput ceiling is shared, so if you re-caption the whole library after a redesign, a 429 means 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")

When a job completes, 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 completion step. The API documents no webhook surface, so plan on polling.

Keeping captions current with the product

The failure mode for onboarding content is staleness: the UI changes and the videos lag. Because re-captioning is one submit call, the same pipeline that re-records the walkthrough can re-caption it, so captions stop being the reason an update does not ship.

Back to all articles