Add burned-in captions to a video over HTTP
Three endpoints: upload a file, start a job, poll for the MP4. Transcription runs on Whisper, rendering uses the same engine as the editor, so the output matches what you'd get by hand. Built for agents and scripts.
Authentication
Create a key from the account menu in the editor. It's shown once. Send it on every request:
Authorization: Bearer sk_live_…Keys inherit your plan. Rendering requires an active subscription; transcription draws on the same monthly minutes as the editor.
Quickstart — local file
import { readFileSync } from "node:fs";
import { put } from "@vercel/blob/client";
const KEY = process.env.SOCAPTIONS_API_KEY;
const auth = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" };
// 1. Get a scoped upload token for your local file.
const upload = await fetch("https://socaptions.com/api/v1/uploads", {
method: "POST",
headers: auth,
body: JSON.stringify({ filename: "clip.mp4", contentType: "video/mp4" }),
}).then((r) => r.json());
// 2. Send the bytes straight to storage.
const blob = await put(upload.pathname, readFileSync("clip.mp4"), {
access: "public",
token: upload.token,
contentType: "video/mp4",
});
// 3. Transcribe + render.
const job = await fetch("https://socaptions.com/api/v1/jobs", {
method: "POST",
headers: auth,
body: JSON.stringify({ videoUrl: blob.url, styleName: "Classic Shadow" }),
}).then((r) => r.json());
// 4. Poll until the MP4 is ready.
let result;
do {
await new Promise((r) => setTimeout(r, 3000));
result = await fetch(`https://socaptions.com/api/v1/jobs/${job.jobId}`, {
headers: auth,
}).then((r) => r.json());
} while (result.status === "processing");
console.log(result.videoUrl);The upload token is scoped to one path, expires in an hour, and caps at 25 MB. The source file is deleted when you poll a finished job.
Already have a public URL
Skip the upload step entirely and post the URL:
curl -X POST https://socaptions.com/api/v1/jobs \
-H "Authorization: Bearer $SOCAPTIONS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"videoUrl": "https://example.com/clip.mp4",
"styleName": "Classic Shadow",
"mode": "segment",
"fontSize": 18,
"textY": 80,
"animation": { "speed": 1, "reveal": "word", "type": "pop", "maxWordsPerLine": 4 }
}'Endpoints
POST /api/v1/uploads
Body: filename, contentType (video/mp4 or video/quicktime). Returns a token and pathname for a direct upload.
POST /api/v1/jobs
Only videoUrl is required. Everything else falls back to the editor defaults. Returns 202 with a jobId. Takes up to a few minutes because transcription happens inline.
subtitles— supply your own entries to skip transcription entirely.mode—segment(default) orword.language— ISO-639-1 hint for Whisper.styleName,customStyle,fontSize,lineHeight,textX,textY,containerWidthPercent,animation— the same knobs as the editor sidebar.videoWidth,videoHeight,videoDurationSeconds— only needed if we can't read them from the container.
GET /api/v1/jobs/{id}
Returns status (processing / done / failed), progress (0–1) and, once done, videoUrl. The rendered MP4 is deleted after 24 hours — download it.
Caption styles
Pass any of these as styleName:
Classic ShadowBox BackgroundEmphasisBold OutlineNeon GlowCinematicTypewriter3D PopKaraokeLimits
- Source files up to 25 MB, MP4 or MOV.
- Transcription minutes come out of your monthly plan quota.
- Uploads are deleted after rendering; renders expire after 24 hours.
- Errors are JSON with an
errorfield:401bad key,402quota or no subscription,413file too large,422unreadable video.