Skip to content

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.

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" }
}

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.