captionbob docs

Version 1.0.0
Contents ↓

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