2026-10-02 · 4 min read

Caption a video from the command line with curl: submit, poll, download

Sometimes the right tool is a shell script. A cron job, a CI step, or a one-off batch over a folder of links does not need an SDK or an FFmpeg install. Captioning a video through caption.sh is three HTTP calls, and curl plus jq is enough to do all three.

The three calls

You POST a video URL to /v1/videos and get a 202 with a job id. You poll /v1/jobs/{job_id} until the status is done. Then you fetch /v1/jobs/{job_id}/result, which returns the MP4 with the captions burned in. The render runs on our side, so nothing heavy runs on your machine.

The script

#!/usr/bin/env bash
set -euo pipefail

API="https://api.caption.sh"
AUTH="Authorization: Bearer $CAPTION_API_KEY"

# 1. submit
JOB=$(curl -sS -X POST "$API/v1/videos" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d "{\"source_url\": \"$1\", \"options\": {\"max_words\": 3}}" | jq -r .job_id)

# 2. poll
while :; do
  STATUS=$(curl -sS -H "$AUTH" "$API/v1/jobs/$JOB" | jq -r .status)
  [ "$STATUS" = "done" ] && break
  [ "$STATUS" = "error" ] && { echo "render failed: $JOB" >&2; exit 1; }
  sleep 5
done

# 3. download
curl -sS -H "$AUTH" "$API/v1/jobs/$JOB/result" -o "$2"
echo "saved $2"

Save it as caption-run.sh, set CAPTION_API_KEY from the dashboard, and run it with a video URL and an output name. The options object is optional. max_words controls how many words show at once, and 3 is a good start for short-form clips.

Handling the cases that happen in production

  • A 402 on the submit means the prepaid balance is empty. Stop the loop, top up, then run it again.
  • A 422 means the input was rejected, for example a video that is too long or an option that failed validation. Fix the input instead of retrying.
  • Add -f to curl on the submit if you want the script to fail on any HTTP error, and print the response body first so the reason is not lost.
  • Job states are awaiting_upload, queued, running, done, and error. Put a ceiling on the loop, for example 120 polls, and treat anything past it as a failure.

Looping over a list

Wrap the script in a while read loop over a text file of URLs and run three to five at a time with xargs -P. Print the job id next to each source URL so a failed render is easy to find later. Because the work happens remotely, the limit is your own concurrency cap, not CPU on your box.

When this is the wrong tool

caption.sh burns captions into the video. It does not export SRT or VTT sidecar files, so if you need a transcript file for an accessibility archive, use a transcription service for that and keep caption.sh for the version people actually watch on a feed.

Try it

Judge the output in the playground at caption.sh first, then grab an API key from the dashboard and run the script above. The full reference is at caption.sh/docs, and the OpenAPI spec is at api.caption.sh/openapi.json.

Back to all articles