captionbob docs

Version 1.0.0
Contents ↓

HTTP API examples

Configure an effect

Each effect has settings, such as its font, its size and its colors. A version holds only the settings that you change. Each other setting keeps its default. 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 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

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: List the effects

The list gives the settings of each effect.

curl

ANSWER=$(curl -s "$CAPTIONBOB_URL/api/styles" \
  -H "Authorization: Bearer $CAPTIONBOB_KEY")
echo "$ANSWER"
STYLES=$(echo "$ANSWER" | jq -r '.styles')

Python

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

JavaScript

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

Answer: status 200.

{
  "styles": [
    {
      "name": "word_by_word",
      "has_emoji_layers": false,
      "settings": [
        {
          "name": "most_words",
          "type": "integer",
          "default": 3
        },
        {
          "name": "fewest_words",
          "type": "integer",
          "default": 1
        },
        {
          "name": "font_name",
          "type": "string",
          "default": "DejaVu Sans"
        },
        {
          "name": "font_size",
          "type": "integer",
          "default": 80
        },
        {
          "name": "bold",
          "type": "boolean",
          "default": true
        },
        {
          "name": "primary_color",
          "type": "string",
          "default": "#FFFFFF"
        },
        {
          "name": "stroke_color",
          "type": "string",
          "default": "#000000"
        },
        {
          "name": "stroke_width",
          "type": "number",
          "default": 6
        },
        {
          "name": "bottom_position",
          "type": "number",
          "default": 0.75
        },
        {
          "name": "side_margin",
          "type": "integer",
          "default": 54
        },
        {
          "name": "fade_seconds",
          "type": "number",
          "default": 0.1
        },
        {
          "name": "hold_seconds",
          "type": "number",
          "default": 0.6
        }
      ]
    }
  ]
}

The answer has 71 effects. This page shows the first 1.

Step 2: Find the effect word_by_word

Find the effect by its name. Each setting has its name, its type and its default value.

curl

STYLE=$(echo "$STYLES" | jq '.[] | select(.name == "word_by_word")')
echo "$STYLE"

Python

style = None
for item in styles:
    if item["name"] == "word_by_word":
        style = item
        break
print(style)

JavaScript

const style = styles.find((item) => item.name === "word_by_word");
console.log(JSON.stringify(style, null, 2));

Result:

{
  "name": "word_by_word",
  "has_emoji_layers": false,
  "settings": [
    {
      "name": "most_words",
      "type": "integer",
      "default": 3
    },
    {
      "name": "fewest_words",
      "type": "integer",
      "default": 1
    },
    {
      "name": "font_name",
      "type": "string",
      "default": "DejaVu Sans"
    },
    {
      "name": "font_size",
      "type": "integer",
      "default": 80
    },
    {
      "name": "bold",
      "type": "boolean",
      "default": true
    },
    {
      "name": "primary_color",
      "type": "string",
      "default": "#FFFFFF"
    },
    {
      "name": "stroke_color",
      "type": "string",
      "default": "#000000"
    },
    {
      "name": "stroke_width",
      "type": "number",
      "default": 6
    },
    {
      "name": "bottom_position",
      "type": "number",
      "default": 0.75
    },
    {
      "name": "side_margin",
      "type": "integer",
      "default": 54
    },
    {
      "name": "fade_seconds",
      "type": "number",
      "default": 0.1
    },
    {
      "name": "hold_seconds",
      "type": "number",
      "default": 0.6
    }
  ]
}

Step 3: 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 4: Add a version with changed settings

Put only the changed settings in settings. This version makes the words larger and yellow, and puts them higher.

curl

BODY=$(jq -n --argjson transcript "$TRANSCRIPT" '{
  "version": {
    "format_version": 1,
    "style": {
      "name": "word_by_word",
      "settings": {
        "font_size": 96,
        "primary_color": "#FFE600",
        "bottom_position": 0.7
      }
    },
    "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": "word_by_word",
                "settings": {
                    "font_size": 96,
                    "primary_color": "#FFE600",
                    "bottom_position": 0.7,
                },
            },
            "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: "word_by_word",
        settings: {
          font_size: 96,
          primary_color: "#FFE600",
          bottom_position: 0.7,
        },
      },
      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": 3,
  "versionCount": 3
}

