captionbob docs

Version 1.0.0
Contents ↓

HTTP API examples

Apply an effect

To apply an effect, add a new version of the video with the effect and burn the version. A version holds the effect with its settings, the transcript and the plugins. This page uses the video and the transcription of the Quickstart.

Before you start

Put your API key in place of <your API key>. The Quickstart shows how to make a key.

This page uses the video id from the page Quickstart.

This page uses the transcription id from the page Quickstart.

CAPTIONBOB_URL is the default address of the server. Change it when your server listens on another host or port. The code of each step runs after this first code. The curl code runs in bash and reads values from the answers with jq. The Python code uses the package requests. The JavaScript code runs in Node.js 22 or later as an ES module, such as a file example.mjs.

The answers on this page are real answers of a captionbob server, recorded on 2026-10-28. Your ids, names and times are different.

curl

CAPTIONBOB_URL="http://127.0.0.1:9400"
CAPTIONBOB_KEY="<your API key>"
# The video id from the page Quickstart
VIDEO_ID=1
# The transcription id from the page Quickstart
TRANSCRIPTION_ID=1

Python

import time

import requests

CAPTIONBOB_URL = "http://127.0.0.1:9400"
HEADERS = {"Authorization": "Bearer <your API key>"}
# The video id from the page Quickstart
video_id = 1
# The transcription id from the page Quickstart
transcription_id = 1

JavaScript

import { readFile, writeFile } from "node:fs/promises";
import { setTimeout } from "node:timers/promises";

const CAPTIONBOB_URL = "http://127.0.0.1:9400";
const HEADERS = { Authorization: "Bearer <your API key>" };
// The video id from the page Quickstart
const videoId = 1;
// The transcription id from the page Quickstart
const transcriptionId = 1;
let response;
let answer;

Step 1: Get the transcript

A new version needs the transcript of the video.

curl

ANSWER=$(curl -s "$CAPTIONBOB_URL/api/renders/$TRANSCRIPTION_ID/file" \
  -H "Authorization: Bearer $CAPTIONBOB_KEY")
echo "$ANSWER"
TRANSCRIPT="$ANSWER"

Python

response = requests.get(
    f"{CAPTIONBOB_URL}/api/renders/{transcription_id}/file",
    headers=HEADERS,
)
if not response.ok:
    raise RuntimeError(response.text)
answer = response.json()
print(answer)
transcript = answer

JavaScript

response = await fetch(`${CAPTIONBOB_URL}/api/renders/${transcriptionId}/file`, { headers: HEADERS });
if (!response.ok) {
  throw new Error(await response.text());
}
answer = await response.json();
console.log(JSON.stringify(answer, null, 2));
const transcript = answer;

Answer: status 200.

{
  "format_version": 1,
  "language": "en",
  "language_probability": 0.9990234375,
  "duration_seconds": 41.991875,
  "words": [
    {
      "text": "Dirty",
      "start_seconds": 0,
      "end_seconds": 0.22,
      "probability": 0.9716796875
    },
    {
      "text": "facts",
      "start_seconds": 0.22,
      "end_seconds": 0.62,
      "probability": 0.78515625
    },
    {
      "text": "about",
      "start_seconds": 0.62,
      "end_seconds": 1,
      "probability": 0.99951171875
    },
    {
      "text": "ancient",
      "start_seconds": 1,
      "end_seconds": 1.44,
      "probability": 0.9482421875
    }
  ]
}

The answer has 116 words. This page shows the first 4.

Step 2: Add a version with the effect

The new version becomes the current version. The answer gives the number of the current version and the number of versions.

curl

BODY=$(jq -n --argjson transcript "$TRANSCRIPT" '{
  "version": {
    "format_version": 1,
    "style": {"name": "pebble", "settings": {}},
    "transcript": $transcript,
    "plugins": []
  }
}')
ANSWER=$(curl -s -X POST "$CAPTIONBOB_URL/api/videos/$VIDEO_ID/versions" \
  -H "Authorization: Bearer $CAPTIONBOB_KEY" \
  -H "Content-Type: application/json" \
  -d "$BODY")
echo "$ANSWER"
VERSION_NUMBER=$(echo "$ANSWER" | jq -r '.currentVersion')

Python

