API documentation

Capsubs API

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.

Quick start

  1. 0
    Create your key in Dashboard → API Keys (Pro plan).
  2. 1

    Upload a video

    bash
    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".

  3. 2

    Check on it

    bash
    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).

  4. 3

    Download the captioned video

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

The same in code

python
# 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)

Authentication

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.

Upload a video

POST/api/v1/captions/auto
Send the video as multipart/form-data. Captions are transcribed automatically. Every field except the file is optional.
file
file · required

The video: MP4, MOV, AVI, MKV or WEBM, up to 95 MB in one request. For bigger files send upload_id instead (see Large files).

style
string · default default

A preset ("default", "classic" or "boxed") or a JSON object of style options (see Caption styles).

tts_enabled
boolean · default false

Add an AI voiceover read from the captions.

tts_voice
string · default alloy

Voiceover voice: alloy, echo, fable, onyx, nova or shimmer.

mute_original_audio
boolean · default false

Remove the original audio (the voiceover stays if enabled).

enable_captions
boolean · default true

Burn captions into the video. Set false for a voiceover-only video.

webhook_url
string · optional

HTTPS URL to notify when this job finishes (unsigned). Register a webhook instead for signed events.

Response:

json
{
  "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"
}

Jobs

GET/api/v1/jobs/{job_id}
Poll every 5 seconds until status is completed or failed.
json
{
  "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.
GET/api/v1/jobs/{job_id}/download
The finished MP4, with your X-API-Key. This is the job's output_url. Kept for 7 days after upload.
GET/api/v1/jobs/
Your jobs, newest first. Query: limit (1–100, default 50), offset, status. Returns {"jobs": [...], "total", "limit", "offset"}.

Caption styles

Pass style as a preset name, or as a JSON object to fine-tune it. Every key in the object is optional.

default

Bold white text, the spoken word highlighted, each line pops in.

classic

Static white text with a black outline, no highlight or animation.

boxed

Text on a solid box (outline_color), easiest to read on bright footage.

style
string · default default

Preset to start from: default, classic or boxed. Other options override it.

font_size
integer · default 110

20 to 200, in pixels on a frame whose short side is 1080 (scaled to your video). Long words shrink to fit.

primary_color
string · default #FFFFFF

Text color as #RRGGBB.

highlight_color
string · default #FFD700

Color of the word being spoken, as #RRGGBB.

highlight_words
boolean · default true

Highlight each word as it is spoken (false in classic).

animation
string · default pop

pop (each line pops in) or none (none in classic).

outline_color
string · default #000000

Outline color as #RRGGBB (the box color in boxed).

outline_width
integer · default 6

0 to 10 (4 in classic).

position
string · default bottom

bottom, center or top.

max_words_per_line
integer · default 3

1 to 10 words shown at a time. Lines also break at the end of a sentence.

uppercase
boolean · default true

Show captions in capitals.

bash
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}'

Voiceover and audio

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.

alloy

Neutral, balanced

echo

Male, clear

fable

British accent

onyx

Deep male

nova

Female, warm

shimmer

Soft, gentle

bash
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"

Custom captions and batch

POST/api/v1/captions/custom
Use your own words instead of transcription. Same fields as /auto, plus captions: a JSON string of words with start and end times in seconds.
bash
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}]}'
POST/api/v1/captions/batch
Up to 5 videos at once: repeat the videos 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.
bash
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": "..."}

Large files (over 95 MB)

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

POST/api/v1/uploads
JSON {"filename", "size"} (size in bytes). Returns upload_id, chunk_size and chunks.
PUT/api/v1/uploads/{upload_id}/chunks/{index}
The raw bytes of part index (0 to chunks − 1). A failed part can simply be sent again.
DELETE/api/v1/uploads/{upload_id}
Cancel. Unfinished uploads are removed after a day.
python
# 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 usual

Webhooks

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

POST/api/v1/webhooks/
Register. The response includes a secret for checking signatures.
GET/api/v1/webhooks/
List your webhooks, with their secrets.
DELETE/api/v1/webhooks/{id}
Stop sending to it.
bash
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:

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.

python
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 and limits

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.
  • API access is on the Pro plan: 40 videos a month, videos up to 6 minutes.
  • One request (including a whole batch) can be up to 95 MB. Files up to 1 GB go through a chunked upload.
  • Up to 5 videos per batch.
  • Finished videos are deleted 7 days after upload: download them before then.
  • One API key per account, up to 5 active webhooks.
  • About 20 requests a second per IP. Polling every 5 seconds is plenty.

Frequently asked questions

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.

How long does a video take?

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.

Should I use polling or webhooks?

Polling is the simplest and always works. Webhooks save requests in production, but deliveries aren't retried, so keep polling as a fallback.

Can an AI assistant write the integration for me?

Yes. Click "Copy for LLM" and paste the reference into ChatGPT, Claude or Cursor, or point the assistant at capsubs.com/docs/llm.txt.

Ready to integrate?

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