2026-11-01 · 5 min read
Captions for X Video: Burn In Subtitles Before You Post
Video on X plays muted more often than not. A clip that needs its audio to make sense loses most of its audience in the first second, so captions on the pixels themselves do real work there. Sidecar files are not an option on X the way they are on YouTube, which makes burned-in captions the practical route.
The workflow
caption.sh is an async job API. You POST a video URL to /v1/videos, the render happens on the service side, and you poll the job until it is done. The result is an MP4 with the captions burned into the frames, ready to upload to X like any other video. Pricing is prepaid at $0.10 per minute of video, 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"
r = requests.post(f"{API}/videos", headers=H,
json={"source_url": "https://example.com/clip.mp4",
"options": {"font": "inter"}}, timeout=30)
r.raise_for_status()
job_id = r.json()["job_id"]
for _ in range(120):
time.sleep(5)
job = requests.get(f"{API}/jobs/{job_id}", headers=H, timeout=30).json()
if job["status"] == "done":
break
if job["status"] == "error":
raise SystemExit(f"render failed: {job}")
else:
raise SystemExit("timed out waiting for render")
result = requests.get(f"{API}/jobs/{job_id}/result", headers=H, timeout=30)
open("captioned.mp4", "wb").write(result.content)
Details worth knowing
- Submissions are rate limited per API key. If a submit answers 429, wait a few seconds and retry; jobs already accepted are unaffected.
- The result endpoint redirects to a signed download URL; results are stored for 24 hours and then deleted. Download the file when the job finishes instead of treating the link as storage.
- If your clip only exists as a local file, the API also documents an upload flow: the POST returns a signed upload URL, you PUT the bytes to it, and the job starts when the upload completes.
- Videos that only exist behind your own login can be rendered by uploading directly rather than passing a URL.
If you caption clips for X on a schedule, the same script slots into a cron job or a CI step: submit, poll with a bounded loop, download, post. The caption styling lives in the options object, so changing the look is a config change, not a retime of an editor timeline.