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) or word.
  • 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 PopKaraoke

Limits

  • 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 error field: 401 bad key, 402 quota or no subscription, 413 file too large, 422 unreadable video.