Skip to main content
Scenes are in private beta. See Scenes to request access.
The Scenes API builds and runs scene loops from your own code. It edits a scene the same way the studio and its AI assistant do: every change is built and checked as the studio checks it, and studios open on the scene see it at once. Nothing reaches viewers until you publish. It follows the same conventions as the REST API: problem documents with a stable code, cursor pages, Idempotency-Key, and RateLimit-* headers (600 requests a minute).

The model

A scene (id scn_…) is what a stream plays when it isn’t a video loop. Its draft holds resources, each at a path: GET /v1/scenes/{id}/resources lists the kinds. A path ending in /* lists one kind, for example frame/* or element/*. Each answer names the paths to read next in children. All resources go through four operations: POST /v1/scenes/{id}/probe fetches a URL once, changing nothing, to show what a table would read from it.

Walkthrough

This builds a one-frame scene, looks at it, publishes it and puts it on a stream. Set your key and workspace first. GET /v1/workspaces lists your workspaces.
1

Create a scene

The answer is 201 with the scene. Note its id (scn_…). Its version is 0: nothing is published yet.
2

Write a frame

Send the frame’s code as JSX. The frame’s id is its path, here frame/intro:
A new resource answers 201, with revision (also the ETag header), a diff of what changed, and draftRevision. If the code doesn’t build or check, the answer is 422 CHECK_FAILED, nothing changes, and errors lists every problem with its line.GET "$SCN/resources/element/Text" shows every prop Text takes, with an example.
3

Look at it

Get a picture of the frame as it goes on air:
With Accept: image/png (or no Accept) the answer is a PNG, at most 1280 px wide. With Accept: application/json you get the picture as a data URL plus what drew (drawn) and what’s wrong (issues): text outside title-safe, overlapping or too small, or a bound table with no rows. Add overlay=grid or overlay=ids to line things up, and at=<ms> to draw a moment after the frame goes on air.
4

Check, then publish

Publishing takes the draftRevision of the draft you checked. It’s required: GET /v1/scenes/{id} and every resource answer carry the current one, also as the Draft-Revision header. So you publish exactly the draft you saw, even if someone edits it meanwhile. A dry run checks the whole scene and lists what would change, publishing nothing:
Then publish:
changes lists what changed since the previous version, as resource paths. If the draft changed after your draftRevision, those edits aren’t published: the answer says so with "stale": true and a note. A draft equal to what’s published makes no version ("unchanged": true).If someone published after the version your draft is based on, publishing it would take their version back. The answer is 409 STALE_PUBLISH with publishedVersion, basedOn and reverted (what would be undone). Read the draft again and publish its new draftRevision, or send "overwritePublished": <publishedVersion> to replace that version on purpose. If that version is a revert (publishedVersionIsRevert: true), reading the draft again doesn’t help: send overwritePublished to publish your draft over it, or discard the draft (POST /discard) to keep the reverted version.
5

Put it on a stream

Use the REST API to make a stopped stream play the scene’s published version, then start it:
The stream opens on the scene’s first frame. While the stream runs, PUT …/scene answers 409 STREAM_LIVE: stop it first. { "sceneId": null } goes back to the stream’s video playlist.

On air

Once a scene is on air:
  • Publishing changes what viewers see at once. So POST /publish (and POST /revert) on a scene a running stream plays answers 409 CONFIRM_REQUIRED, naming the streams. Send it again with "confirm": true to accept that. confirm always means “I accept what the refusal named”.
  • GET /v1/scenes/{id}/live says what’s on air on each stream: under onAir, the frame on air (frame), the next one cued (next), control values and source status.
  • POST /v1/scenes/{id}/live acts like an operator, with the same names as the MCP’s control_scene: goFrame with target (take frame/<target> to air, with a transition such as fade), next (cue a frame), setControl with controlId and value, setData with sourceId and rows (replace a table’s rows) and skip with sourceId (a playlist source’s next item); streamId when several streams play it. A transition is a kind (cut, fade, slide, wipe, dip) or { "kind": …, "ms": … } with ms a whole number of 0 or more; anything else, or a streamId that isn’t a string, is 422 INVALID_INPUT, said before what’s published is looked at. A 504 SCENE_TIMEOUT may or may not have been applied: send an Idempotency-Key (one per action) and retry with the same key and body, and it applies at most once. Without one, don’t retry a skip or a button blind: read GET …/live first. The action is checked first against what’s published (a frame, control, table or playlist source that isn’t there, or a toggle given "yes", is 422 INVALID_INPUT); then it answers 409 SCENE_OFFLINE when no stream plays the scene.
  • Any resource (or picture) reads from a published version with ?version=<n> or ?version=published; the answer carries a Scene-Version header and no Draft-Revision.
  • GET /versions lists published versions; POST /revert with { "version": n, "expectedVersion": <the latest published version you saw> } publishes an old one again as the next version (409 STALE_PUBLISH if someone published since; after a revert, publishing the draft asks for overwritePublished, or discard the draft to match); POST /discard with { "draftRevision": "…" } puts the draft back to what’s published and makes that version the draft’s base, so the next unchanged publish answers unchanged and an edited one is the next version. Add "dryRun": true first to see what would be lost (lost). If the draft changed after your draftRevision, nothing is discarded (409 CONFLICT). Both take the precondition as If-Match instead ("<draftRevision>" on discard, "<expectedVersion>" on revert): then a mismatch is 412 with the same code and details.
Live inputs and keys:
  • POST /v1/scenes/{id}/ingest-keys with { "sourceId": "…" } gives an encoder such as OBS its RTMP, RTMPS and SRT addresses for an rtmp or srt source. The key is in that answer only.
  • PUT /v1/scenes/{id}/secrets/{name} saves an API key that a table sends as $secret:<name>. It’s sealed and never shown again.
  • /v1/scenes/{id}/cameras adds and tests IP cameras. See Cameras.

Concurrency

Every resource has a revision, sent as its ETag. Use it so you never overwrite someone’s newer edit, whether from another script or a person in the studio: On 412, read the resource again, reapply your change and retry.

Idempotency

Send an Idempotency-Key header on POST and PATCH to retry safely. The first answer to a key (per caller, method and path) is replayed for 24 hours, with Idempotency-Replayed: true; a retry that arrives while the first request is still running waits for its answer instead of running again. Reusing a key with a different request body is 422 IDEMPOTENCY_KEY_REUSED. Use it on publish, revert and live in particular: after a 504 SCENE_TIMEOUT, retry with the same key.

Errors

Errors are RFC 7807 problem documents (application/problem+json) with a stable code:
The full list is in the Problem schema of the OpenAPI document.

Scopes

With an OAuth token, every GET and probe needs streamloop:read. Edits, publishing and live control need streamloop:write. Deleting a scene, discarding its draft, and revoking or deleting an ingest key, camera or secret need streamloop:destructive.
Prefer to describe the scene in words? Connect an AI agent to the MCP server. Its scene_get, scene_set, scene_remove and scene_probe tools are these operations.