CaptionPass API v1
Programmatic caption QA for Pilot accounts (same limits as planned Pro: up to 99,999 successful conversions per UTC day via this API). Create API keys under Dashboard → Account. Only HTTP 200 responses count toward the daily cap; failed attempts do not decrement remainingApiRunsToday.
Authentication
Send your secret key on every request (TLS required in production). Do not put API keys in query strings, logs, or client-side bundles served to browsers.
Authorization: Bearer cp_live_...
Creating and revoking keys uses the logged-in web session at Dashboard → Account (/api/account/api-keys with cookies). That flow is separate from the Bearer contract below, which is intended for server-to-server integrations.
POST /api/v1/process
This endpoint accepts POST only. There is no GET health check on /api/v1/process.
Content-Type: multipart/form-data (browser or library-generated boundary).
Form fields
file— required. UTF-8 caption file (SRT, VTT, SBV, ASS, TTML, or JSON IR depending on input). Same rules as the public converter.preset— optional; defaultyoutube. See the preset table below.
Limits and rate control
- Upload size. Each part must be at most 2048 KB (about 2 MiB). The default is 2 MiB unless the host sets
CAPTIONPASS_MAX_FILE_BYTES(legacy:CAPTIONDELIVERY_MAX_FILE_BYTES). Oversized files return400withbad_requestand a message mentioning the KB limit. - Declared body size. If the client sends a numeric
Content-Lengthlarger than about 4.25 MiB (twice the file cap plus 256 KiB headroom), the server rejects the request with400andpayload_too_largebefore reading the body. - Burst. Each API key is limited to roughly 45 successful or failed POSTs per rolling 60 second window per warm server instance (in-memory counter). Configure with
CAPTIONPASS_V1_API_BURST_PER_MINUTE(integer, clamped to 1–500). Excess traffic returns429withrate_limited. - Daily quota. Up to 99,999 successful conversions per UTC calendar day (see intro). When exhausted,
429withquota_exceeded. - 429 behavior. Responses do not include
Retry-After. Back off with exponential delay and respect both burst and daily limits. - Client timeouts. Use a generous HTTP read timeout (for example 120 seconds) when files are large; the server may run in a serverless environment with its own execution ceiling.
- CORS. This route is not configured for cross-origin browser access. Call it from your backend or from the same site origin; do not expose live API keys in front-end JavaScript.
Presets
The preset field selects platform rules and output format. Default is youtube.
| preset | Output | Label / intent |
|---|---|---|
| youtube | srt | YouTubeSRT output, conservative line length and reading speed; strips styling tags YouTube ignores. |
| tiktok | srt | TikTok / ShortsSRT optimized for vertical shorts: shorter lines, faster pace, single-line cues preferred. |
| html5 | vtt | HTML5 / WebVTT playerWebVTT output with required header, normalized timestamps, and conservative styling. |
| generic | srt | Generic safeThe safest possible output: SRT, plain text only, broad reading-speed defaults. |
| lms | ttml | LMS / TTML (IMSC1-friendly)TTML 1.0 / IMSC1-friendly XML output for LMS players and accessibility pipelines. |
| developer-json | json | Developer JSON (IR)Stable JSON timeline (CaptionPass IR v1) for round-tripping, automation, and future API use. |
Base URL (production site or CloudFront origin):
https://www.captionpass.com/api/v1/process
curl
curl -sS -X POST "https://www.captionpass.com/api/v1/process" \ -H "Authorization: Bearer YOUR_API_KEY" \ -F "file=@./captions.srt" \ -F "preset=youtube"
HTTPie
http --timeout 120 --form POST "https://www.captionpass.com/api/v1/process" \ "Authorization: Bearer YOUR_API_KEY" \ file@./captions.srt \ preset=youtube
Node.js (fetch)
import fs from "node:fs";
const apiKey = process.env.CAPTIONPASS_API_KEY;
const form = new FormData();
form.set("file", new Blob([fs.readFileSync("captions.srt", "utf8")], { type: "text/plain" }), "captions.srt");
form.set("preset", "youtube");
const res = await fetch("https://www.captionpass.com/api/v1/process", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
body: form,
});
const body = await res.json();
console.log(res.status, body);Python
import os, requests
url = "https://www.captionpass.com/api/v1/process"
headers = {"Authorization": f"Bearer {os.environ['CAPTIONPASS_API_KEY']}"}
files = {"file": open("captions.srt", "rb")}
data = {"preset": "youtube"}
r = requests.post(url, headers=headers, files=files, data=data, timeout=120)
print(r.status_code, r.json())Success response (200)
Content-Type: application/json. Typical headers: Cache-Control: no-store, X-Content-Type-Options: nosniff.
The JSON body merges the pipeline result (report + output from the caption engine) with a quota field:
| Field | Type | Description |
|---|---|---|
| report | object | inputFormat, outputFormat, preset, diagnostics[], fixes[], qaScore (0–100), cueCount, totalDurationMs. |
| output | string | Rendered caption text in report.outputFormat (e.g. SRT, VTT, TTML, JSON depending on preset). |
| remainingApiRunsToday | number | Successful v1 calls remaining this UTC calendar day after this response (0 when you are on the last allowed run). |
Diagnostics and fixes
Each entry in report.diagnostics includes code, severity (error, warn, or info), message, and optionally line or cueIndex. report.fixes entries include code, message, optional cueIndex, and optional before / after snippets for traceability.
Common diagnostic code values include:
| code | Typical meaning |
|---|---|
| empty_input | Parser: file was empty. |
| no_cues | Parser: no valid cues found. |
| invalid_timestamp | Parser: malformed time range. |
| missing_webvtt_header | VTT: WEBVTT missing on line 1. |
| overlapping_cues | Rule: two cues overlap in time. |
| reading_speed_high | Rule: CPS above preset maximum for a cue. |
| too_many_lines | Rule: cue exceeds max lines per cue. |
| strip_styling | Rule: markup removed per preset. |
Example (truncated)
{
"report": {
"inputFormat": "srt",
"outputFormat": "srt",
"preset": "youtube",
"diagnostics": [
{ "code": "overlapping_cues", "severity": "warn", "message": "…", "cueIndex": 1 }
],
"fixes": [
{
"code": "wrap_long_line",
"message": "Wrapped cue 0 to 42 chars per line.",
"cueIndex": 0,
"before": "…",
"after": "…"
}
],
"qaScore": 92,
"cueCount": 12,
"totalDurationMs": 145000
},
"output": "1\n00:00:01,000 --> 00:00:04,000\n…",
"remainingApiRunsToday": 99887
}Error responses (v1)
All v1 errors use this JSON shape (human-readable message may vary slightly; rely on code for branching):
{ "error": { "code": "unauthorized", "message": "…" } }| HTTP | error.code | When |
|---|---|---|
| 401 | unauthorized | Missing/invalid Bearer token, or unknown/revoked API key. |
| 403 | forbidden | Key resolved but account is not allowed API access (entitlement). |
| 429 | rate_limited | Per-key burst limit (too many requests in a short window). |
| 429 | quota_exceeded | Daily successful conversion cap reached for your account (UTC day). |
| 400 | bad_request | Malformed multipart, missing file, bad preset, empty file, decode error, etc. |
| 400 | payload_too_large | Declared Content-Length exceeds the allowed request ceiling (before body read). |
| 413 | payload_too_large | Engine refused input as too large (e.g. cue count cap). |
| 422 | processing_failed | Validation/processing error; message explains the failure. |
| 503 | unavailable | Server misconfiguration (API data store not available). |
Example error bodies
401 unauthorized
{
"error": {
"code": "unauthorized",
"message": "Invalid or revoked API key."
}
}429 rate_limited
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Slow down and try again shortly."
}
}429 quota_exceeded
{
"error": {
"code": "quota_exceeded",
"message": "Daily API conversion limit reached. Try again tomorrow."
}
}422 processing_failed
{
"error": {
"code": "processing_failed",
"message": "Could not parse cues: …"
}
}Public web converter (comparison)
POST /api/process (browser, no API key) returns the same success shape: report, output, and remainingFreeRunsToday instead of remainingApiRunsToday. Many errors from that route use a flat JSON body like { "error": "string message" } (not the nested error.code object). Prefer /api/v1/process for integrations.
Versioning
Paths are versioned (/api/v1/...). Non-breaking additions may land without a new version; incompatible changes will ship under /api/v2 with advance notice when possible.