2026-10-29 · 6 min read
Captions for Course Platforms: A Captioning Step for Lesson Video Pipelines
A course platform's content team does not edit video; they publish it. Recordings come from instructors in every format and lighting condition, and the platform's job is to turn an upload into a lesson page as fast as possible. If captions depend on someone opening a timeline editor, the caption step either becomes a bottleneck or gets skipped, and a lesson without captions is hard to follow for anyone watching with the sound off or in a second language.
caption.sh is an API that burns word-timed, styled captions into video. You POST a lesson video, poll the job, and download a new MP4 with the captions rendered into the pixels. Because the captions are part of the image, every player, download, and re-encode shows them identically.
Where the captioning step fits
The natural place is right after the platform finishes its own processing: transcoding, thumbnail generation, and DRM packaging if you do that. Once the final MP4 or a reachable URL exists, hand it to the captioning job. When the job finishes, replace the lesson's video asset with the captioned render.
// caption a lesson video after your pipeline produces the final file
const KEY = process.env.CAPTION_API_KEY;
const submit = await fetch("https://api.caption.sh/v1/videos", {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
source_url: "https://cdn.example.com/lessons/week-3-recursion.mp4",
options: { font: "inter", position: "bottom" }
})
});
const { job_id } = await submit.json();
if (!job_id) { console.error("submit failed", await submit.text()); process.exit(1); }
const deadline = Date.now() + 20 * 60 * 1000; // long lessons need a generous window
let status = "";
while (Date.now() < deadline) {
const r = await fetch(`https://api.caption.sh/v1/jobs/${job_id}`, {
headers: { Authorization: `Bearer ${KEY}` }
});
status = (await r.json()).status;
if (status === "done" || status === "error") break;
await new Promise((s) => setTimeout(s, 10000));
}
if (status !== "done") { console.error("caption job did not finish:", status); process.exit(1); }
// GET /v1/jobs/{id}/result redirects to a signed download URL; results are stored for 24 hours, then deletedDetails that matter for course content
- Lessons are long compared to social clips, so price the job, not the month: captioning costs $0.10 per minute of video, prepaid, so a 45-minute lecture, split into segments under the 10-minute per-job cap, is a few dollars and a short intro lesson costs cents.
- Pick one caption style for the whole catalog. Font, size, position, and word highlighting are per-job options, so a platform-level default keeps every instructor's videos looking consistent.
- Jobs are independent, so a cohort's worth of uploads can be submitted as they arrive. Each API key has a throughput ceiling; a 429 response means slow down and retry, not that the job failed.
- The API documents no webhook surface. Poll the job endpoint with your own timeout, as in the sample, and treat a job that outlives your deadline as an operational error to retry or flag.
If your platform hosts the lessons, the cheapest evaluation is to caption one real lecture and watch it the way a student on a phone would. The difference between a lesson you can follow on mute and one you cannot is visible in the first thirty seconds.