# caption.sh: the captions API built for AI agents Turn raw video into post-ready content without opening an editor. One API for transcription, styling, rendering, and delivery. caption.sh takes a video with speech, transcribes it with per-word timestamps, and burns animated captions into a new MP4: same resolution, frame rate and audio, with the spoken word highlighted in sync and the text auto-fitted to the frame. It is built for developers, AI agents, and automated UGC / short-form pipelines rather than for manual editing in a web UI. ## Who it is for - AI agents that make or edit video and need a captions step (via the REST API or the hosted MCP server) - Automated UGC, faceless-video and AI-avatar ad pipelines - Short-form social: TikTok, Reels, YouTube Shorts - Podcast clips, tutorials, product demos, and accessibility ## How it works 1. **Upload once.** Create a job with `POST /v1/videos`, then upload the file straight to storage, or pass a `source_url` and the API pulls the video itself. 2. **Style in JSON.** Font, size, words per caption, position, colours, highlight, outline or backdrop, tilt, emphasis and entrance animation are all plain options. All optional. 3. **Render async.** A durable queue drives isolated render workers built for real video jobs. 4. **Ship the MP4.** Poll `GET /v1/jobs/{id}`, then download the captioned MP4 from `GET /v1/jobs/{id}/result`. ## For agents: hosted MCP server Point any MCP client at `https://mcp.caption.sh/mcp` with your API key as a bearer token. No install. ```bash claude mcp add --transport http caption https://mcp.caption.sh/mcp \ --header "Authorization: Bearer $CAPTION_API_KEY" ``` Tools: `caption_video`, `get_caption_job`, `list_caption_styles`, `list_caption_jobs`, `get_caption_balance`. Every tool describes what it does, what the result looks like on screen, and when to use it. ## Pricing Simple prepaid pricing: **$0.10 per minute** of video, rounded up to the next minute. Duration is measured server-side, so you pay for the real length. Top up with credit packs of $5, $10, $20, $50 (includes a $5 bonus) or $100 (includes a $10 bonus). A job never starts if your balance can't cover it. ## FAQ See the FAQ section at https://caption.sh/#faq or the full reference at https://caption.sh/docs.md. ## Links - API reference: https://caption.sh/docs.md - Get an API key: https://caption.sh/dashboard - Everything in one file for LLMs: https://caption.sh/llms-full.txt --- # caption.sh API reference An async job API. One call to `POST /v1/videos` creates a job: upload a file or pass a `source_url`, then poll for status and download the captioned MP4. Every styling option is optional and has a good-looking default, so the simplest call produces a clean result with zero configuration. All endpoints live under `https://api.caption.sh/v1`. Every request is authenticated. ## Authentication Create an API key in the dashboard (https://caption.sh/dashboard, Keys) and send it as a bearer token. Keys are shown once at creation, hashed at rest, and revocable from the dashboard. The web app uses a Clerk session JWT; scripts, pipelines and agents use an API key. Both resolve to the same account. ``` Authorization: Bearer sk_live_... # an API key Authorization: Bearer eyJraWQ... # or a Clerk JWT (web session) ``` ## Endpoints | Method | Path | Purpose | |---|---|---| | POST | /v1/videos | Create a job: upload a file or pull a `source_url`. Returns 202 with `job_id` | | GET | /v1/jobs/{id} | Job status: `awaiting_upload`, `queued`, `running`, `done`, or `error` | | GET | /v1/jobs/{id}/result | 302 redirect to a signed download URL for the captioned MP4 | | GET | /v1/jobs | List your jobs | | GET | /v1/balance | Prepaid balance and recent transactions | ## Quickstart: pull from a URL (one call) Hand the API a public video URL and it pulls the video itself. No upload step. ```bash curl -X POST https://api.caption.sh/v1/videos \ -H "Authorization: Bearer $CAPTION_API_KEY" \ -H "Content-Type: application/json" \ -d '{"source_url":"https://cdn.example.com/clip.mp4","options":{"size":"large"}}' # -> 202 { "job_id": "..." } then poll /v1/jobs/{id} ``` ## Quickstart: upload a file 1. `POST /v1/videos` with `{"filename": "clip.mp4", "content_type": "video/mp4", "options": {...}}`. The 202 response includes `job_id` and an `upload` object with `method` (`PUT`), `url`, `headers`, `max_bytes` and `expires_in`. 2. Send the file bytes with an HTTP `PUT` to `upload.url` (relative URLs resolve against `https://api.caption.sh`) using `upload.headers`. The upload URL is signed, so no Authorization header is needed. The job starts automatically when the upload lands. 3. Poll `GET /v1/jobs/{id}` until `status` is `done` or `error`, then download from `GET /v1/jobs/{id}/result` (follow the redirect). ```bash curl -H "Authorization: Bearer $CAPTION_API_KEY" https://api.caption.sh/v1/jobs/$JOB_ID curl -L -H "Authorization: Bearer $CAPTION_API_KEY" https://api.caption.sh/v1/jobs/$JOB_ID/result -o out.mp4 ``` Send an `Idempotency-Key` header on `POST /v1/videos` to make retries safe. ## Job lifecycle ``` # File upload: awaiting_upload -> queued -> running -> done # result ready at /v1/jobs/{id}/result # source_url pull: queued -> running -> done # on failure: running -> error # generic message to you, detail logged server-side ``` Finished results are kept for 24 hours. ## Options Send these inside `options` on `POST /v1/videos`. All are optional. - `font` (font key (string); default `"luckiest-guy"`): Caption typeface. One of the bundled fonts (see Fonts below). Unknown key → `422`. - `caps` (boolean; default `true`): Uppercase all caption text. `false` keeps the transcription's natural casing: sentence case, proper nouns and punctuation as Deepgram returns them. - `size` (`"small"` | `"medium"` | `"large"` | number; default `"medium"`): Text height as a fraction of video height. Presets below. - `max_words` (integer ≥ 1 | `"auto"`; default `3`): Words per caption group. `1` = one word at a time. `"auto"` = pack as many as fit the size wrap budget. - `max_lines` (integer ≥ 1; default `2`): How tall captions may wrap. The block never exceeds this many lines. - `position` (`"top"` | `"center"` | `"bottom"` | number; default `"bottom"`): Vertical placement of the caption block. Number = fraction of height (0 = top edge, 1 = bottom edge) for the block's baseline. - `text_color` (hex string; default `"#FFFFFF"`): Colour of normal (non-active) words. - `highlight` (boolean; default `true`): Highlight the word currently being spoken. - `highlight_color` (hex string; default `"#00E676"`): Colour of the active word. Ignored when `highlight` is `false`. - `decoration` (`"outline"` | `"backdrop"` | `"none"`; default `"outline"`): How glyphs are made legible on any background: a stroke around each glyph, a filled rounded box behind the lines, or nothing. Outline and backdrop are mutually exclusive. - `decoration_color` (hex string; default `"#000000"`): Colour of the outline stroke (outline mode) or the backdrop box (backdrop mode). - `outline_width` (`"auto"` | integer (px); default `"auto"`): Stroke thickness. `"auto"` scales with the font size. `decoration: "outline"` only. - `backdrop_radius` (`"square"` | `"rounded"` | `"pill"` | number; default `"rounded"`): Corner roundness of the backdrop box, as a fraction of the box height (0 = square, 0.5 = pill). `decoration: "backdrop"` only. - `backdrop_opacity` (number 0–1; default `1.0`): Opacity of the backdrop box (0 = invisible, 1 = solid). Text stays fully opaque. `decoration: "backdrop"` only. - `max_tilt` (number 0–45; default `0`): Max random tilt in degrees. Each caption is rotated a random amount within `±max_tilt`, biased to alternate direction. `0` = always straight. - `censor_captions` (boolean; default `false`): Mask common profanity in the caption text (e.g. `F***`). - `censor_audio` (boolean; default `false`): Silence (mute) the audio over profane words. Independent of `censor_captions`. Presets: `size` small 0.045, medium 0.060, large 0.080 (fraction of video height). `position` top 0.18, center 0.55, bottom 0.86 (fraction of height). `backdrop_radius` square 0.0, rounded 0.30, pill 0.50. Captions always fit: text height is a fraction of the source height and wrapping uses a 90% width margin, so the same options produce proportional captions on vertical, square, or landscape video. Fonts (`font`): luckiest-guy, roboto, inter, merriweather, noto-serif, dynapuff, mouse-memoirs, jim-nightshade, imperial-script, lobster, unifrakturmaguntia, story-script, black-ops-one, arvo, ultra, michroma, shadows-into-light, fredoka. ## Errors | Status | When | |---|---| | 400 | Malformed body: send either a `filename` (file upload) or a `source_url` | | 401 | Missing or invalid credentials | | 402 | Prepaid balance too low to cover the estimated charge. Top up and retry; a paid render never starts on an empty balance | | 404 | Unknown job id, or a job that isn't yours | | 422 | `options` failed validation, or the video is too long (max 10 minutes), too high-resolution, or unreadable | | 429 | Throughput ceiling reached. Retry shortly | | 500 | Transcription or rendering failed. Generic message; details are logged server-side with a `request_id` | ## Pricing $0.10 per minute, rounded up to the nearest minute (a 1:30 video bills as 2 minutes, $0.20). Billing is a prepaid balance: top up credit in the dashboard and each job debits the real duration, measured server-side. Credit packs: $5, $10, $20, $50 (includes $5 bonus credit), $100 (includes $10 bonus credit). --- # MCP server caption.sh runs a hosted MCP server (streamable HTTP) at `https://mcp.caption.sh/mcp`. Authenticate with your caption.sh API key as a bearer token; billing and limits are the same as the REST API. No install. Claude Code: ```bash claude mcp add --transport http caption https://mcp.caption.sh/mcp \ --header "Authorization: Bearer $CAPTION_API_KEY" ``` Cursor (`~/.cursor/mcp.json`): ```json { "mcpServers": { "caption": { "url": "https://mcp.caption.sh/mcp", "headers": { "Authorization": "Bearer sk_live_..." } } } } ``` ## Tools - `caption_video`: transcribes speech word by word and burns animated captions into a new MP4 (same resolution and audio). Use for any request for captions, subtitles, or text on screen on a video with speech. Not for videos without speech. - `get_caption_job`: job status and a direct `download_url` for the MP4 once it's done. Can wait for the render. - `list_caption_styles`: every font with its look, the defaults, and ready-made recipes with exact options. - `list_caption_jobs`: recent jobs with status and cost. - `get_caption_balance`: prepaid balance and recent charges. ## Style recipes - `default`: White Inter caps, 3 words at a time, black outline, spoken word turns yellow, bottom of frame. Use when: No style direction was given. Safe on any footage and aspect ratio. - `short-form-punch`: One huge uppercase word at a time in Luckiest Guy, centred, the active word green and slightly enlarged, each word pops in. Use when: TikTok, Reels, Shorts and UGC ads where retention matters most. Vertical video with fast talking. - `karaoke`: Bold rounded caps, 3 words per group, centred, spoken word turns yellow and grows 25%. Use when: Creator talking-head clips, hooks, and anything where you want the viewer reading along word by word. - `clean-subtitles`: Sentence-case Inter on a semi-transparent black pill, up to two lines, bottom of frame, no colour change or motion. Use when: Podcasts, interviews, webinars, tutorials, landscape YouTube, and accessibility-first captions where calm readability beats energy. - `native-social-box`: Sentence-case dark Roboto text on solid white rounded boxes, like Instagram's own caption stickers. Use when: Bright or busy footage, lifestyle and vlog content, or when the platform-native look is wanted. - `neon-glow`: White caps in Michroma with a soft cyan glow, active word cyan, slow vertical drift. Use when: Tech, gaming, music, nightlife and premium product teasers on dark footage. - `sticker-tilt`: DynaPuff bubble caps, 2 words at a time, each group tilted a few degrees left or right and bouncing in. Use when: Playful, meme, kids, food and comedy clips. - `editorial`: Sentence-case Merriweather serif, white with a soft shadow, no word highlight, bottom of frame. Use when: Finance, news, documentary, luxury and thought-leadership content. - `brand-safe`: The default look with profanity masked in text (F***) and muted in the audio. Use when: Paid ads, brand accounts, and any clip that might contain swearing.