response = requests.post(
    f"{CAPTIONBOB_URL}/api/videos/{video_id}/versions",
    headers=HEADERS,
    json={
        "version": {
            "format_version": 1,
            "style": {"name": "pebble", "settings": {}},
            "transcript": transcript,
            "plugins": [],
        },
    },
)
if not response.ok:
    raise RuntimeError(response.text)
answer = response.json()
print(answer)
version_number = answer["currentVersion"]

JavaScript

response = await fetch(`${CAPTIONBOB_URL}/api/videos/${videoId}/versions`, {
  method: "POST",
  headers: { ...HEADERS, "Content-Type": "application/json" },
  body: JSON.stringify({
    version: {
      format_version: 1,
      style: { name: "pebble", settings: {} },
      transcript: transcript,
      plugins: [],
    },
  }),
});
if (!response.ok) {
  throw new Error(await response.text());
}
answer = await response.json();
console.log(JSON.stringify(answer, null, 2));
const versionNumber = answer.currentVersion;

Answer: status 201.

{
  "currentVersion": 2,
  "versionCount": 2
}

Step 3: List the versions

The history of the video, the oldest version first.

curl

curl -s "$CAPTIONBOB_URL/api/videos/$VIDEO_ID/versions" \
  -H "Authorization: Bearer $CAPTIONBOB_KEY"

Python

response = requests.get(
    f"{CAPTIONBOB_URL}/api/videos/{video_id}/versions",
    headers=HEADERS,
)
if not response.ok:
    raise RuntimeError(response.text)
answer = response.json()
print(answer)

JavaScript

response = await fetch(`${CAPTIONBOB_URL}/api/videos/${videoId}/versions`, { headers: HEADERS });
if (!response.ok) {
  throw new Error(await response.text());
}
answer = await response.json();
console.log(JSON.stringify(answer, null, 2));

Answer: status 200.

{
  "currentVersion": 2,
  "versionCount": 2,
  "versions": [
    {
      "number": 1,
      "createdBy": "editor",
      "createdAt": "2026-10-28T01:33:34.443Z"
    },
    {
      "number": 2,
      "createdBy": "editor",
      "createdAt": "2026-10-28T01:34:09.760Z"
    }
  ]
}

Step 4: Get the version

One version with its JSON.

curl

curl -s "$CAPTIONBOB_URL/api/videos/$VIDEO_ID/versions/$VERSION_NUMBER" \
  -H "Authorization: Bearer $CAPTIONBOB_KEY"

Python

response = requests.get(
    f"{CAPTIONBOB_URL}/api/videos/{video_id}/versions/{version_number}",
    headers=HEADERS,
)
if not response.ok:
    raise RuntimeError(response.text)
answer = response.json()
print(answer)

JavaScript

response = await fetch(`${CAPTIONBOB_URL}/api/videos/${videoId}/versions/${versionNumber}`, { headers: HEADERS });
if (!response.ok) {
  throw new Error(await response.text());
}
answer = await response.json();
console.log(JSON.stringify(answer, null, 2));

Answer: status 200.

{
  "number": 2,
  "version": {
    "format_version": 1,
    "style": {
      "name": "pebble",
      "settings": {}
    },
    "transcript": {
      "format_version": 1,
      "language": "en",
      "language_probability": 0.9990234375,
      "duration_seconds": 41.991875,
      "words": [
        {
          "text": "Dirty",
          "start_seconds": 0,
          "end_seconds": 0.22,
          "probability": 0.9716796875
        },
        {
          "text": "facts",
          "start_seconds": 0.22,
          "end_seconds": 0.62,
          "probability": 0.78515625
        },
        {
          "text": "about",
          "start_seconds": 0.62,
          "end_seconds": 1,
          "probability": 0.99951171875
        }
      ]
    },
    "plugins": []
  },
  "createdBy": "editor",
  "createdAt": "2026-10-28T01:34:09.760Z"
}

The answer has 116 words. This page shows the first 3.

Step 5: Burn the version

A burn burns the current version.

curl

ANSWER=$(curl -s -X POST "$CAPTIONBOB_URL/api/videos/$VIDEO_ID/renders" \
  -H "Authorization: Bearer $CAPTIONBOB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kind": "burn"}')
echo "$ANSWER"
BURN_ID=$(echo "$ANSWER" | jq -r '.render.id')

Python

