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 key

One command adds the server. Sign in once and it is yours in every project.

  1. 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. 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.

.mcp.json
{
  "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.
get_caption_job, completed
{
  "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.

every failure
{
  "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://..." }
}
CodeWhenRetryDo this
unauthenticatedMissing or invalid key or token. HTTP 401.NoCheck the Authorization header, or sign in again in your client.
job_not_foundUnknown jobId, or one that belongs to another account.NoUse the jobId from the response that created it.
invalid_sourceBoth or neither of jobId and inputUrl.NoSend exactly one.
invalid_requestMalformed arguments, or a dictionary or words list of the wrong shape.NoThe message names the field.
upload_incompletecaption_video called before the PUT finished.YesFinish the upload, then call again with the same jobId.
upload_url_expiredPUT attempted after the one-hour window.NoCall create_upload again for a fresh URL.
checksum_mismatchYou sent a sha256 and the uploaded bytes do not match it.NoUpload the file again, or omit the hash.
file_size_limit_exceededOver 2 GB, at create_upload or mid-stream.NoCompress or split the file.
invalid_mediaThe file could not be read as a video with usable audio.NoCheck it plays and has audio, then send it again.
probe_failedThe file could not be inspected.NoRe-encode to H.264 in an MP4 and try again.
unsupported_ratioSquare or an unusual aspect ratio.NoUse a 9:16 or 16:9 video. Details carry width and height.
unsupported_inputUnsupported codec, or frame rate above the ceiling.NoRe-encode to H.264 at 60 fps or below.
duration_limit_exceededOver 30 minutes.NoTrim the video.
trial_duration_limit_exceededOver 3 minutes on a trial account.NoUse a shorter video, or buy minutes to caption up to 30:00.
insufficient_balanceThe video costs more seconds than you have. Checked after the probe, before any speech.After a top-uptopUpUrl in the response goes straight to checkout.
trial_concurrency_limitA second job while a trial job is running.YesWait for the running job.
concurrency_limitMore than 3 of your jobs running, or the service is at capacity.YesWait for a job to finish and retry.
unsupported_source_urlA platform page rather than a video file, content that is not video, or an address we never connect to (private, local, metadata).NoPass a direct public https URL to the media file, or upload the file.
url_fetch_failedThe host did not resolve, connect, or answer 200; too slow to start or to finish; too many redirects.When transientCheck that the URL serves the file directly, or upload it.
rerenders_exhaustedrender_captions past the allowance.NoStart a new job.
rerender_window_expiredrender_captions more than 24 hours after completion.NoStart a new job.
processing_unavailableBoth speech vendors are down, or a job could not be dispatched.YesIf 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.