HTTP API examples
Use the emoji routes
Some effects draw emoji images next to the words. Their has_emoji_layers is true. The script route gives the emoji layers of a version, and the emoji route gives each image. A burn or a script of such an effect needs the emoji index folder, which the setting CAPTIONBOB_EMOJI_INDEX_FOLDER of the server names. 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
import { readFile, writeFile } from "node:fs/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: List the effects
The list tells for each effect whether it has emoji layers.
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 bouncy_emoji
Find the effect by its name. Its has_emoji_layers is true.
curl
STYLE=$(echo "$STYLES" | jq '.[] | select(.name == "bouncy_emoji")')
echo "$STYLE"
Python
style = None
for item in styles:
if item["name"] == "bouncy_emoji":
style = item
break
print(style)
JavaScript
const style = styles.find((item) => item.name === "bouncy_emoji");
console.log(JSON.stringify(style, null, 2));
Result:
{
"name": "bouncy_emoji",
"has_emoji_layers": true,
"settings": [
{
"name": "most_words",
"type": "integer",
"default": 6
},
{
"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": "#FF1911"
},
{
"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": "words_per_line",
"type": "integer",
"default": 3
},
{
"name": "secondary_color",
"type": "string",
"default": "#FFE434"
},
{
"name": "tertiary_color",
"type": "string",
"default": "#FF1911"
},
{
"name": "tilt_degrees",
"type": "number",
"default": 5
},
{
"name": "start_scale",
"type": "number",
"default": 0.8
},
{
"name": "grow_seconds",
"type": "number",
"default": 0.5
},
{
"name": "emoji_width_part",
"type": "number",
"default": 0.2
},
{
"name": "emoji_gap",
"type": "number",
"default": 20
},
{
"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 the effect
The new version becomes the current version.
curl
BODY=$(jq -n --argjson transcript "$TRANSCRIPT" '{
"version": {
"format_version": 1,
"style": {"name": "bouncy_emoji", "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": "bouncy_emoji", "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: "bouncy_emoji", 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": 4,
"versionCount": 4
}
Step 5: Get the emoji layers
The script route gives the ASS script and the emoji layers of the version. Each layer has the path of its image, the time at which the image starts to play, its first frame and its box on each frame.
curl
ANSWER=$(curl -s "$CAPTIONBOB_URL/api/videos/$VIDEO_ID/versions/$VERSION_NUMBER/script" \
-H "Authorization: Bearer $CAPTIONBOB_KEY")
echo "$ANSWER"
IMAGE_PATH=$(echo "$ANSWER" | jq -r '.layers[0].imagePath')
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)
image_path = answer["layers"][0]["imagePath"]
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));
const imagePath = answer.layers[0].imagePath;
Answer: status 200.
{
"number": 4,
"assText": "[Script Info]\nScriptType: v4.00+\nPlayResX: 1080\nPlayResY: 1920\nWrapStyle: 0\nScaledBorderAndShadow: yes",
"layers": [
{
"imagePath": "/api/emoji-image?path=gif%2F1faa8.gif",
"startSeconds": 3.24,
"firstFrame": 81,
"boxes": [
[
498,
1126,
81
],
[
498,
1122,
82
],
[
498,
1118,
84
]
]
}
]
}
The answer has 57 lines of assText. This page shows the first 6.
The answer has 15 layers. This page shows the first 1.
The answer has 57 boxes of the first layer. This page shows the first 3.
The shown part of assText, as lines:
[Script Info]
ScriptType: v4.00+
PlayResX: 1080
PlayResY: 1920
WrapStyle: 0
ScaledBorderAndShadow: yes
Step 6: Download an emoji image
The path of a layer is the path of its image on the server, with the query value path in the emoji index folder.
curl
curl -s -D - -o emoji.gif "$CAPTIONBOB_URL$IMAGE_PATH" \
-H "Authorization: Bearer $CAPTIONBOB_KEY"
Python
response = requests.get(
f"{CAPTIONBOB_URL}{image_path}",
headers=HEADERS,
)
if not response.ok:
raise RuntimeError(response.text)
with open("emoji.gif", "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}${imagePath}`, { headers: HEADERS });
if (!response.ok) {
throw new Error(await response.text());
}
await writeFile("emoji.gif", 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: image/gif
Content-Length: 2498364
Accept-Ranges: bytes