response = requests.post(
    f"{CAPTIONBOB_URL}/api/videos/{video_id}/renders",
    headers=HEADERS,
    json={"kind": "burn"},
)
if not response.ok:
    raise RuntimeError(response.text)
answer = response.json()
print(answer)
burn_id = answer["render"]["id"]

JavaScript

response = await fetch(`${CAPTIONBOB_URL}/api/videos/${videoId}/renders`, {
  method: "POST",
  headers: { ...HEADERS, "Content-Type": "application/json" },
  body: JSON.stringify({ kind: "burn" }),
});
if (!response.ok) {
  throw new Error(await response.text());
}
answer = await response.json();
console.log(JSON.stringify(answer, null, 2));
const burnId = answer.render.id;

Answer: status 201.

{
  "render": {
    "id": 3,
    "videoId": 1,
    "versionNumber": 2,
    "kind": "burn",
    "state": "queued",
    "requestedBy": "editor",
    "apiKeyName": "Quickstart",
    "doneSeconds": 0,
    "totalSeconds": null,
    "hasOutput": false,
    "errorMessage": null,
    "createdAt": "2026-10-28T01:34:09.766Z",
    "startedAt": null,
    "finishedAt": null
  }
}

Step 6: Wait for the burn

Read the render every 2 seconds until its state is done or failed.

curl

while true; do
  ANSWER=$(curl -s "$CAPTIONBOB_URL/api/renders/$BURN_ID" \
    -H "Authorization: Bearer $CAPTIONBOB_KEY")
  STATE=$(echo "$ANSWER" | jq -r '.render.state')
  echo "$STATE"
  if [ "$STATE" = "done" ] || [ "$STATE" = "failed" ]; then
    break
  fi
  sleep 2
done
echo "$ANSWER"

Python

while True:
    response = requests.get(
        f"{CAPTIONBOB_URL}/api/renders/{burn_id}",
        headers=HEADERS,
    )
    if not response.ok:
        raise RuntimeError(response.text)
    answer = response.json()
    state = answer["render"]["state"]
    print(state)
    if state == "done" or state == "failed":
        break
    time.sleep(2)
print(answer)

JavaScript

while (true) {
  response = await fetch(`${CAPTIONBOB_URL}/api/renders/${burnId}`, { headers: HEADERS });
  if (!response.ok) {
    throw new Error(await response.text());
  }
  answer = await response.json();
  console.log(answer.render.state);
  if (answer.render.state === "done" || answer.render.state === "failed") {
    break;
  }
  await setTimeout(2000);
}
console.log(JSON.stringify(answer, null, 2));

Answer: status 200, the last answer of the wait.

{
  "render": {
    "id": 3,
    "videoId": 1,
    "versionNumber": 2,
    "kind": "burn",
    "state": "done",
    "requestedBy": "editor",
    "apiKeyName": "Quickstart",
    "doneSeconds": 42,
    "totalSeconds": 42,
    "hasOutput": true,
    "errorMessage": null,
    "createdAt": "2026-10-28T01:34:09.766Z",
    "startedAt": "2026-10-28T01:34:10.404Z",
    "finishedAt": "2026-10-28T01:34:41.108Z"
  }
}

Step 7: Download the video

The output file of the burn is the video with the effect.

curl

curl -s -D - -o pebble.mp4 "$CAPTIONBOB_URL/api/renders/$BURN_ID/file" \
  -H "Authorization: Bearer $CAPTIONBOB_KEY"

Python

response = requests.get(
    f"{CAPTIONBOB_URL}/api/renders/{burn_id}/file",
    headers=HEADERS,
)
if not response.ok:
    raise RuntimeError(response.text)
with open("pebble.mp4", "wb") as output_file:
    output_file.write(response.content)
print(response.status_code, response.headers["Content-Type"], len(response.content))

JavaScript

response = await fetch(`${CAPTIONBOB_URL}/api/renders/${burnId}/file`, { headers: HEADERS });
if (!response.ok) {
  throw new Error(await response.text());
}
await writeFile("pebble.mp4", Buffer.from(await response.arrayBuffer()));
console.log(response.status, response.headers.get("content-type"));

Answer: status 200. The body is the file. Its headers are:

HTTP/1.1 200 OK
Content-Type: video/mp4
Content-Length: 24300517
Accept-Ranges: bytes