Docs
Connect, then caption.
Pick the tool you are sitting in. Interactive clients sign in through the browser; automation tools use a key from the dashboard. Either way it is the same four tools, and a video at a URL is one call.
- MCP
- https://api.captionpipe.com/mcp
- REST
- https://api.captionpipe.com/v1
Claude Code
Sign in, or a keyOne command adds the server. Sign in once and it is yours in every project.
- 1
Add the server and sign in
claude mcp add --transport http captionpipe https://api.captionpipe.com/mcp claude > /mcp # choose captionpipe, finish sign-in in the browser
- 2
Ask
Caption https://cdn.acme.com/clip.mp4 with the highlight preset. Get "Acme" and "SK-7" spelled right, and give me the MP4 and the SRT.
A file on your machine
Give the path. Claude Code calls create_upload, sends the bytes, then caption_video. You do none of that yourself.
Other ways in
Instead of the command in step 1
Or check it into the repo
Claude Code skips a remote entry with no type and warns that it needs one. Most snippets online omit it.
{
"mcpServers": {
"captionpipe": { "type": "http", "url": "https://api.captionpipe.com/mcp" }
}
}Instead of signing in
With an API key instead
Create a key in the dashboard under Connect. It is shown once.
claude mcp add --transport http captionpipe https://api.captionpipe.com/mcp \ --header "Authorization: Bearer $CAPTIONPIPE_API_KEY"
Verified against Claude Code 2.1.263 on 8 September 2026.
Tools
The four tools
REST paths are /v1/create_upload, /v1/caption_video, /v1/jobs/{id} and /v1/render_captions. The MCP tools take the identical JSON as arguments.
- create_upload{ contentLength?, sha256?, idempotencyKey? }
→ { jobId, uploadUrl, expiresAt }
Only for a file on your side. Returns a PUT-only URL for one object, valid for an hour. Declare contentLength if you know the size in bytes and the URL is bound to it; leave it out and the 2 GB cap is checked when caption_video runs instead. The object is sealed when caption_video runs. If your video is already at a URL, skip this tool entirely.
- caption_video{ jobId | inputUrl, preset, highlightColor?, dictionary?, language?, idempotencyKey? }
→ { jobId, status: "probing" }
Exactly one source: the jobId from create_upload, or a direct link to a video file. Platform pages such as YouTube or TikTok are refused. preset is highlight, clean or boxed. dictionary is up to 1,000 terms of up to six words each, held for this job and deleted with its files. language is auto or a BCP-47 tag. highlightColor is a hex RGB for the spoken word and applies to the highlight preset only; the default is the CaptionPipe accent.
- get_caption_job{ jobId }
→ the job, with artifacts once completed
Poll it. retryAfterSeconds tells you when to come back while the job is running. Once completed, artifacts are signed links valid until expiresAt, 24 hours after completion; after that the job answers job_not_found. After a render_captions call, render says which version was asked for and whether it is still rendering, done or failed; the links always belong to the newest finished version. Every response carries your balance, so an agent never discovers an empty balance by hitting an error.
- render_captions{ jobId, words, preset?, highlightColor?, idempotencyKey? }
→ the job, with render.status accepted
Send the edited words.json and we burn the video again from exactly those words. No speech model runs. Optionally change the preset or the highlight colour. Free, 3 times per job on a paid account and 1 on the trial, within 24 hours of completion; the window and the files expire together. Poll get_caption_job until render.status is completed, when the links switch to the new version. Without an idempotencyKey every call uses one re-render from the allowance.
Status
Job status
One response shape everywhere: MCP, REST and the dashboard read the same object.
- upload_pending
- create_upload issued a URL; the bytes have not arrived yet.
- probing
- We are checking that it is a video within the limits. Nothing is charged yet.
- reserved
- Duration known; the seconds are held on your balance and speech is about to start.
- processing
- Speech and burn. retryAfterSeconds says when to poll again.
- completed
- Artifacts are ready. chargedSeconds is final and never exceeds the hold.
- failed
- A typed error. The hold is released and nothing is charged.
{
"jobId": "job_9f2",
"status": "completed",
"preset": "highlight",
"durationSeconds": 42,
"chargedSeconds": 42,
"retryAfterSeconds": null,
"balance": {
"remainingSeconds": 258,
"trialRemainingSeconds": 0,
"paidRemainingSeconds": 258,
"reservedSeconds": 0,
"topUpUrl": null
},
"balanceWarning": null,
"rerendersRemaining": 3,
"dictionaryCapacityApplied": 1000,
"artifacts": {
"mp4": { "url": "https://...", "expiresAt": "2026-09-04T12:00:00Z" },
"srt": { "url": "https://...", "expiresAt": "..." },
"vtt": { "url": "https://...", "expiresAt": "..." },
"ass": { "url": "https://...", "expiresAt": "..." },
"wordsJson": { "url": "https://...", "expiresAt": "..." }
},
"expiresAt": "2026-09-04T12:00:00Z",
"error": null,
"render": null
}Errors
Errors
One envelope, a machine-readable code, and a message written to be the fix. No error is ever charged: the hold, if there was one, is returned.
{
"ok": false,
"error": {
"code": "insufficient_balance",
"message": "This video needs 0:42 and 0:18 is available. No job was started.",
"retryable": false,
"details": { "requiredSeconds": 42, "remainingSeconds": 18 }
},
"balance": { "remainingSeconds": 18, "reservedSeconds": 0, "topUpUrl": "https://..." }
}| Code | When | Retry | Do this |
|---|---|---|---|
| unauthenticated | Missing or invalid key or token. HTTP 401. | No | Check the Authorization header, or sign in again in your client. |
| job_not_found | Unknown jobId, or one that belongs to another account. | No | Use the jobId from the response that created it. |
| invalid_source | Both or neither of jobId and inputUrl. | No | Send exactly one. |
| invalid_request | Malformed arguments, or a dictionary or words list of the wrong shape. | No | The message names the field. |
| upload_incomplete | caption_video called before the PUT finished. | Yes | Finish the upload, then call again with the same jobId. |
| upload_url_expired | PUT attempted after the one-hour window. | No | Call create_upload again for a fresh URL. |
| checksum_mismatch | You sent a sha256 and the uploaded bytes do not match it. | No | Upload the file again, or omit the hash. |
| file_size_limit_exceeded | Over 2 GB, at create_upload or mid-stream. | No | Compress or split the file. |
| invalid_media | The file could not be read as a video with usable audio. | No | Check it plays and has audio, then send it again. |
| probe_failed | The file could not be inspected. | No | Re-encode to H.264 in an MP4 and try again. |
| unsupported_ratio | Square or an unusual aspect ratio. | No | Use a 9:16 or 16:9 video. Details carry width and height. |
| unsupported_input | Unsupported codec, or frame rate above the ceiling. | No | Re-encode to H.264 at 60 fps or below. |
| duration_limit_exceeded | Over 30 minutes. | No | Trim the video. |
| trial_duration_limit_exceeded | Over 3 minutes on a trial account. | No | Use a shorter video, or buy minutes to caption up to 30:00. |
| insufficient_balance | The video costs more seconds than you have. Checked after the probe, before any speech. | After a top-up | topUpUrl in the response goes straight to checkout. |
| trial_concurrency_limit | A second job while a trial job is running. | Yes | Wait for the running job. |
| concurrency_limit | More than 3 of your jobs running, or the service is at capacity. | Yes | Wait for a job to finish and retry. |
| unsupported_source_url | A platform page rather than a video file, content that is not video, or an address we never connect to (private, local, metadata). | No | Pass a direct public https URL to the media file, or upload the file. |
| url_fetch_failed | The host did not resolve, connect, or answer 200; too slow to start or to finish; too many redirects. | When transient | Check that the URL serves the file directly, or upload it. |
| rerenders_exhausted | render_captions past the allowance. | No | Start a new job. |
| rerender_window_expired | render_captions more than 24 hours after completion. | No | Start a new job. |
| processing_unavailable | Both speech vendors are down, or a job could not be dispatched. | Yes | If the call was refused, retry it with the same idempotencyKey after retryAfterSeconds. If a job came back failed, send it again with a new one. |
Limits
Limits
Every one of these is checked before you are charged, so a file we cannot take costs you nothing.
- Length
- Up to 30 minutes
- Size
- Up to 2 GB in
- Output
- 1080p H.264, 60 fps ceiling
- Shape
- Portrait first. 16:9 supported. Square is refused before you are charged
- Files
- Available for 24 hours after a job finishes
- Per call
- One video
Billing
Billing and refunds
- Every job is charged by the whole second of the video's duration, rounded up, against a prepaid balance.
- Trial seconds are drawn before paid seconds. One job can span both.
- When a job starts we hold the probed seconds; the dashboard shows the hold as reserved. On completion the hold becomes the charge, never more. On failure the hold is returned.
- $10 buys 250 minutes, 1 to 10 blocks per checkout. No subscription, nothing renews, paid minutes never expire.
- Every response carries balance.topUpUrl. It is null until you are low or blocked, then it is a checkout link an agent can hand to you.
- Unused paid minutes are refundable on request, net of the card fee Stripe keeps. The trial is not.
Troubleshooting
Troubleshooting
- The job sits in probing or processing
- Poll get_caption_job and honor retryAfterSeconds. A job that cannot complete is failed within minutes with the hold returned; it is never stuck forever.
- The artifact link returns 403
- Links expire 24 hours after completion, and so do the files. Start a new job.
- My client lists no tools
- Reload or restart the client after editing its config, and check the config shape for that client above. Cursor and VS Code blocks are not interchangeable.
- Sign-in opens but the client never connects
- Finish the browser step on the same machine the client runs on. If it still fails, create an API key in the dashboard and use the key form of the config.
- A name is spelled wrong
- Send it in dictionary next time. For this job, edit words.json and call render_captions; the re-render is free.
- The response says dictionaryCapacityApplied: 0
- Our primary speech vendor was unavailable and the fallback ran without the dictionary. Re-render with the corrected words, or re-run the job later.