2026-11-07 · 4 min read
How to Add Captions to YouTube Shorts
Shorts viewing happens in every kind of session: sound on in the Shorts player, muted previews in the home and search feeds, and plenty of moments where audio is simply off. Attention is measured in seconds either way. Captions are not an accessibility extra there; they are the difference between someone watching and someone swiping. If you publish Shorts at any volume, captioning each one in an editor becomes the bottleneck fast. An API makes captions part of the render instead of a separate step.
Why burned-in captions still earn their place on Shorts
- Home and search previews autoplay muted with captions on, so the readable text carries the preview.
- Burned-in captions survive reposts to other platforms with styling intact.
- Fast speech and jump cuts are easier to follow with words on screen.
- YouTube also auto-generates captions on Shorts. Burned-in text is part of the image, so there is no selectable caption track for a viewer to toggle, resize, or auto-translate. Keep both: burned-in captions for the guaranteed look, platform captions for accessibility.
- One captioned master keeps the same look everywhere the Short gets reposted.
Submit a Short for captioning
caption.sh accepts a JSON POST with the video's source_url and returns a job_id. The API fetches the file from the URL you give it, and a two-step signed-URL upload path is documented for files that are not publicly reachable. Captioning is $0.10 per minute, prepaid, and Shorts are short, so the cost per video is a few cents.
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 render
Jobs are asynchronous: submit, keep the job_id, and poll the status endpoint until the job reports done or failed, with a timeout. The transcription throughput ceiling is shared, so on a batch upload day 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 the job finishes, the result endpoint redirects to a signed download URL, and results are stored for 24 hours, then deleted. Pull the captioned file down or copy it to your storage in the completion step. The API documents no webhook surface, so polling is the supported pattern.
Fitting captions into a Shorts pipeline
The natural setup: cut the Short, submit it for captioning, and upload the captioned render. Because the text is burned into the file, the Short keeps its exact look on every platform it touches. The tradeoff is that burned-in text is part of the image, with no selectable caption track, so leave the platform captions enabled alongside it for accessibility.