The developer API arrives in the next release. This is the contract it will ship with.
Developer API
Trigger product videos from your releases, CI, dashboards or agents. Submit a generation, get a job id immediately, and receive a signed webhook when the video is ready.
API keys
Create keys in Settings → API keys. Live keys start with sb_live_ and spend credits; test keys start with sb_test_ and render free, watermarked previews. A key is shown once — store it as a secret, never in browser code.
Request a video
Send an Idempotency-Key with every create call. If your CI retries the same request, you get the same generation back instead of a second video.
curl https://api.scenebranch.com/v1/generations \
-H "Authorization: Bearer $SCENEBRANCH_API_KEY" \
-H "Idempotency-Key: release-4.2-team-invites" \
-H "Content-Type: application/json" \
-d '{
"productId": "prd_01j9zq3v8m6r5t4w2x1y0z9a8b",
"prompt": "Show how Harvey files a motion in three clicks",
"targetSeconds": 30,
"resolution": "1080p",
"aspect": "16:9",
"approval": "auto",
"metadata": { "release": "4.2.0" }
}'HTTP/1.1 202 Accepted
{
"id": "run_01j9zq6a2d4f6h8k0m2p4r6t8v",
"object": "generation",
"status": "queued",
"progress": 0,
"estimate": { "totalCredits": 214, "reserveCredits": 268, "totalUsd": 2.14, "lines": [ … ] },
"reservedCredits": 268,
"metadata": { "release": "4.2.0" }
}Poll or listen
Generations run asynchronously. Poll the generation for its status, or better, subscribe a webhook endpoint and let SceneBranch tell you when it is done.
curl https://api.scenebranch.com/v1/generations/run_01j9zq6a2d4f6h8k0m2p4r6t8v \
-H "Authorization: Bearer $SCENEBRANCH_API_KEY"Webhooks
SceneBranch webhooks follow the Standard Webhooks specification, so any compliant library verifies them and no-code tools like Zapier (Catch Hook), Make and n8n can receive them as they are. Each endpoint subscribes to the events it needs:
| generation.completed | A video finished rendering and passed review. |
| generation.failed | A generation stopped; the payload says why and whether credits were returned. |
| generation.awaiting_approval | A direction or render is waiting for a person to approve it. |
| video.published | A version was published and its embed is live. |
webhook-idis the event id. It stays the same across retries, so use it to ignore duplicates.- A delivery that doesn't get a 2xx answer is retried with exponential backoff for about 24 hours.
- Every delivery is logged with its response, and you can replay any of them from Settings → Webhooks.
- Endpoint secrets look like
whsec_…. While a secret rotates, deliveries carry a signature for each active secret.
POST /webhooks/scenebranch HTTP/1.1
content-type: application/json
webhook-id: evt_01j9zqb7k3m5p7r9t1v3x5z7b9
webhook-timestamp: 1790505600
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"type": "generation.completed",
"timestamp": "2026-09-27T10:40:00.000Z",
"data": {
"generationId": "run_01j9zq6a2d4f6h8k0m2p4r6t8v",
"videoId": "vid_01j9zqc4n6q8s0u2w4y6a8c0e2",
"versionId": "ver_01j9zqc5p7r9t1v3x5z7b9d1f3",
"durationSeconds": 31.2,
"credits": { "settled": 207, "released": 61 },
"metadata": { "release": "4.2.0" }
}
}Verify a webhook
The signature is v1, followed by the base64 HMAC-SHA256 of <webhook-id>.<webhook-timestamp>.<raw body>, keyed with your endpoint secret. The quickest way is the standardwebhooks package:
// npm install standardwebhooks
import { Webhook } from 'standardwebhooks';
const webhook = new Webhook(process.env.SCENEBRANCH_WEBHOOK_SECRET);
// Throws on a bad signature or a stale timestamp; returns the parsed event.
// rawBody must be the body exactly as received (a string), not parsed JSON.
export function verifySceneBranchWebhook(rawBody, headers) {
const header = (name) => (typeof headers.get === 'function' ? headers.get(name) : headers[name]);
return webhook.verify(rawBody, {
'webhook-id': header('webhook-id'),
'webhook-timestamp': header('webhook-timestamp'),
'webhook-signature': header('webhook-signature'),
});
}
Or without dependencies, in plain Node:
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 300;
/**
* Verifies a SceneBranch webhook (Standard Webhooks) and returns the event.
* Pass the raw body exactly as received — not parsed JSON. `headers` can be a
* Fetch Headers object (Next.js, Remix, Workers) or Node's req.headers (Express).
*/
export function verifySceneBranchWebhook(rawBody, headers, secret = process.env.SCENEBRANCH_WEBHOOK_SECRET) {
const header = (name) => (typeof headers.get === 'function' ? headers.get(name) : headers[name]);
const id = header('webhook-id');
const timestamp = header('webhook-timestamp');
const signatures = header('webhook-signature');
if (!id || !timestamp || !signatures || !secret) throw new Error('Missing webhook headers or secret');
// Reject replays: the timestamp must be close to now.
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!(age <= TOLERANCE_SECONDS)) throw new Error('Webhook timestamp is outside the tolerance');
const key = Buffer.from(secret.startsWith('whsec_') ? secret.slice(6) : secret, 'base64');
const expected = createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest();
// During a secret rotation the header carries one signature per secret.
const valid = String(signatures)
.split(' ')
.some((entry) => {
const [version, signature] = entry.split(',');
if (version !== 'v1' || !signature) return false;
const received = Buffer.from(signature, 'base64');
return received.length === expected.length && timingSafeEqual(received, expected);
});
if (!valid) throw new Error('Invalid webhook signature');
return JSON.parse(rawBody);
}
Live progress
To show progress in your own UI, exchange your API key for a short-lived live token and open a stream. Tokens last five minutes; request a fresh one whenever you reconnect.
const { streamUrl } = await fetch('https://api.scenebranch.com/v1/live-token', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.SCENEBRANCH_API_KEY}` },
}).then((r) => r.json());
const stream = new EventSource(streamUrl); // the token is in the URL; no cookies needed
stream.addEventListener('run.stage.updated', (e) => console.log(JSON.parse(e.data)));Errors and limits
Errors use RFC 9457 problem details with a stable code and a requestId to quote when you contact us. Rate limits are per workspace and plan, reported in RateLimit-* headers; a 429 carries Retry-After.
HTTP/1.1 402 Payment Required
content-type: application/problem+json
{
"type": "https://docs.scenebranch.com/errors/insufficient-credits",
"title": "Not enough credits",
"status": 402,
"code": "insufficient_credits",
"detail": "This generation needs about 268 credits; the workspace has 120.",
"requestId": "req_01j9zqd8p0r2t4v6x8z0b2d4f6"
}Need higher limits, more renders at once or a custom contract? Call +1 (786) 837-0641 or email sales@scenebranch.com.