API and webhooks.
REST at /api/v1, signed deliveries.
Workspaces can work with their channels and episodes from their own software, Zapier or Make through a REST API, and get signed webhooks when something happens. Publishing to YouTube is the one thing the API never does: it always needs a person in the studio.
01Turn it on
- Console, Settings, Public API: turn "Public API and webhooks" on. "Requests per minute per API key" (default 60) limits each key.
- Console, Plans: tick "REST API and webhooks" on every plan that should have it.
- In the studio, the workspace owner opens Developers and creates a key. The key (
msk_…) is shown once and stored only as a hash; revoke it there. A key acts for its workspace, on behalf of the person who created it (credits, history).
02Endpoints
Base URL: https://your-address/api/v1. Send the key in every request: Authorization: Bearer msk_…. Bodies and answers are JSON with snake_case fields; dates are ISO 8601.
Channels and episodes
| Method and path | What it does |
|---|---|
GET /channels | The workspace's channels: language, style pack, YouTube connection, autopilot, episodes per stage, next posting slots |
GET /channels/{id}/episodes?stage=&limit=50 | A channel's episodes, newest change first (limit 1-200) |
POST /channels/{id}/episodes | Start an episode: {"topic": "…", "minutes": 10, "urls": ["https://…"], "notes": "…"}. Answers 201. Nothing runs or is charged yet |
GET /episodes/{id} | One episode: stage, every step's state and error, language, the files of its last render (links valid for one hour) and its YouTube uploads |
POST /episodes/{id}/steps/{step} | Run a step in the background: research, script, factcheck, produce, footage, render, package, shorts; localize on a language version. Charged like in the studio and refunded when it fails. Answers 202. publish answers 403 |
POST /episodes/{id}/versions | A language version: {"language": "de-DE", "channel_id": "…", "voice_id": "…"} (the last two optional). Answers 202 with the new episode |
An episode looks like this (shortened):
{
"id": "ep_…", "channel_id": "ch_…", "channel": "Harbour Lights", "topic": "How lighthouses guided ships before GPS",
"title": "How Lighthouses Told Ships Exactly Where They Were", "stage": "review", "language": "en-US", "version_of": null,
"steps": { "research": { "status": "done", "error": null, "note": null, "at": "2026-11-11T18:20:00.000Z" }, "…": {} },
"project_id": "prj_…", "render_id": "rnd_…", "publish_at": null, "autopilot": false,
"created_at": "…", "updated_at": "…",
"render": { "id": "rnd_…", "status": "done", "files": [ { "name": "video.mp4", "url": "https://…" } ] },
"publications": []
}
Run the steps in order. A typical client starts an episode, runs research, waits for episode.step.done, runs the next step, and so on up to shorts; then a person reviews and publishes in the studio.
Storyboards and renders
The storyboard behind an episode is a project. These endpoints work on projects directly, for example to render a storyboard you built yourself.
| Method and path | What it does |
|---|---|
GET /workspace | Name, credits and the plan's limits |
GET /projects?limit=25 | The workspace's storyboards, newest change first (limit 1-100) |
POST /projects | Create one from a storyboard: {"name": "…", "storyboard": { … }}. Answers 201 with status ready |
GET /projects/{id} | One storyboard; add ?storyboard=1 to include the storyboard JSON |
GET /projects/{id}/renders | Its last 30 renders |
POST /projects/{id}/renders | Queue a render (charges credits): {"formats": ["9:16"], "review": true} |
GET /renders/{id} | Status, progress and, when done, file links |
The easiest way to get a valid storyboard is to export one from the studio's scene editor and change it. formats are extra aspect ratios next to the storyboard's own (the plan must allow several formats); review: false skips the AI check of the frames. File links download without a key for one hour; ask again for fresh links.
Errors
{"error": "what happened", "status": 4xx}: 401 wrong or revoked key, 402 not enough credits or the plan does not include it, 403 the API is turned off (or publishing was asked for), 404 not found, 409 not ready yet or already exists, 422 a field is wrong, 429 rate limit (wait a minute).
03Webhooks
On the Developers page the owner adds an https URL (public address, port 443) and picks events:
| Event | When |
|---|---|
episode.step.done | A step of an episode finished |
episode.step.failed | A step failed (its credits are refunded) |
episode.ready_for_review | An episode is rendered and waits for a person |
episode.published | An upload to YouTube finished |
render.done | A render finished; the data has the files with their links |
render.failed | A render failed (its credits are refunded) |
project.ready | A storyboard exists for the first time (an episode's Visuals step finished, or one was created) |
Episode events carry { "episode": { … }, "step": "…", "error": "…", "publication": { … } } (the last three when they apply).
Each delivery is a POST with content-type: application/json and this body:
{ "id": "evt_…", "type": "episode.step.done", "created": "2026-11-11T10:00:00.000Z", "workspace": "ws_…", "data": { … } }
Headers: x-studio-event (the event) and x-studio-signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>.<raw body>" with the webhook's secret (whsec_…, shown once when you add it). Check it before you trust a delivery, and refuse old timestamps:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, rawBody, header) {
const p = Object.fromEntries(header.split(",").map((x) => x.split("=")));
if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
const want = createHmac("sha256", secret).update(`${p.t}.${rawBody}`).digest("hex");
return want.length === p.v1?.length && timingSafeEqual(Buffer.from(want), Buffer.from(p.v1));
}
Answer with any 2xx within 10 seconds. The Developers page has a "Test" button (sends a ping event) and the last 50 deliveries of each webhook with their result.