Skip to content

Version 1 · Live

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: up to 25 a day and 3 at a time per workspace, enough to wire up a pipeline and test it. 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.

POST /v1/generations
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" }
  }'
Response
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" }
}

A request is targetSeconds long (30 by default). Send "length": "auto" instead and the video is as long as the feature’s steps need: about 7 seconds a step, at least 30, with targetSeconds as the cap (180 by default). The estimate prices the likely length and holds enough for the cap; what isn’t used comes back.

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.

GET /v1/generations/{id}
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.completedA video finished rendering and passed review.
generation.failedA generation stopped; the payload says why and whether credits were returned.
generation.awaiting_approvalA direction or render is waiting for a person to approve it.
video.publishedA version was published and its embed is live.
  • webhook-id is 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.
A delivery
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:

With 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:

verify-webhook.mjs (no dependencies)
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.

Follow a generation live
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.

An error
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.