跳到主要内容

使用 REST API

创建 API Key、上传视频、创建字幕项目并下载导出的成片。

集成能力向 Max 订阅用户开放。视频处理使用账户现有的时长和套餐限制。
打开集成设置

创建 API Key

集成能力向 Max 订阅用户开放。打开设置 → 集成,选择 创建 API Key,填写名称并选择权限。

只授予工作流需要的权限。读取、写入和导出权限默认勾选;查询进度或下载结果时,请保留读取权限。

| 设置中的权限 | Scope | 用途 | | --- | --- | --- | | 读取项目 | projects:read | 账户、样式、预设、项目、操作状态和结果 | | 创建和管理项目 | projects:write | 上传、创建项目、取消和重试 | | 导出项目 | exports:write | 导出、自动导出和导出重试 | | 管理 Webhooks | webhooks:manage | 接收地址和投递记录;可选,默认不勾选 |

密钥默认 90 天后过期,可选择 1–365 天或永不过期。在设置中撤销不再使用的密钥。撤销阻止新请求,但不会取消已经接受的任务。

验证首次请求

REST 基础地址为 https://api.captionbolt.com。从服务器或本地脚本通过 Authorization 请求头发送密钥。不要将密钥放入浏览器代码、URL 或 AI 对话。接口不接受会话 Cookie。

在 Bash 终端中输入密钥,避免将其写入命令历史:

read -r -s -p 'CaptionBolt API key: ' CAPTIONBOLT_API_KEY
export CAPTIONBOLT_API_KEY
printf '\n'

curl --fail-with-body https://api.captionbolt.com/v1/account \
  -H "Authorization: Bearer $CAPTIONBOLT_API_KEY"

成功响应包含处理时长余额、上传限制、可用导出格式和项目访问期。时长字段以秒为单位,除以 60 即为分钟。开始任务前检查 credits.availableCreditsSec。

通过 GET /v1/styles 查找字幕样式,或通过 GET /v1/presets 查询已保存的预设。列表支持 search、limit(1–100,默认 50)和 offset。

上传视频

创建项目前,先上传 MP4、MOV、WebM 或 AVI 文件的实际内容,不接受视频 URL。步骤如下:

  1. 使用 POST /v1/uploads、真实文件元数据和 Idempotency-Key 创建上传。
  2. 轮询返回的 operation。达到 completed 后,将最终 resourceId 作为上传 ID;遇到 failed 则停止。
  3. 使用 GET /v1/uploads/{uploadId} 获取分片大小、数量及已上传的分片。
  4. 向 POST /v1/uploads/{uploadId}/parts 发送 {"partNumbers":[1]} 获取上传 URL,每次最多请求 32 个分片。
  5. 将每个缺失分片 PUT 到返回的 URL,字节长度必须准确;不要向这些 URL 发送 API Key。
  6. 向 POST /v1/uploads/{uploadId}/complete 发送 {},确认状态为 uploaded。

通过 durationSec 提交测量得到的视频时长。如果省略,CaptionBolt 会预留套餐允许的最大视频时长;余额较少时,即使短视频也可能被拒绝。处理前仍会核验实际时长。

上传与创建项目示例

以下 Node.js 24 示例完成分片上传,并创建 review 项目。保存为 upload-and-create.mjs。它逐片读取文件,不会将整个视频载入内存;运行期间不要修改文件。

import { createHash } from "node:crypto";
import { openAsBlob } from "node:fs";
import { stat } from "node:fs/promises";
import { basename, extname } from "node:path";
import { setTimeout as sleep } from "node:timers/promises";

const apiKey = process.env.CAPTIONBOLT_API_KEY;
const uploadKey = process.env.CAPTIONBOLT_UPLOAD_KEY;
const createKey = process.env.CAPTIONBOLT_CREATE_KEY;
const [filePath, durationText] = process.argv.slice(2);
const durationSec = Number(durationText);
if (!apiKey || !uploadKey || !createKey || !filePath || !Number.isFinite(durationSec) || durationSec <= 0) {
  throw new Error("Set the API key and action keys, then supply a file and measured duration in seconds.");
}