Step 5: Check the version

The script route gives the ASS script that the Python library makes for a version. The live preview of the dashboard uses it. The style line of the script shows the font size 96 and the color &H0000E6FF. ASS writes a color as blue, green and red, so this color is #FFE600. The bottom position 0.7 gives the bottom margin 576 of the 1920 lines of the video.

curl

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

Python

response = requests.get(
    f"{CAPTIONBOB_URL}/api/videos/{video_id}/versions/{version_number}/script",
    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}/script`, { 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": 3,
  "assText": "[Script Info]\nScriptType: v4.00+\nPlayResX: 1080\nPlayResY: 1920\nWrapStyle: 0\nScaledBorderAndShadow: yes\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Caption,DejaVu Sans,96,&H0000E6FF,&H0000E6FF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,6,0,2,54,54,576,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:01.00,Caption,,0,0,0,,{\\fad(100,100)}Dirty {\\alpha&HFF&\\t(219,219,\\alpha&H00&)}facts {\\alpha&HFF&\\t(619,619,\\alpha&H00&)}about",
  "layers": []
}

The answer has 57 lines of assText. This page shows the first 14.

The shown part of assText, as lines:

[Script Info]
ScriptType: v4.00+
PlayResX: 1080
PlayResY: 1920
WrapStyle: 0
ScaledBorderAndShadow: yes

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Caption,DejaVu Sans,96,&H0000E6FF,&H0000E6FF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,6,0,2,54,54,576,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.00,0:00:01.00,Caption,,0,0,0,,{\fad(100,100)}Dirty {\alpha&HFF&\t(219,219,\alpha&H00&)}facts {\alpha&HFF&\t(619,619,\alpha&H00&)}about

Step 6: Add a version with a wrong setting

The server keeps a new version as it is. It does not check the settings. Here font_size is a text, but it must be a whole number.

curl

BODY=$(jq -n --argjson transcript "$TRANSCRIPT" '{
  "version": {
    "format_version": 1,
    "style": {"name": "word_by_word", "settings": {"font_size": "big"}},
    "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"
WRONG_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": "word_by_word", "settings": {"font_size": "big"}},
            "transcript": transcript,
            "plugins": [],
        },
    },
)
if not response.ok:
    raise RuntimeError(response.text)
answer = response.json()
print(answer)
wrong_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: "word_by_word", settings: { font_size: "big" } },
      transcript: transcript,
      plugins: [],
    },
  }),
});
if (!response.ok) {
  throw new Error(await response.text());
}
answer = await response.json();
console.log(JSON.stringify(answer, null, 2));
const wrongVersionNumber = answer.currentVersion;

Answer: status 201.

{
  "currentVersion": 4,
  "versionCount": 4
}

Step 7: See the fault of the wrong setting

The Python library checks the settings when it makes the script or burns the version. A wrong setting gives the error python_failed, and its message names the setting.

curl

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

Python

response = requests.get(
    f"{CAPTIONBOB_URL}/api/videos/{video_id}/versions/{wrong_version_number}/script",
    headers=HEADERS,
)
print(response.status_code)
answer = response.json()
print(answer)

JavaScript

response = await fetch(`${CAPTIONBOB_URL}/api/videos/${videoId}/versions/${wrongVersionNumber}/script`, { headers: HEADERS });
console.log(response.status);
answer = await response.json();
console.log(JSON.stringify(answer, null, 2));

Answer: status 502.

{
  "error": {
    "code": "python_failed",
    "message": "captionbob server: the Python action build-script failed: TypeError: The setting 'font_size' of WordByWordSettings must be a whole number, got str 'big'"
  }
}

Step 8: Undo the wrong version

The version before the current version becomes the current version again. A burn now burns the version with the good settings.

curl

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

Python

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

JavaScript

response = await fetch(`${CAPTIONBOB_URL}/api/videos/${videoId}/undo`, {
  method: "POST",
  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": 3,
  "versionCount": 4,
  "version": {
    "format_version": 1,
    "style": {
      "name": "word_by_word",
      "settings": {
        "font_size": 96,
        "primary_color": "#FFE600",
        "bottom_position": 0.7
      }
    },
    "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": []
  }
}

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