HTTP API examples
Track a task
A task of the server is a render: a transcription or a burn. The server queues each render and answers at once. Then you read the render until it ends. This page burns the current version of the video 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.
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
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
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;
let response;
let answer;
Step 1: Start a burn
The state of a new render is queued.
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": 4,
"videoId": 1,
"versionNumber": 4,
"kind": "burn",
"state": "queued",
"requestedBy": "editor",
"apiKeyName": "Quickstart",
"doneSeconds": 0,
"totalSeconds": null,
"hasOutput": false,
"errorMessage": null,
"createdAt": "2026-10-28T01:34:56.646Z",
"startedAt": null,
"finishedAt": null
}
}
Step 2: Start a second render of the same video
A video has one render at a time. A second render while one is queued or running gives the error render_in_progress.
curl
curl -s -X POST "$CAPTIONBOB_URL/api/videos/$VIDEO_ID/renders" \
-H "Authorization: Bearer $CAPTIONBOB_KEY" \
-H "Content-Type: application/json" \
-d '{"kind": "transcribe"}'
Python
response = requests.post(
f"{CAPTIONBOB_URL}/api/videos/{video_id}/renders",
headers=HEADERS,
json={"kind": "transcribe"},
)
print(response.status_code)
answer = response.json()
print(answer)
JavaScript
response = await fetch(`${CAPTIONBOB_URL}/api/videos/${videoId}/renders`, {
method: "POST",
headers: { ...HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ kind: "transcribe" }),
});
console.log(response.status);
answer = await response.json();
console.log(JSON.stringify(answer, null, 2));
Answer: status 409.
{
"error": {
"code": "render_in_progress",
"message": "captionbob server: the video 1 has a render that is queued or running"
}
}
Step 3: Read the render
The state is queued, running, done or failed. doneSeconds and totalSeconds give the progress of a render that runs.
curl
curl -s "$CAPTIONBOB_URL/api/renders/$BURN_ID" \
-H "Authorization: Bearer $CAPTIONBOB_KEY"
Python
response = requests.get(
f"{CAPTIONBOB_URL}/api/renders/{burn_id}",
headers=HEADERS,
)
if not response.ok:
raise RuntimeError(response.text)
answer = response.json()
print(answer)
JavaScript
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(JSON.stringify(answer, null, 2));
Answer: status 200.
{
"render": {
"id": 4,
"videoId": 1,
"versionNumber": 4,
"kind": "burn",
"state": "queued",
"requestedBy": "editor",
"apiKeyName": "Quickstart",
"doneSeconds": 0,
"totalSeconds": null,
"hasOutput": false,
"errorMessage": null,
"createdAt": "2026-10-28T01:34:56.646Z",
"startedAt": null,
"finishedAt": null
}
}
Step 4: Wait until the render ends
Read the render every 2 seconds until its state is done or failed. When the state is failed, errorMessage tells why. hasOutput is true when the render has an output file.
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": 4,
"videoId": 1,
"versionNumber": 4,
"kind": "burn",
"state": "done",
"requestedBy": "editor",
"apiKeyName": "Quickstart",
"doneSeconds": 42,
"totalSeconds": 42,
"hasOutput": true,
"errorMessage": null,
"createdAt": "2026-10-28T01:34:56.646Z",
"startedAt": "2026-10-28T01:34:57.626Z",
"finishedAt": "2026-10-28T01:35:42.741Z"
}
}
Step 5: List the renders of the video
The renders of the video, the newest first.
curl
curl -s "$CAPTIONBOB_URL/api/videos/$VIDEO_ID/renders" \
-H "Authorization: Bearer $CAPTIONBOB_KEY"
Python
response = requests.get(
f"{CAPTIONBOB_URL}/api/videos/{video_id}/renders",
headers=HEADERS,
)
if not response.ok:
raise RuntimeError(response.text)
answer = response.json()
print(answer)
JavaScript
response = await fetch(`${CAPTIONBOB_URL}/api/videos/${videoId}/renders`, { 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.
{
"renders": [
{
"id": 4,
"videoId": 1,
"versionNumber": 4,
"kind": "burn",
"state": "done",
"requestedBy": "editor",
"apiKeyName": "Quickstart",
"doneSeconds": 42,
"totalSeconds": 42,
"hasOutput": true,
"errorMessage": null,
"createdAt": "2026-10-28T01:34:56.646Z",
"startedAt": "2026-10-28T01:34:57.626Z",
"finishedAt": "2026-10-28T01:35:42.741Z"
},
{
"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"
}
]
}
The answer has 4 renders. This page shows the first 2.
Step 6: Download the output
The output file of a burn is the burned video. The output file of a transcription is the transcript JSON.
curl
curl -s -D - -o burned.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("burned.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("burned.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: 26439122
Accept-Ranges: bytes