Endpoints
Captions API
POST /v1/captions burns word-by-word animated captions into any video up to 15 minutes long. It transcribes the speech with Whisper large-v3-turbo, or uses word timings you send, and styles the text with 3 presets you can fine-tune.
You get the captioned MP4 back, plus the transcript with word timings and ready-made SRT, VTT and ASS subtitle files from the same job.
- Typical render
- 13 to 40 s
- 7 s and 42 s clips in our tests
- Price
- $0.02
- per output minute
- Input
- Up to 15 minutes
- 500 MB per file
Example#
Send a public link to your video, or the URL of an upload, and a preset. The call waits up to 60 seconds for the render, so a short clip comes back finished.
curl -sS "https://api.reelsdk.com/v1/captions" \
-H "Authorization: Bearer $REELSDK_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://example.com/interview.mp4",
"style": "karaoke"
}'Parameters#
Send a JSON body. Fields we don't recognize are ignored, so check the spelling of optional fields.
| Parameter | Description |
|---|---|
video_urlstringRequired | The video to caption. Any public URL works, including a Public http(s) URL, up to 4,096 characters |
stylestring or object | A preset name, or a style object that starts from a preset and changes only the fields you set. See Caption styles. bolddefaultkaraokeminimal |
languagestring | Language of the speech. Leave it out and we detect it from the audio. Region tags are dropped, so A code like |
wordsarray | Word timings to burn in instead of transcribing, as Up to 50,000 items |
promptstring | Names and terms the transcriber should spell correctly, such as product or people names. Ignored when you send Up to 500 characters |
qualitystring | H.264 encoder quality: draftstandarddefaulthigh |
webhook_urlstring | We POST the finished job to this URL, signed with your webhook secret. See Webhooks. Public http(s) URL |
Caption styles#
Pass a preset name like bold, karaoke or minimal to style your captions right away. To change how they look, send a style object instead. It starts from the preset you name, or bold, and overrides only the fields you set.
| Preset | Font | Layout | Colors |
|---|---|---|---|
| bold | Montserrat, uppercase | Middle, 3 words at a time | White text, the spoken word in #FFE14D yellow |
| karaoke | Poppins | Bottom, 5 words at a time | White text, the spoken word in #3DDCFF cyan |
| minimal | Poppins at 0.85 size | Bottom, 7 words at a time | White text, no highlight |
All three presets draw a black outline. Colors accept #RRGGBB, #RGB or one of 15 names: white, black, yellow, gold, orange, red, pink, magenta, purple, blue, cyan, green, lime, gray and grey.
| Style field | Description |
|---|---|
style.presetstring | The preset the other fields start from. bolddefaultkaraokeminimal |
style.fontstring | Caption font. Words in scripts these fonts can't draw, such as Japanese or Arabic, switch to a matching Noto font. MontserratAntonBebas NeuePoppins |
style.positionstring | Where the captions sit. topmiddlebottom |
style.max_wordsinteger | The most words on screen at once. 1 to 8 |
style.uppercaseboolean | Show every word in capitals. |
style.text_colorstring | Color of the caption text.
|
style.highlight_colorstring | Color of the word being spoken.
|
style.outline_colorstring | Color of the outline around each word.
|
style.sizenumber | Text scale, where 0.5 to 2 |
{
"style": {
"preset": "bold",
"font": "Anton",
"position": "bottom",
"max_words": 2,
"highlight_color": "lime"
}
}Languages and fonts#
We detect the language from the audio, or you can set language with a code like en or pt-BR to skip detection. Three-letter codes such as jpn work too.
Japanese, Chinese, Korean, Arabic, Hebrew, Thai, Devanagari, Lao and Myanmar text switches to a matching Noto font automatically. Chinese, Japanese and Korean captions break lines between words instead of at spaces, and right-to-left scripts render right to left.
Your own word timings#
If you already have word timings from your own transcriber or an edited transcript, pass them in words and we skip transcription. Each item is a word with start and end in seconds. An end earlier than its start is moved up to the start.
{
"video_url": "https://example.com/interview.mp4",
"style": {
"preset": "karaoke",
"highlight_color": "#FF5A36"
},
"words": [
{ "word": "Ship", "start": 0.12, "end": 0.41 },
{ "word": "it", "start": 0.41, "end": 0.58 },
{ "word": "today.", "start": 0.58, "end": 1.1 }
]
}When we transcribe, send prompt with names and terms so the transcriber spells them right.
Response#
A finished job answers 200 and its outputs hold five file URLs. If the render takes longer than your wait, you get 202 with the same job and a poll_url. See Jobs for waiting and polling.
{
"job": {
"id": "job_q33902h8shjtevc5j2cd",
"op": "captions",
"status": "succeeded",
"stage": null,
"created_at": "2026-10-26T01:15:43.902Z",
"finished_at": "2026-10-26T01:15:56.165Z",
"outputs": {
"video": "https://api.reelsdk.com/f/out/job_q33902h8shjtevc5j2cd/video.mp4",
"transcript": "https://api.reelsdk.com/f/out/job_q33902h8shjtevc5j2cd/transcript.json",
"srt": "https://api.reelsdk.com/f/out/job_q33902h8shjtevc5j2cd/captions.srt",
"vtt": "https://api.reelsdk.com/f/out/job_q33902h8shjtevc5j2cd/captions.vtt",
"ass": "https://api.reelsdk.com/f/out/job_q33902h8shjtevc5j2cd/captions.ass"
},
"result": null,
"duration_s": 7.2,
"error": null,
"usage": {
"output_seconds": 7.2,
"audio_seconds": 7.211,
"tts_characters": 0,
"images": 0
},
"poll_url": "https://api.reelsdk.com/v1/jobs/job_q33902h8shjtevc5j2cd"
}
}| Output | File | Contents |
|---|---|---|
| video | video.mp4 | The captioned H.264 video in your framing, with the short side capped at 1080 px and the frame rate at 60 fps. AAC audio is kept as is; other audio becomes AAC at 192 kbps. |
| transcript | transcript.json | Full text, language, duration and every word with start and end in seconds |
| srt | captions.srt | SubRip subtitles for players and upload forms |
| vtt | captions.vtt | WebVTT subtitles for the HTML <track> element |
| ass | captions.ass | The styled subtitle file we burned in |
transcript.json
{
"text": "Three days by the water, mountains at sunrise, forests by noon, and a lake that held the whole sky.",
"language": "en",
"duration_s": 7.2,
"words": [
{ "word": "Three", "start": 0, "end": 0.44 },
{ "word": "days", "start": 0.44, "end": 0.66 },
{ "word": "by", "start": 0.66, "end": 0.88 },
{ "word": "the", "start": 0.88, "end": 0.98 },
{ "word": "water,", "start": 0.98, "end": 1.32 }
]
}captions.srt
1
00:00:00,000 --> 00:00:05,500
Three days by the water, mountains at
sunrise, forests by noon, and a lake that
2
00:00:05,500 --> 00:00:07,080
held the whole sky.usage.output_seconds is the length of the captioned video and usage.audio_seconds the speech we transcribed. Output files stay up for 7 days.
Errors#
Validation problems return 400 before a job starts. Problems with the video itself fail the job with 422, and the body carries the job next to the error. The full list is in Errors.
| Code | HTTP | When it happens |
|---|---|---|
| INVALID_REQUEST | 400 | A field is missing or invalid, for example video_url: must be an http(s) URL |
| INPUT_FETCH_FAILED | 422 | We couldn't download the video, for example Could not fetch video_url: HTTP 404 |
| INPUT_TOO_LARGE | 422 | The video is over 500 MB |
| INPUT_UNSUPPORTED | 422 | The file isn't readable media, runs longer than 15 minutes, or has no audio track and you sent no words |
| TRANSCRIBE_FAILED | 500 | Speech-to-text failed, even after our automatic retry. Send the request again. |
Tips#
- Captions render in about the length of the clip. Clips longer than about a minute can outlast the default 60 s wait, so raise
?waitor use a webhook. - Your video needs an audio track with speech, unless you send
words. - The
srtandvttfiles work with YouTube, video players and platforms that take subtitle files, so one job gives you burned-in captions and a subtitle track. - Use
quality: "draft"while you tune a style, then render the final cut withstandardorhigh.
Last updated 26 October 2026