async function api(method, path, input, idempotencyKey) {
  const response = await fetch(`https://api.captionbolt.com${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
    },
    ...(input === undefined ? {} : { body: JSON.stringify(input) }),
    redirect: "error",
    signal: AbortSignal.timeout(30000),
  });
  if (!response.ok) {
    const error = await response.json().catch(() => null);
    const detail = error?.message ?? error?.code ?? "Request failed";
    throw new Error(`API HTTP ${response.status}: ${detail}; Retry-After: ${response.headers.get("retry-after") ?? "none"}`);
  }
  return response.json();
}

async function waitForOperation(operation) {
  console.log("Operation ID:", operation.id);
  const deadline = Date.now() + 120000;
  while (operation.state !== "completed") {
    if (operation.state === "failed") throw new Error(operation.error ?? "Action failed");
    if (Date.now() >= deadline) throw new Error(`Continue polling operation ${operation.id}; do not start a new action.`);
    await sleep(Math.max(1, operation.pollAfterSeconds) * 1000);
    operation = await api("GET", `/v1/operations/${operation.id}`);
  }
  return operation;
}

const contentType = {
  ".mp4": "video/mp4", ".mov": "video/quicktime",
  ".webm": "video/webm", ".avi": "video/x-msvideo",
}[extname(filePath).toLowerCase()];
if (!contentType) throw new Error("Choose an MP4, MOV, WebM or AVI file.");
const metadata = await stat(filePath);
if (!metadata.isFile()) throw new Error("Choose a regular video file.");
const video = await openAsBlob(filePath, { type: contentType });
const fileName = basename(filePath);
const lastModifiedMs = Math.trunc(metadata.mtimeMs);
const fingerprint = createHash("sha256")
  .update(`captionbolt-upload-v1\0${fileName}\0${video.size}\0${lastModifiedMs}\0${contentType}`)
  .update(Buffer.from(await video.slice(0, 65536).arrayBuffer()))
  .update(Buffer.from(await video.slice(Math.max(65536, video.size - 65536)).arrayBuffer()))
  .digest("hex");

const uploaded = await waitForOperation(await api("POST", "/v1/uploads", {
  fileName, fileSize: video.size, lastModifiedMs, contentType, fingerprint, durationSec,
}, uploadKey));
const uploadId = uploaded.resourceId;
console.log("Upload ID:", uploadId);
let upload = await api("GET", `/v1/uploads/${uploadId}`);
if (upload.status === "uploading" || upload.status === "completing") {
  upload = await api("POST", `/v1/uploads/${uploadId}/resume`, {});
}
if (upload.status === "completing") {
  throw new Error("Upload completion is still in progress. Wait, then rerun with the same action keys.");
}
if (upload.status === "expired" || upload.status === "aborted") {
  throw new Error(`Upload is ${upload.status}. Start a new upload with new action keys.`);
}
if (upload.status !== "uploaded" && upload.status !== "consumed") {
  const completed = new Set(upload.completedPartNumbers);
  for (let number = 1; number <= upload.partCount; number++) {
    if (completed.has(number)) continue;
    const signed = await api("POST", `/v1/uploads/${uploadId}/parts`, { partNumbers: [number] });
    for (const part of signed.signedParts) {
      const start = (part.partNumber - 1) * upload.partSize;
      const bytes = video.slice(start, start + part.contentLength);
      if (bytes.size !== part.contentLength) throw new Error("File part length changed.");
      const response = await fetch(part.url, {
        method: "PUT", body: bytes,
        headers: { "Content-Length": String(part.contentLength) },
        redirect: "error", signal: AbortSignal.timeout(120000),
      });
      if (!response.ok) throw new Error(`Part ${part.partNumber}: HTTP ${response.status}`);
    }
  }
  const finished = await api("POST", `/v1/uploads/${uploadId}/complete`, {});
  if (finished.status !== "uploaded") throw new Error("Upload is not ready for project creation.");
}

const created = await waitForOperation(await api("POST", "/v1/projects", {
  uploadSessionId: uploadId, mode: "review",
}, createKey));
console.log("Project ID:", created.resourceId);

为本次上传和项目创建各生成一次操作键。重发相同请求时保留原键;处理不同视频或发起新操作时使用新键。

export CAPTIONBOLT_UPLOAD_KEY="$(node -e 'console.log(crypto.randomUUID())')"
export CAPTIONBOLT_CREATE_KEY="$(node -e 'console.log(crypto.randomUUID())')"
node upload-and-create.mjs /absolute/path/lesson.mp4 60

把路径和 60 替换为你的文件及测量得到的秒数。可使用 FFprobe 等媒体工具测量时长,不要根据文件大小猜测。脚本先打印用于恢复的 operation ID,再打印上传 ID 和项目 ID。

文件指纹使用 SHA-256:先处理示例中的 UTF-8 元数据前缀,再处理首尾各 64 KiB,重叠部分不重复计算。恢复时保持文件名、大小、修改时间和 MIME 类型不变。

查询项目进度

将 PROJECT_ID 设为脚本打印的项目 ID:

export PROJECT_ID='<project-id>'
curl --fail-with-body "https://api.captionbolt.com/v1/projects/$PROJECT_ID" \
  -H "Authorization: Bearer $CAPTIONBOLT_API_KEY"

读取返回的 status,按 pollAfterSeconds 间隔轮询:

| 项目状态 | 下一步 | | --- | --- | | processing | 等待视频准备和字幕处理 | | ready | 在 CaptionBolt 中检查、保存,再导出 | | exporting | 等待导出 | | completed | 获取结果并下载文件 | | failed | 读取 error,解决原因后明确发起重试 | | cancelled | 停止轮询,本次处理已取消 |

operation 的 completed 不表示视频已经完成。operation 描述操作,项目状态描述视频。通过 GET /v1/operations/{id} 查询操作;遇到 failed 停止并检查 error。

创建项目时可提供 language,并选择列表返回的 styleId 或 presetId。两者都省略时使用默认字幕样式。设置 mode: "auto-export" 可在处理后自动导出 MP4,另需 exports:write。若项目一直停在 ready,检查其 autoExport operation 是否报错。

导出已保存的结果

对于 review 项目,在 CaptionBolt 编辑器中检查并保存修改。为导出生成一次操作键,再发送请求:

export CAPTIONBOLT_EXPORT_KEY="$(node -e 'console.log(crypto.randomUUID())')"
curl --fail-with-body "https://api.captionbolt.com/v1/projects/$PROJECT_ID/export" \
  -H "Authorization: Bearer $CAPTIONBOLT_API_KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $CAPTIONBOLT_EXPORT_KEY" \
  --data '{"outputFormats":["mp4"]}'

先轮询 operation,再查询项目直到 completed;任一失败则停止。导出使用最新保存的修改,不含未保存内容。outputFormats 可包含 mp4、srt、vtt、ass 和 txt,具体取决于当前套餐,默认导出 MP4。

下载文件

项目达到 completed 后:

curl --fail-with-body "https://api.captionbolt.com/v1/projects/$PROJECT_ID/result" \
  -H "Authorization: Bearer $CAPTIONBOLT_API_KEY"

响应包含 files,每项都有 format 和 url,另有下载链接的 expiresAt。从文件 URL 下载时不要发送 API Key。

链接最长有效十分钟。过期后,可在项目仍可访问时重新获取。项目自创建起可访问 14 天,更新下载链接不会延长访问期。项目的 expiresAt 是访问截止时间,结果的 expiresAt 是下载链接截止时间。

已完成项目保留现有导出。需要再次编辑时,在 CaptionBolt 中创建可编辑副本。

处理错误与恢复

| 响应 | 下一步 | | --- | --- | | 400 | 检查字段及必需的幂等请求头 | | 401 | 检查密钥是否有效、未过期且未撤销 | | 403 | 检查权限及当前账户限制;只有写入权限不能读取进度 | | 404 | 检查 ID 和账户;已删除或过期的项目不可访问 | | 409 | 检查项目状态,或是否用同一操作键提交了不同输入 | | 429 | 按返回的 Retry-After 等待并读取错误;可能是请求频率或上传、任务容量限制。REST 和 MCP 共用每账户每分钟 120 次请求的限制 | | 503 或超时 | 等待、查询已知 operation,或用原键重发完全相同的请求 |

HTTP 成功可能只表示操作已接受,之后仍可能失败。检查 operation.state 和 operation.error。PAYMENT_REQUIRED 表示可用处理时长不足,也可能是现有任务已预留时长;PAYLOAD_TOO_LARGE 表示文件超过当前上传限制。查询 GET /v1/account 并解决原因,确认此前 operation 已失败后,再用新键发起新操作;原键只会返回原来的失败 operation。

创建上传、创建项目、导出、重试和取消需要 Idempotency-Key:1–128 个无空格的可打印 ASCII 字符。新操作使用新键。超时后使用原键和相同输入重发;响应丢失不代表失败。

恢复中断上传时,先用 GET /v1/uploads/{uploadId} 核对原始元数据和指纹,再调用 POST /v1/uploads/{uploadId}/resume 并使用返回的状态和分片列表。若仍是 completing,等待后再次恢复;只有 uploading 才上传缺失分片并完成上传。expired 或 aborted 无法恢复,需新建上传。DELETE /v1/uploads/{uploadId} 中止尚未被项目使用的上传,不取消已创建项目。

明确重试或取消时,向 POST /v1/projects/{projectId}/retry 或 /cancel 发送空 JSON 对象,并使用该操作自己的幂等键。导出重试另需 exports:write。

无需密钥的 GET /health/integrations 在集成服务就绪时返回 200,不可用时返回 503。它不验证你的凭证,也不表示视频已完成。

完整结构见 API 参考 (将在新窗口中打开)和 OpenAPI 文档。需要接收进度节点而非轮询时,配置 Webhooks。

我们使用 Cookie 记住你的偏好、衡量网站表现,并持续改进 CaptionBolt。