Usa la REST API
Crea una clave API, carga un video, crea un proyecto de subtítulos y descarga la exportación.
Crea una clave API
Las integraciones están disponibles con las suscripciones Max. Abra Configuración → Integraciones, seleccione Crear clave API, indique un nombre y elija los permisos.
Conceda solo los permisos necesarios. Lectura, escritura y exportación están seleccionados por defecto. Mantenga la lectura para consultar el progreso o descargar resultados.
| Permiso en la configuración | Scope | Uso |
| --- | --- | --- |
| Leer proyectos | projects:read | Cuenta, estilos, preajustes, proyectos, operaciones y resultados |
| Crear y gestionar proyectos | projects:write | Cargas, creación, cancelación y reintentos |
| Exportar proyectos | exports:write | Exportaciones, exportación automática y sus reintentos |
| Gestionar Webhooks | webhooks:manage | Endpoints y entregas; opcional y desmarcado por defecto |
Las claves caducan a los 90 días por defecto; puede elegir 1–365 días o sin caducidad. Revoque las claves que no use. La revocación bloquea nuevas solicitudes, pero no cancela tareas aceptadas.
Verifique la primera solicitud
La URL base es https://api.captionbolt.com. Envíe la clave en Authorization desde su servidor o script local. No la incluya en código del navegador, URL ni conversaciones de AI. No se aceptan cookies de sesión.
Introduzca la clave en Bash sin guardarla en el historial:
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"
La respuesta devuelve saldo, límites de carga, formatos de salida y plazo de acceso. Los valores de saldo están en segundos; divida entre 60 para obtener minutos. Compruebe credits.availableCreditsSec antes de empezar.
Busque estilos con GET /v1/styles y preajustes guardados con GET /v1/presets. Las listas admiten search, limit (1–100; 50 por defecto) y offset.
Cargue el video
Antes de crear el proyecto, cargue los bytes de un MP4, MOV, WebM o AVI. No se aceptan URL de video:
- Cree la carga con
POST /v1/uploads, metadatos reales eIdempotency-Key. - Consulte la operación. En
completed, use elresourceIddefinitivo como ID de carga. Deténgase enfailed. - Consulte
GET /v1/uploads/{uploadId}para conocer tamaño, número y partes ya cargadas. - Solicite URL con
POST /v1/uploads/{uploadId}/partsy{"partNumbers":[1]}; hasta 32 partes por solicitud. - Envíe cada parte pendiente mediante PUT a su URL, con la longitud exacta. No envíe la clave API a esas URL.
- Envíe
{}aPOST /v1/uploads/{uploadId}/completey confirmeuploaded.
Envíe la duración medida en durationSec. Si la omite, se reserva la duración máxima de su plan; esto puede rechazar un video corto cuando queda poco saldo. La duración real se comprueba antes del procesamiento.
Ejemplo de carga y creación
Este ejemplo de Node.js 24 realiza la carga multipart y crea un proyecto review. Guárdelo como upload-and-create.mjs. Lee el archivo por partes, sin cargar todo el video en memoria. No modifique el archivo mientras se ejecuta.
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);
Genere una vez las claves de acción para la carga y la creación. Consérvelas al reenviar esas solicitudes; use claves nuevas para otro video o una acción nueva.
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
Sustituya la ruta y 60 por su archivo y duración medida en segundos. Use una herramienta como FFprobe; no deduzca la duración del tamaño. El script muestra los ID de operación para recuperación, seguidos de los ID de carga y proyecto.
La huella SHA-256 combina el prefijo de metadatos UTF-8 mostrado con los primeros y últimos 64 KiB, sin duplicar el solapamiento. Mantenga nombre, tamaño, fecha de modificación y MIME al reanudar.
Siga el proyecto
Asigne a PROJECT_ID el ID mostrado por el script:
export PROJECT_ID='<project-id>'
curl --fail-with-body "https://api.captionbolt.com/v1/projects/$PROJECT_ID" \
-H "Authorization: Bearer $CAPTIONBOLT_API_KEY"
Lea status y consulte a intervalos de pollAfterSeconds:
| Estado | Qué hacer |
| --- | --- |
| processing | Esperar la preparación y los subtítulos |
| ready | Revisar y guardar en CaptionBolt; después exportar |
| exporting | Esperar la exportación |
| completed | Obtener el resultado y descargar los archivos |
| failed | Leer error, corregir la causa y solicitar un reintento |
| cancelled | Dejar de consultar; la ejecución se canceló |
Una operación completed no significa que el video esté terminado. Las operaciones describen acciones; el proyecto describe el video. Consulte GET /v1/operations/{id}; deténgase en failed y lea error.
Al crear, puede indicar language y un styleId o presetId de las listas. Omita ambos para usar el estilo predeterminado. mode: "auto-export" exporta un MP4 automáticamente tras procesar y requiere exports:write. Si permanece en ready, revise la operación autoExport del proyecto.
Exporte los cambios guardados
Para proyectos review, revise y guarde en el editor de CaptionBolt. Genere una clave de exportación una vez y envíe:
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"]}'
Consulte la operación y luego el proyecto hasta completed; deténgase si cualquiera falla. Solo se exportan los últimos cambios guardados. outputFormats admite mp4, srt, vtt, ass y txt, según el plan actual. MP4 es el formato por defecto.
Descargue los archivos
Cuando el proyecto esté en completed:
curl --fail-with-body "https://api.captionbolt.com/v1/projects/$PROJECT_ID/result" \
-H "Authorization: Bearer $CAPTIONBOLT_API_KEY"
La respuesta incluye files, cada uno con format y url, y expiresAt para los enlaces. Descargue desde las URL sin enviar la clave API.
Los enlaces duran como máximo diez minutos. Solicite otros mientras el proyecto siga accesible. El acceso dura 14 días desde la creación; renovar enlaces no amplía ese plazo. El expiresAt del proyecto limita el acceso; el del resultado limita los enlaces.
Los proyectos terminados conservan su exportación. Cree una copia editable en CaptionBolt para volver a editar.
Recupérese de errores
| Respuesta | Siguiente paso |
| --- | --- |
| 400 | Revisar campos y cabeceras de idempotencia obligatorias |
| 401 | Comprobar que la clave sea válida, no haya caducado ni sido revocada |
| 403 | Revisar permisos y límites; escritura no permite leer el progreso |
| 404 | Comprobar ID y cuenta; los proyectos borrados o caducados no están disponibles |
| 409 | Revisar el estado y si se reutilizó una clave con datos distintos |
| 429 | Respetar Retry-After y leer el error; puede limitarse la frecuencia o la capacidad de cargas/tareas. REST y MCP comparten 120 solicitudes por cuenta y minuto |
| 503 o timeout | Esperar, consultar una operación conocida o reenviar la acción idéntica con la clave original |
Un éxito HTTP puede aceptar una acción que falle después. Consulte operation.state y operation.error. PAYMENT_REQUIRED indica minutos disponibles insuficientes, posiblemente reservados por tareas activas. PAYLOAD_TOO_LARGE indica que el archivo supera el límite actual. Consulte GET /v1/account y corrija la causa. Solo tras confirmar el fallo, inicie otra acción con otra clave; la clave original devuelve la misma operación fallida.
Crear cargas o proyectos, exportar, reintentar y cancelar requiere Idempotency-Key: 1–128 caracteres ASCII imprimibles sin espacios. Cada acción nueva necesita otra clave. Tras un timeout, conserve clave y datos originales; perder la respuesta no demuestra un fallo.
Para recuperar una carga, compare metadatos y huella con GET /v1/uploads/{uploadId} y llame a POST /v1/uploads/{uploadId}/resume. Use el estado y las partes devueltos. Si sigue en completing, espere; en uploading, cargue solo las partes pendientes y complete. expired y aborted requieren otra carga, no se reanudan. DELETE /v1/uploads/{uploadId} aborta una carga aún no consumida; no cancela un proyecto creado.
Para acciones explícitas, envíe un objeto JSON vacío a POST /v1/projects/{projectId}/retry o /cancel, con su propia clave. Reintentar una exportación también requiere exports:write.
GET /health/integrations no requiere clave: devuelve 200 si el servicio está listo y 503 si no está disponible. No comprueba credenciales ni la finalización del video.
Consulte la referencia API (se abre en una ventana nueva) y OpenAPI para las estructuras completas. Use Webhooks para recibir avances sin consultar periódicamente.