Which plan includes API access?
The Pro plan ($15/month, 40 videos a month). API keys and webhooks aren't available on Free or Basic.
Send a video, get it back with word-by-word captions burned in. Three requests: upload, check on it, download.
Using ChatGPT, Claude or Cursor? Copy the whole reference and paste it in, then ask for the integration you need.
curl -X POST "https://capsubs.com/api/v1/captions/auto" \ -H "X-API-Key: YOUR_API_KEY" \ -F "file=@video.mp4"
The response has the job "id".
curl -H "X-API-Key: YOUR_API_KEY" \ "https://capsubs.com/api/v1/jobs/JOB_ID"
Repeat every few seconds until "status" is "completed" (usually under a minute).
curl -H "X-API-Key: YOUR_API_KEY" -o captioned.mp4 \ "https://capsubs.com/api/v1/jobs/JOB_ID/download"
That's the job's output_url. Videos are kept for 7 days.
# pip install requests
import os
import time
import requests
BASE = "https://capsubs.com"
HEADERS = {"X-API-Key": os.environ["CAPSUBS_API_KEY"]}
# 1. Upload
with open("video.mp4", "rb") as f:
r = requests.post(f"{BASE}/api/v1/captions/auto", headers=HEADERS,
files={"file": f}, data={"style": "default"})
r.raise_for_status()
job = r.json()
# 2. Wait until it's done
while job["status"] not in ("completed", "failed"):
time.sleep(5)
r = requests.get(f"{BASE}/api/v1/jobs/{job['id']}", headers=HEADERS)
r.raise_for_status()
job = r.json()
if job["status"] == "failed":
raise SystemExit(job["error_message"])
# 3. Download
r = requests.get(job["output_url"], headers=HEADERS)
r.raise_for_status()
with open("captioned.mp4", "wb") as out:
out.write(r.content)Send your key in the X-API-Key header on every request. Keys start with vk_ and are shown only once, when you create them. API access is part of the Pro plan; there is one key per account. Changing or resetting your password deletes the key.
Each accepted video uses one video from your plan, the same as uploading in the dashboard. Failed jobs are refunded automatically, and rejected requests (4xx errors) use nothing.
/api/v1/captions/automultipart/form-data. Captions are transcribed automatically. Every field except the file is optional.fileThe video: MP4, MOV, AVI, MKV or WEBM, up to 95 MB in one request. For bigger files send upload_id instead (see Large files).
styleA preset ("default", "classic" or "boxed") or a JSON object of style options (see Caption styles).
tts_enabledAdd an AI voiceover read from the captions.
tts_voiceVoiceover voice: alloy, echo, fable, onyx, nova or shimmer.
mute_original_audioRemove the original audio (the voiceover stays if enabled).
enable_captionsBurn captions into the video. Set false for a voiceover-only video.
webhook_urlHTTPS URL to notify when this job finishes (unsigned). Register a webhook instead for signed events.
Response:
{
"id": "3f0c6c1e-8a51-4c47-9d0b-6b2f1f0f7a9e",
"status": "pending",
"progress": 0,
"message": "Job created. Processing will begin shortly.",
"created_at": "2026-01-20T12:00:00+00:00"
}/api/v1/jobs/{job_id}status is completed or failed.{
"id": "3f0c6c1e-8a51-4c47-9d0b-6b2f1f0f7a9e",
"status": "completed",
"progress": 100,
"original_filename": "video.mp4",
"output_url": "https://capsubs.com/api/v1/jobs/3f0c6c1e-8a51-4c47-9d0b-6b2f1f0f7a9e/download",
"duration_seconds": 31.2,
"word_count": 84,
"error_message": null,
"tts_enabled": true,
"created_at": "2026-01-20T12:00:00+00:00",
"processing_started_at": "2026-01-20T12:00:01+00:00",
"completed_at": "2026-01-20T12:00:40+00:00"
}pendingQueued. Processing starts shortly.processingTranscribing and rendering. progress goes from 0 to 100.completedDone. Download the video from output_url.failedSomething went wrong; error_message says what. The video is returned to your quota.reviewOnly for videos uploaded in the dashboard with caption review on: waiting for you to check the words. API jobs never use it./api/v1/jobs/{job_id}/downloadX-API-Key. This is the job's output_url. Kept for 7 days after upload./api/v1/jobs/limit (1–100, default 50), offset, status. Returns {"jobs": [...], "total", "limit", "offset"}.Pass style as a preset name, or as a JSON object to fine-tune it. Every key in the object is optional.
defaultBold white text, the spoken word highlighted, each line pops in.
classicStatic white text with a black outline, no highlight or animation.
boxedText on a solid box (outline_color), easiest to read on bright footage.
stylePreset to start from: default, classic or boxed. Other options override it.
font_size20 to 200, in pixels on a frame whose short side is 1080 (scaled to your video). Long words shrink to fit.
primary_colorText color as #RRGGBB.
highlight_colorColor of the word being spoken, as #RRGGBB.
highlight_wordsHighlight each word as it is spoken (false in classic).
animationpop (each line pops in) or none (none in classic).
outline_colorOutline color as #RRGGBB (the box color in boxed).
outline_width0 to 10 (4 in classic).
positionbottom, center or top.
max_words_per_line1 to 10 words shown at a time. Lines also break at the end of a sentence.
uppercaseShow captions in capitals.
curl -X POST "https://capsubs.com/api/v1/captions/auto" \
-H "X-API-Key: YOUR_API_KEY" \
-F "file=@video.mp4" \
-F 'style={"style":"boxed","position":"top","highlight_color":"#39FF14","max_words_per_line":2}'tts_enabled=true adds an AI voice reading the captions. Add mute_original_audio=true to replace the original sound, or enable_captions=false for a voiceover without captions.
alloyNeutral, balanced
echoMale, clear
fableBritish accent
onyxDeep male
novaFemale, warm
shimmerSoft, gentle
curl -X POST "https://capsubs.com/api/v1/captions/auto" \ -H "X-API-Key: YOUR_API_KEY" \ -F "file=@video.mp4" \ -F "tts_enabled=true" \ -F "tts_voice=nova" \ -F "mute_original_audio=true"
/api/v1/captions/custom/auto, plus captions: a JSON string of words with start and end times in seconds.curl -X POST "https://capsubs.com/api/v1/captions/custom" \
-H "X-API-Key: YOUR_API_KEY" \
-F "file=@video.mp4" \
-F 'captions={"words":[{"text":"Hello","start":0.0,"end":0.4},{"text":"world","start":0.4,"end":0.9}]}'/api/v1/captions/batchvideos field for each file (all of them together up to 95 MB). Other fields apply to every video, and the batch is only accepted if every video fits in your quota.curl -X POST "https://capsubs.com/api/v1/captions/batch" \
-H "X-API-Key: YOUR_API_KEY" \
-F "videos=@video1.mp4" \
-F "videos=@video2.mp4" \
-F "style=classic"
# Response: {"jobs": [{"id": ...}, {"id": ...}], "total": 2, "message": "..."}One request can carry up to 95 MB. For files up to 1 GB, send the file in parts and then pass its upload_id to /auto or /custom instead of file (or several as upload_ids to /batch).
/api/v1/uploads{"filename", "size"} (size in bytes). Returns upload_id, chunk_size and chunks./api/v1/uploads/{upload_id}/chunks/{index}index (0 to chunks − 1). A failed part can simply be sent again./api/v1/uploads/{upload_id}# Files over 95 MB (up to 1 GB): send them in parts, then caption the upload_id.
import os
import requests
BASE = "https://capsubs.com"
HEADERS = {"X-API-Key": os.environ["CAPSUBS_API_KEY"]}
path = "long-video.mp4"
# 1. Start an upload: tells you the part size and how many parts to send
r = requests.post(f"{BASE}/api/v1/uploads", headers=HEADERS,
json={"filename": os.path.basename(path), "size": os.path.getsize(path)})
r.raise_for_status()
upload = r.json() # {"upload_id": "...", "chunk_size": 52428800, "chunks": 3}
# 2. Send each part as the raw request body (parts are numbered from 0)
with open(path, "rb") as f:
for index in range(upload["chunks"]):
r = requests.put(f"{BASE}/api/v1/uploads/{upload['upload_id']}/chunks/{index}",
headers=HEADERS, data=f.read(upload["chunk_size"]))
r.raise_for_status()
# 3. Caption it: same fields as a normal upload, with upload_id instead of file
r = requests.post(f"{BASE}/api/v1/captions/auto", headers=HEADERS,
data={"upload_id": upload["upload_id"], "style": "default"})
r.raise_for_status()
job = r.json() # then poll and download as usualInstead of polling, register a URL to be called when a job completes or fails. It must be public HTTPS on port 443; up to 5 at a time.
/api/v1/webhooks/secret for checking signatures./api/v1/webhooks//api/v1/webhooks/{id}curl -X POST "https://capsubs.com/api/v1/webhooks/" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-server.com/capsubs-webhook", "events": ["job.completed", "job.failed"]}'
# The response includes "secret": use it to check each delivery's signature.Each event is POSTed as JSON:
{
"event": "job.completed",
"job_id": "3f0c6c1e-8a51-4c47-9d0b-6b2f1f0f7a9e",
"timestamp": "2026-01-20T12:00:40.123456",
"data": {
"status": "completed",
"output_url": "https://capsubs.com/api/v1/jobs/3f0c6c1e-8a51-4c47-9d0b-6b2f1f0f7a9e/download",
"output_filename": "processed_3f0c6c1e_input.mp4",
"word_count": 84,
"duration_seconds": 31.2
}
}
// job.failed: "data": { "status": "failed", "error": "..." }Every delivery has X-Capsubs-Timestamp and X-Capsubs-Signature: sha256= + HMAC-SHA256 of timestamp + "." + raw body with the webhook's secret. Deliveries time out after 10 seconds and aren't retried, so keep polling as a fallback. A per-job webhook_url gets the same payload, unsigned.
import hashlib, hmac
def is_from_capsubs(secret: str, headers, raw_body: bytes) -> bool:
timestamp = headers["X-Capsubs-Timestamp"]
expected = "sha256=" + hmac.new(
secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, headers["X-Capsubs-Signature"])Errors are JSON with a detail field explaining what to fix.
400Invalid input: wrong file type, style, captions, voice or webhook URL, or a video longer than 6 minutes. detail says which.401Missing, wrong or deleted API key.403Your plan doesn't include API access (Pro only), or the job belongs to another account.404Job, upload or webhook not found.409A chunked upload is busy (a part is still being written). Retry in a moment.413Request larger than 95 MB. Use a chunked upload.429No videos left this period (detail is an object with used, limit and resets_at), or too many requests (detail is a message). Wait and retry.503Processing queue briefly unavailable. Nothing was charged; retry.The Pro plan ($15/month, 40 videos a month). API keys and webhooks aren't available on Free or Basic.
Usually under a minute for a short clip. Poll the job every 5 seconds, or register a webhook to be told when it's done.
Polling is the simplest and always works. Webhooks save requests in production, but deliveries aren't retried, so keep polling as a fallback.
Yes. Click "Copy for LLM" and paste the reference into ChatGPT, Claude or Cursor, or point the assistant at capsubs.com/docs/llm.txt.
Create your key on the Pro plan and caption your first video in a few minutes.
Get your API key View pricing and plan limits