# Authentication (/authentication) ## API keys [#api-keys] Every request carries a first-party API key as a Bearer token: ```http Authorization: Bearer sbk_live_… ``` Keys are created in the app under **Settings → API Keys**. A key belongs to the workspace it was created in and acts as the user who created it; it sees that workspace's projects and nothing else. Another workspace's ids answer `404`, never `403` — existence is not disclosed. The full key is shown **once**, at creation. The app keeps only the display prefix (`sbk_live_a1b2…`) and the time the key was last used, so a lost key is replaced, not recovered. Revoking a key takes effect on its next request. ## Scopes [#scopes] A key's scope is chosen when it is created and cannot be widened later: | Level in the app | Scopes | Allows | | ---------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Read-only | `read` | `GET /v1/me`, credits, models, and every read of projects, files, scenes, artifacts, operations, sessions and the library | | Read & write | `read`, `write` | Everything above, plus creating and changing entities, uploading files and artifacts, running steps, posting messages, cancel | `write` does not imply `read`. The app always mints *Read & write* as both, which is what API clients and the [MCP server](/mcp-server) need. Each operation in the [reference](/api-reference) names the scope it requires. ## Responses [#responses] **`401 Unauthorized`** — no key, an unknown key, or a revoked one. The response carries `WWW-Authenticate: Bearer` and a problem document: ```json { "type": "about:blank", "title": "Unauthorized", "status": 401, "detail": "Missing API key", "instance": "/v1/me" } ``` **`403 Forbidden`** — the key is live but lacks the scope the operation needs. Because the caller is authenticated, the response names the missing scope, as RFC 6750 §3.1 specifies: ```http HTTP/1.1 403 Forbidden WWW-Authenticate: Bearer error="insufficient_scope", scope="write" Content-Type: application/problem+json ``` ```json { "type": "https://scenebrew.com/problems/insufficient-scope", "title": "Forbidden", "status": 403, "detail": "This operation requires the 'write' scope", "instance": "/v1/scenes/0192a7c4-3f5e-7c8a-9b1d-2e3f4a5b6c7d/steps", "code": "insufficient_scope" } ``` The fix is a new key at the *Read & write* level; scopes are fixed for the life of a key. ## Keeping keys safe [#keeping-keys-safe] * Put keys in environment variables or your CI's secret store, never in source. The prefix is `sbk_live_` followed by 40 characters — add that pattern to your secret scanner. * Use one key per environment or integration, named after it, so the *last used* column in the app tells you what is alive. * Rotate by creating the replacement first, switching, then revoking the old key. * Presigned download URLs — on artifacts and files — are separate, short-lived credentials: they need no header and expire at `download_expires_at` (equivalently, after `download_expires_in` seconds). Do not store them. ## Playground [#playground] Each reference page has a playground that sends requests **from your browser directly to the API**. Your key is kept in the browser for the session and never passes through this site. # Changelog (/changelog) The contract is published as `@scenebrew/public-api-spec`; its version is the `info.version` the API reports at `/openapi.json` and the version shown at the bottom of this site's sidebar. Scenebrew is a development preview, so the contract is pre-1.0 and there is no major version: a change that breaks callers bumps the minor like any other and says so in its entry below, which is where the warning lives rather than in the digit. Releases up to and including 0.9.0 were originally numbered 1.0.0 to 5.0.0; they were renumbered onto this scheme, one release per minor, without any change to what they contain. ## 0.14.0 — September 2026 [#0140--september-2026] Add a file to an existing asset. Additive. * **`add_asset_file`** — `POST /v1/assets/{id}/files` with an uploaded file id and a role (`mesh`, `texture`, or a material channel: `albedo`, `normal`, `roughness`, `height`, `metallic`, `ao`, `emission`, `opacity`, `mask`) fills an empty role of an asset, whether or not a recipe already references it. Answers the asset with `ETag` = the role's digest: `201` when it filled the role, `200` when the role already held exactly these bytes. A role holding different bytes is never replaced here: `409 role_filled` names the recipes (with slots) and themes using the asset under `consumers`. There is no force flag. See [Library → Add a file to an asset](/library#add-a-file-to-an-asset). * **`replace_asset_file`** — `PUT /v1/assets/{id}/files/{role}` with an uploaded file id and `If-Match` on the digest the role holds (`files[role].sha256`) replaces the bytes deliberately: stale `412 precondition_failed` with `current_sha256`, missing header `428`, empty role `412`. The replaced digest keeps resolving through `download_asset_file?sha256=` for the retention period. * Problem documents carry the platform's extension members (`consumers`, `current_sha256`, `role`) next to `code`; `412` and `428` join the relayed statuses. The MCP server exposes both operations (tools 0.6.0). ## 0.13.0 — September 2026 [#0130--september-2026] Theme export. Additive. * **`export_theme`** — `GET /v1/themes/{id}/export` answers the theme and everything reachable from it (its designs, their recipes, the assets those slot and the assets those reference, embedded once each) as one Scenebrew entity container (`.sbe`, `application/vnd.scenebrew.entity+json`): the file a Unity project or the standalone Architect imports. The body is byte for byte what the library wrote; `X-Scenebrew-Content-Hash` carries the container's hash, the same whatever the attachment mode. `?attachments=full|by-size|by-role|reference` (default `by-size`) decides whether asset bytes ride inline or by reference; `roles` and `slots` (repeatable) name what `by-role` inlines. Every attachment `uri` is `download_asset_file` by digest (`/v1/assets/{id}/files/{role}?sha256=`), never a storage URL. A theme in another project is a `404`. See [Library → Export a theme](/library#export-a-theme); the MCP server exposes it as `export_theme` (tools 0.5.0). * **`EntityContainer`** and **`EmbeddedEntity`** join the published schemas, so generated clients type the export's body. ## 0.12.0 — September 2026 [#0120--september-2026] Assets by role, recipe slots as identity. **Breaking** in two places; the rest is additive. * **`AssetRecord.files`** — the asset's stored bytes as attachments keyed by *role*: `mesh` for a mesh asset, one map per channel for a material (`albedo`, `normal`, `roughness`, `height`, `metallic`, `ao`, `emission`, `opacity`, `mask`), `texture` for a texture asset. The record also carries what its type holds: `format`/`resolution` (texture), `shader_name`/`shader_parameters`/`texture_refs` (material), `meshes`/`materials`/`textures` (mesh). `attachment` stays on texture records, deprecated, for exports made before this version. * **`download_asset_file`** is `GET /v1/assets/{id}/files/{role}` and takes `?sha256=`: with a digest it serves those bytes while they are retained, not the asset's current binding. **Breaking:** `AssetOut.files` is now the record's map by role; the presigned list moved to `stored_files` (with `media_type`, `sha256`, `size_bytes`). **Migration:** read download URLs from `stored_files`, roles from `files`. * **`link_recipe_asset` requires `slot`.** A slot holds one asset (`409` when filled); one asset may fill several slots. New **`unlink_recipe_slot`** (`DELETE /v1/recipes/{id}/slots/{slot}`) empties one slot. **Breaking:** `create_recipe` takes `assets` (slot → asset id) instead of `asset_ids`. **Migration:** name the operator slot for every asset you bind. * **`ThemeStyle`** — the style profile under a theme's `metadata.style`, published as a schema and validated on write. Creates and updates accept `metadata`. New recipe type `scatter_props` and design type `scatter-props`; `DesignTypeOut.hda_operator` is nullable. ## 0.11.0 — September 2026 [#0110--september-2026] Stable asset files and the shared reference shapes. One schema rename; otherwise additive. * **`download_asset_file`** — `GET /v1/assets/{id}/files/{filename}` answers `307` to a short-lived presigned URL, like `download_scene_artifact` does for artifacts: the stable form of an attachment's `uri` in an exported entity container. The route never expires; the URL it redirects to does. * **`EntityReference` is now `Reference`**, and `Attachment` is scenebrew-formats' own model, so the API and the `.sbe` container share one definition of both shapes. Same keys, same constraints; only the schema name moved. **Migration:** rename the one generated type. ## 0.10.0 — September 2026 [#0100--september-2026] Library records — the entity container payloads. Additive. * **Records.** `ThemeRecord`, `DesignRecord`, `RecipeRecord` and `AssetRecord` are what a library entity *is*, independent of the cloud: the payload a Scenebrew entity container embeds. The responses `ThemeOut`, `DesignOut`, `RecipeOut`, `AssetOut` are each a record plus the server-only fields (`id`, `slug`, `project_id`, `thumbnail_url`, `created_at`, `updated_at`). * **Theme**: `world` (the constraints-first world block), `scene_parameters` keyed by planning module, `designs` as ordered references. **Design**: `recipes` as ordered references. **Recipe**: `operator`, `operator_version`, `assets` keyed by operator slot. **Asset**: `attachment`. * **Inputs**: `create_theme` / `update_theme` accept `world` and `scene_parameters`; `create_recipe` / `update_recipe` accept `operator_version`; `link_recipe_asset` accepts `slot`. ## 0.9.1 — September 2026 [#091--september-2026] Wording only. The delete operations now say what deletion does: a deleted project or scene is hidden at once and purged — rows, stored artifacts, uploaded files and conversation transcripts — 30 days later, and a Scenebrew user can restore it from the app's trash until then. No shape, status or field changed. ## 0.9.0 — September 2026 [#090--september-2026] Artifact-format and validation follow-up. **Breaking** for the artifact kind enum; the rest is additive or a stricter upload check. * **`tilemap` is no longer an artifact kind.** A tilemap is never a standalone artifact — it rides inside the `blueprint` `swfbpx` package, alongside heightmaps and shapes. `?kind=tilemap` and `GET …/artifacts/tilemap` now return `422`/`404`. **Migration:** read the blueprint; the tilemap is inside it. * **`download_expires_at`** (RFC 3339) is added next to `download_expires_in` on every presigned download — scene artifacts, files, asset files and recipe previews — the absolute instant the URL stops working. * **Upload validation.** The `sdd` is open-ended (an AI reads it): only UTF-8 is required, plus that every image it references is embedded inline as a `data:` URI (`SDD_IMAGE_NOT_CONTAINED` otherwise). The `peg` is fully schema-validated, and the `blueprint` `swfbpx` is schema- and integrity-validated, on upload — invalid documents answer `400 INVALID_ARTIFACT` before anything is stored. ## 0.8.0 — September 2026 [#080--september-2026] The full developer surface: one name per action across REST, SDK and MCP; everything slow is an [operation](/operations); cursor pagination; files as a resource; the [library](/library). Old names are gone — there are no aliases. * **Steps are named after the tool that runs them**: `create_sdd → generate_scene_sdd`, `build_peg → generate_scene_peg`, `generate_layout → generate_scene_blueprint`, `brew_scene → generate_scene_model`. Artifact kind `glb → model`. Declared but not yet available (`501 not_implemented`): `revise_scene_sdd`, `revise_scene_peg`, `analyze_scene_image`, `decorate_scene`. * **Operations**: `GET /v1/scenes/{id}/steps/{step_id}` and `GET /v1/sessions/{id}/turns/{turn_id}` are replaced by `GET /v1/operations/{id}` (one resource for scene steps, session turns and library generations; statuses `queued | running | canceling | succeeded | failed | canceled`), `GET /v1/operations?target_id=` (history) and `POST /v1/operations/{id}/cancel`. Every write that starts work answers `202` with the operation and honours `Prefer: wait=N` (N ≤ 60): `200` with the finished operation when it completes in time. * **Pagination** is `limit`/`cursor` with `next_cursor` everywhere (`page`/`page_size` are gone); transcripts page with `before`/`after` message ids, newest first, and carry `operation_id`. * **Sessions**: `POST /v1/projects/{id}/sessions` requires `text` — a session is born with its first message and returns `initial_turn`. Added `PATCH`/`DELETE /v1/sessions/{id}`, `POST /v1/sessions/{id}/context`, `DELETE /v1/sessions/{id}/context/{kind}/{entity_id}`; messages take `file_ids`. * **Files and artifacts**: `POST /v1/files` (many files per request, 100 MB) and `GET /v1/files/{id}`; scenes take `theme_id` and `source_file_id` at create; `DELETE /v1/scenes/{id}`; `GET /v1/scenes/{id}/sessions`; `GET /v1/scenes/{id}/artifacts/{kind}` (`?inline` for SDD/PEG text), `GET …/artifacts/{kind}/content` (307 to the bytes) and `PUT /v1/scenes/{id}/artifacts/{kind}` to bring your own SDD, PEG or blueprint. * **Projects, credits, models**: `POST /v1/projects`, `GET`/`PATCH`/`DELETE /v1/projects/{id}`, `GET /v1/me/credits` and `GET /v1/models` (`model` is accepted on AI steps, sessions and generations). * **Library**: `/v1/projects/{id}/{themes,designs,recipes,assets}` (list, create) and `/v1/{themes,designs,recipes,assets}/{id}` (get, update, delete), the three membership links, `GET /v1/design-types`, `GET /v1/recipe-types`, `POST /v1/recipes/{id}/preview` and `POST /v1/projects/{id}/generations` (one contract for all four kinds; `asset` runs today). * **MCP tools 0.2.0** at `mcp.scenebrew.com/v1/mcp`: the same names as tools, a `library` toolset via `?toolsets=`, every operation-backed tool takes `wait`. **Migration from 0.7.0**: rename the four step values and the three tool names, read `kind: model` where `glb` was read, poll `GET /v1/operations/{id}` instead of the step/turn paths, page with `limit`/`cursor`, and give `POST /v1/projects/{id}/sessions` a `text`. `Prefer: wait=N` replaces any implicit hold a client relied on. ## 0.7.0 — September 2026 [#070--september-2026] Layout and brew became **steps**, not synchronous calls: a brew of a real scene runs for minutes, longer than a request should stay open. * `POST /v1/scenes/{id}/layout` and `POST /v1/scenes/{id}/brew` were removed. Both ran as `POST /v1/scenes/{id}/steps` (`generate_layout`, `brew_scene`; `202`) and were polled like the AI steps. Preconditions `409 MISSING_PEG` / `MISSING_BLUEPRINT`; failures `layout_failed`, `brew_failed`, `runner_unavailable`. The procedural steps stayed free. ## 0.6.0 — September 2026 [#060--september-2026] AI work became keyed by **scene**, not by session. * `POST /v1/sessions/{id}/turns` (step runs) was removed; AI steps ran as `POST /v1/scenes/{id}/steps` on a scene created with the new `POST /v1/projects/{id}/scenes`. * A session carried `context` — the scenes and library entities it refers to — instead of a `scene_id`; a turn described a **posted message** (`POST /v1/sessions/{id}/messages`). * Added `GET /v1/projects/{id}/sessions` and `GET /v1/sessions/{id}/messages`. ## 0.5.0 — September 2026 [#050--september-2026] The first write surface: sessions, turns, layout, brew, scene updates, the `write` scope, and the [MCP server](/mcp-server) at `mcp.scenebrew.com/v1/mcp`. ## 0.4.0 — September 2026 [#040--september-2026] * `size_bytes` declared `int64` on artifacts. * Contract version now equals the API's `info.version`. ## 0.3.0 — September 2026 [#030--september-2026] * Description-only changes. ## 0.2.0 — August 2026 [#020--august-2026] * Scopes enforced: `read` required on the data operations, `403` with `WWW-Authenticate: Bearer error="insufficient_scope"` otherwise. `GET /v1/me` stays scope-free. ## 0.1.0 — August 2026 [#010--august-2026] Initial public API: `GET /v1/me`, `GET /v1/projects`, `GET /v1/projects/{project_id}/scenes`, `GET /v1/scenes/{scene_id}/artifacts` with presigned downloads; API keys in the app; problem+json errors. # Errors (/errors) The API has one error envelope. Whatever failed — validation, authentication, a pipeline precondition, the platform — the response is `Content-Type: application/problem+json` with the members of RFC 9457 plus two extensions: ```json { "type": "https://scenebrew.com/problems/missing-sdd", "title": "Conflict", "status": 409, "detail": "generate_scene_peg needs the scene's SDD; run generate_scene_sdd first", "instance": "/v1/scenes/0192a7c4-3f5e-7c8a-9b1d-2e3f4a5b6c7d/steps", "code": "missing_sdd" } ``` | Member | Meaning | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `type` | A stable identifier derived from `code` (`https://scenebrew.com/problems/`, kebab-case). `about:blank` when the status code is the whole story. Not a link — it is not dereferenced. | | `title` | The HTTP status phrase. | | `status` | The HTTP status code, repeated. | | `detail` | Human-readable explanation of this occurrence. Wording may change; branch on `code`, not on `detail`. | | `instance` | The request path. | | `code` | Machine-readable error code, lower snake case. Absent when the status is self-explanatory. | | `errors` | On `422` only: the field-level failures, each with `loc`, `msg`, `type`. | Successful responses never use this envelope; a `2xx` body is the resource. ## Status codes and codes [#status-codes-and-codes] | Status | `code` | When | | ------ | --------------------------- | ----------------------------------------------------------------------------------------------------- | | `400` | `missing_brief` | `generate_scene_sdd` was requested without a `brief`. | | `400` | `invalid_cursor` | A `cursor` that cannot be read, or one from another collection. See [Pagination](/pagination). | | `400` | `invalid_prefer` | A `Prefer` header the API does not understand (`wait` above 60, an unknown token). | | `400` | `no_files`, `kind_mismatch` | `POST /v1/files` without a part; a file whose content does not match the artifact or asset it is for. | | `401` | — | No key, unknown key, or revoked key. `WWW-Authenticate: Bearer`. | | `402` | `quota_exceeded_projects` | Your plan has no room for another project. | | `403` | `insufficient_scope` | The key lacks the scope; the header names it. See [Authentication](/authentication). | | `404` | — | No such resource in your workspace — also the answer for another workspace's ids. | | `409` | `missing_sdd` | `generate_scene_peg` before the scene has a design. | | `409` | `missing_peg` | `generate_scene_blueprint` before the scene has a graph. | | `409` | `missing_blueprint` | `generate_scene_model` before the scene has a blueprint. | | `409` | `step_in_progress` | A step was requested while another step runs on the scene. | | `409` | `turn_in_progress` | A message was posted while the session's previous turn is still running. | | `409` | `not_cancelable` | Cancel on an asset generation that has already started. | | `409` | — | A scene name already used in the project. | | `413` | `payload_too_large` | A file upload over 100 MB. | | `415` | — | An upload whose media type is not GLB, PNG, JPEG or WebP. | | `422` | — | The request did not match the schema; `errors` lists the fields. | | `429` | — | Reserved for [rate limits](/rate-limits); not issued today. | | `500` | — | Unexpected failure. Retry once; report it if it persists. | | `501` | `not_implemented` | A step or generation kind that is declared on the contract but not available yet. | | `502` | — | The platform behind the API is temporarily unavailable. Retry with backoff. | A `422` looks like this: ```json { "type": "about:blank", "title": "Unprocessable Content", "status": 422, "detail": "Request validation failed", "instance": "/v1/scenes/0192a7c4-3f5e-7c8a-9b1d-2e3f4a5b6c7d/steps", "errors": [ { "type": "enum", "loc": ["body", "step"], "msg": "Input should be 'generate_scene_sdd', 'revise_scene_sdd', 'generate_scene_peg', …", "input": "make_sdd" } ] } ``` ## Failures inside an operation [#failures-inside-an-operation] Work that was accepted (`202`) but did not finish is **not** an HTTP error: the [operation](/operations) ends with `status: "failed"` and an `error` object — `code`, `message` and `retryable`: | `error.code` | Meaning | | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | | `insufficient_credits` | The workspace has no credits for this step. Nothing was charged. | | `sdd_issues` | The brief could not be turned into a consistent design. Rephrase and run `generate_scene_sdd` again. | | `agent_unavailable` | The AI service could not be reached. Retry later. | | `step_failed` | The step ran and failed; `message` carries the detail. | | `layout_failed`, `brew_failed` | The procedural step failed on its input; `message` carries the detail. | | `runner_unavailable` | The procedural runner could not be reached. Retry later. | | `canceled` | You canceled it (`status` is `canceled`). | | `generation_failed`, `generation_refused`, `generation_lost` | An asset generation failed, was refused by the generator, or was lost to a restart. | Credits are charged only when a step succeeds; a canceled turn pays for the model calls it had made. ## Retrying [#retrying] * `502`, `agent_unavailable`, `runner_unavailable` and network errors: retry with exponential backoff. * Steps, session creates, messages and generations: send an `Idempotency-Key` so a retry after a lost response returns the operation already created instead of a second one. * The procedural steps are idempotent with respect to their inputs: rerunning them writes a new artifact version from the same graph or blueprint. # Generate a scene (/generate-a-scene) This walkthrough needs a key with the **Read & write** scope (see [Authentication](/authentication)) and a project id from [Getting started](/getting-started). It runs the four pipeline steps in order, waits for each one inline, and ends with a GLB on disk. `generate_scene_sdd` and `generate_scene_peg` are AI steps: each deducts credits once it completes. `generate_scene_blueprint` and `generate_scene_model` are procedural and free. A client that brings its own design and graph pays nothing — see [Bring your own documents](#bring-your-own-documents). ## Create a scene [#create-a-scene] Every artifact the pipeline produces belongs to a scene. Create one in your project; the name must be unique within the project (`409` otherwise). ```bash export SCENE_ID=$(curl -s -X POST "https://api.scenebrew.com/v1/projects/$PROJECT_ID/scenes" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Forest outpost", "workflow": "t2s"}' | jq -r '.id') ``` ## Write the design (AI) [#write-the-design-ai] Steps run with one call, `POST /v1/scenes/{scene_id}/steps`, and the request names the step. `generate_scene_sdd` needs a `brief` — the text a designer would hand a level artist. Without `Prefer` the API answers `202` at once with the queued [operation](/operations); with `Prefer: wait=60` it holds the request open and answers `200` with the finished operation when the step completes in time, which for a design takes about half a minute. ```bash curl -s -X POST "https://api.scenebrew.com/v1/scenes/$SCENE_ID/steps" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" \ -H "Content-Type: application/json" \ -H "Prefer: wait=60" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "step": "generate_scene_sdd", "brief": "A small forest outpost on a river bend: a watchtower, two huts, a jetty, a path that loops back to the gate. Low-poly, warm afternoon light." }' | tee /tmp/op.json | jq '{status, credits_charged, error, result}' ``` ```json { "status": "succeeded", "credits_charged": 1, "error": null, "result": { "artifacts": [{ "kind": "sdd", "version": 1 }] } } ``` `Idempotency-Key` is optional; repeating a request with the same key returns the operation it already created (`200`) instead of starting another. If `status` came back `queued` or `running`, the step outran the wait — [poll it](/operations#poll) with the operation's `id`. If it is `failed`, `error.code` says why: `insufficient_credits`, `sdd_issues` (the brief could not be turned into a consistent design), `agent_unavailable`, or `step_failed`. See [Errors](/errors). ## Build the graph (AI) [#build-the-graph-ai] Same call, next step. `generate_scene_peg` reads the scene's current design; without one the API answers `409` with code `missing_sdd`. One step runs at a time per scene — a second request while one is running is `409` `step_in_progress`. ```bash curl -s -X POST "https://api.scenebrew.com/v1/scenes/$SCENE_ID/steps" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" \ -H "Content-Type: application/json" \ -H "Prefer: wait=60" \ -d '{"step": "generate_scene_peg"}' | jq '{status, credits_charged, error}' ``` ## Blueprint (procedural, free) [#blueprint-procedural-free] The current graph becomes a blueprint — a `swfbpx` package that also carries the scene's tilemaps, heightmaps and shapes — plus a layout preview. It takes a few seconds; `409` `missing_peg` means the scene has no graph yet. ```bash curl -s -X POST "https://api.scenebrew.com/v1/scenes/$SCENE_ID/steps" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" \ -H "Content-Type: application/json" \ -H "Prefer: wait=60" \ -d '{"step": "generate_scene_blueprint"}' | jq '{status, credits_charged}' ``` ## Model (procedural, free) [#model-procedural-free] `generate_scene_model` cooks the blueprint into the GLB. Small scenes finish inside the wait; a large one can run for minutes, in which case the `200` becomes a `202` and you poll. `409` `missing_blueprint` means the blueprint step has not run. ```bash curl -s -X POST "https://api.scenebrew.com/v1/scenes/$SCENE_ID/steps" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" \ -H "Content-Type: application/json" \ -H "Prefer: wait=60" \ -d '{"step": "generate_scene_model"}' | tee /tmp/op.json | jq '{id, status}' # if it outran the wait: OP_ID=$(jq -r '.id' /tmp/op.json) until [ "$(curl -s "https://api.scenebrew.com/v1/operations/$OP_ID?wait=60" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" | tee /tmp/op.json | jq -r '.status')" = "succeeded" ]; do jq -e '.status == "failed" or .status == "canceled"' /tmp/op.json >/dev/null && break done jq '{status, error}' /tmp/op.json ``` ## Download [#download] The scene now lists a `model` artifact. Its bytes are one redirecting call: ```bash curl -sL -o scene.glb "https://api.scenebrew.com/v1/scenes/$SCENE_ID/artifacts/model/content" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" ``` `GET /v1/scenes/$SCENE_ID/artifacts` lists every artifact with a presigned `download_url` if you would rather pick from the listing — the same call as in [Getting started](/getting-started#artifacts-and-download). ## Bring your own documents [#bring-your-own-documents] The two AI steps are optional. `PUT /v1/scenes/{scene_id}/artifacts/{kind}` writes a new version of the `sdd`, the `peg` or the `blueprint`, so a pipeline that authors designs elsewhere runs only the free steps: * **`sdd`** — Markdown. The format is deliberately open: write it however an AI can read the intent. The one hard rule is that every image it references must be embedded inline as a `data:` URI, since the upload is a single self-contained document (a non-embedded reference is rejected with `SDD_IMAGE_NOT_CONTAINED`). * **`peg`** — JSON, fully validated against the PEG schema on upload. * **`blueprint`** — a `swfbpx` package (a zip of the blueprint and its tilemaps, heightmaps and shapes), schema- and integrity-validated on upload. A bare `.swfbp` JSON is for local use only; the transferable artifact is the `swfbpx`. ```bash curl -s -X PUT "https://api.scenebrew.com/v1/scenes/$SCENE_ID/artifacts/sdd" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" \ -H "Content-Type: text/markdown" \ --data-binary @scene.sdd.md | jq '{kind, version}' curl -s -X PUT "https://api.scenebrew.com/v1/scenes/$SCENE_ID/artifacts/peg" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" \ -H "Content-Type: application/json" \ --data-binary @scene.peg.json | jq '{kind, version}' ``` Uploading content identical to the current version is not a new version. Then run `generate_scene_blueprint` and `generate_scene_model` as above: `credits_charged` stays `0`. ## Iterating [#iterating] * Re-running `generate_scene_sdd` on the same scene with a new brief writes a new version of the design; the later steps pick up the newest versions. Every artifact keeps its version history and `is_current` marks the latest. `GET /v1/operations?target_id=$SCENE_ID` is the scene's step history. * `revise_scene_sdd`, `revise_scene_peg`, `analyze_scene_image` and `decorate_scene` are declared on the contract but answer `501` `not_implemented` today; the [changelog](/changelog) announces when they run. * `PATCH /v1/scenes/{scene_id}` renames, re-describes or re-themes a scene (`theme_id` from the [library](/library)) without touching the pipeline — see the [reference](/api-reference/scenes/update-scene). * `POST /v1/operations/{id}/cancel` stops a running step; a canceled AI step charges nothing. See [Operations](/operations). * A conversation instead of a pipeline: `POST /v1/projects/{project_id}/sessions` with a `text` and the scene in `context` opens a session with Scenebrew's own agent, and each message runs as an operation too — see the [reference](/api-reference/sessions/create-session). # Getting started (/getting-started) Everything below runs verbatim against production. You need a Scenebrew account and `curl`; `jq` makes the JSON easier to read but is optional. ## Create an API key [#create-an-api-key] In the app, open **Settings → API Keys** and choose **Create key**. Give it a name you will recognise in the *last used* column later, and pick a scope: * **Read-only** is enough for this walkthrough. * **Read & write** is what you need to [generate scenes](/generate-a-scene) and to connect the [MCP server](/mcp-server). The key is shown **once**. It starts with `sbk_live_`; copy it into an environment variable and never into a repository — see [Authentication](/authentication) for scopes and key hygiene. ```bash export SCENEBREW_API_KEY="sbk_live_…" ``` ## Who am I? [#who-am-i] `GET /v1/me` answers with the workspace, user and key behind the token — the first call to make from any new environment. ```bash curl -s https://api.scenebrew.com/v1/me \ -H "Authorization: Bearer $SCENEBREW_API_KEY" ``` ```json { "tenant": { "slug": "acme-games", "name": "Acme Games" }, "user": { "email": "you@example.com", "name": "You" }, "key": { "name": "Laptop", "prefix": "sbk_live_a1b2c3" }, "scopes": ["read"], "plan": "Solo" } ``` A `401` here means the header is missing or the key is invalid or revoked; see [Errors](/errors). `GET /v1/me/credits` is the balance and the most recent ledger entries, each naming the operation that charged it — the number to compare before and after an AI step: ```bash curl -s https://api.scenebrew.com/v1/me/credits \ -H "Authorization: Bearer $SCENEBREW_API_KEY" | jq '{balance, plan}' ``` ## List your projects [#list-your-projects] Scenes live in projects. Collections are [cursor-paginated](/pagination): pass a `limit` up to 100 and follow `next_cursor` until it is `null`. ```bash curl -s "https://api.scenebrew.com/v1/projects?limit=20" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" | jq ``` ```json { "items": [ { "id": "0192a7c4-3f5e-7c8a-9b1d-2e3f4a5b6c7d", "slug": "forest-outpost", "name": "Forest outpost", "description": null, "status": "active", "created_at": "2026-08-30T09:12:44Z", "updated_at": "2026-09-01T16:03:10Z" } ], "next_cursor": null } ``` Keep a project id for the next step: ```bash export PROJECT_ID=$(curl -s "https://api.scenebrew.com/v1/projects" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" | jq -r '.items[0].id') ``` With a *Read & write* key you can also create one — `POST /v1/projects` with `{"name": "…"}` — as long as your plan has room for another project; otherwise the answer is `402` with code `quota_exceeded_projects`. ## List a project's scenes [#list-a-projects-scenes] ```bash curl -s "https://api.scenebrew.com/v1/projects/$PROJECT_ID/scenes" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" | jq '.items[] | {id, name, status, asset_count}' ``` Each scene reports its `workflow`, `status`, triangle and asset counts. Pick one: ```bash export SCENE_ID=$(curl -s "https://api.scenebrew.com/v1/projects/$PROJECT_ID/scenes" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" | jq -r '.items[0].id') ``` `GET /v1/scenes/$SCENE_ID` is the scene with its current artifacts, and `GET /v1/operations?target_id=$SCENE_ID` is its step history, newest first — see [Operations](/operations). ## Artifacts and download [#artifacts-and-download] A scene's artifacts are the files each pipeline step produced: the design (`sdd`), the graph (`peg`), the `blueprint` (a `swfbpx` package), the `model` (a GLB), plus a `layout_preview` and a `thumbnail`. Every listing carries a **short-lived presigned URL** per artifact; download before `download_expires_at` and re-list for a fresh one. ```bash curl -s "https://api.scenebrew.com/v1/scenes/$SCENE_ID/artifacts" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" \ | jq '.items[] | {kind, version, is_current, size_bytes, download_expires_at}' ``` One artifact at a time is `GET /v1/scenes/{scene_id}/artifacts/{kind}`; with `?inline=true` the text of an SDD or PEG travels in the record, which is what a host agent wants to read. The bytes themselves are one call: ```bash curl -sL -o scene.glb "https://api.scenebrew.com/v1/scenes/$SCENE_ID/artifacts/model/content" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" ``` `…/content` answers `307` to the presigned object, so `-L` follows it and the download itself needs no Authorization header. The file is a standard binary glTF; the first four bytes read `glTF`. ## Next [#next] * [Generate a scene](/generate-a-scene) — run the pipeline with a *Read & write* key. * [Operations](/operations) — how the slow calls are waited on, polled and canceled. * [API reference](/api-reference) — every operation, with a playground that uses your key from the browser. * [MCP server](/mcp-server) — the same operations as tools for a coding agent. # Overview (/) Scenebrew turns a written brief into a playable 3D scene. The pipeline has four steps, and every one of them is reachable from outside the app as a **scene step**: 1. **`generate_scene_sdd`** — an AI author expands your brief into a Scene Design Document. 2. **`generate_scene_peg`** — an AI builder turns the design into a Player Experience Graph. 3. **`generate_scene_blueprint`** — a procedural step turns the graph into a blueprint and a layout preview. 4. **`generate_scene_model`** — a procedural step turns the blueprint into a GLB you can drop into an engine. Only the two AI steps cost credits. Blueprint and model are free, so a client that brings its own SDD and PEG pays nothing. Around the pipeline sit the things a real integration needs: **projects** to own scenes, **files** you upload once and reference by id, **sessions** — conversations with Scenebrew's own agent — and the **library** of themes, designs, recipes and assets a scene draws on. Anything slow runs as an [**operation**](/operations) you can wait on, poll or cancel. ## Three ways in [#three-ways-in] `https://api.scenebrew.com/v1` — JSON over HTTPS, API keys as Bearer tokens, problem+json errors. Everything the app can do with a scene, from any language. `https://mcp.scenebrew.com/v1/mcp` — the same operations as tools for Claude Code, Codex, Cursor, Zed and any other MCP host. One name per action on both surfaces. The API publishes its own OpenAPI 3.2 document. This reference is rendered from it, and so can your client. ## Start here [#start-here] Create a key and walk through the read endpoints with curl. Drive the four pipeline steps end to end and download the GLB. Wait, poll and cancel — the one async model behind steps, turns and generations. Every operation, with schemas and an in-browser playground. ## Stability [#stability] The API is versioned by path: everything documented here lives under `/v1`. Compatible additions — new fields, new operations — ship without notice beyond the [changelog](/changelog). A field or operation that has to go is marked deprecated first and removed only after a published sunset date. A breaking change would ship as a new path generation, side by side with `/v1`. The contract version is the `info.version` the API reports at [`/openapi.json`](https://api.scenebrew.com/openapi.json) — **0.9.1** as of September 2026. The contract is pre-1.0 while Scenebrew is a development preview: there is no major, and a change that breaks callers bumps the minor and says so in the [changelog](/changelog), which also has the migrations. ## For agents [#for-agents] Every page is available as Markdown — append `.md` to its URL (`/getting-started.md`), or use the *copy* button at the top of each page; [`/llms.txt`](/llms.txt) indexes the site and [`/llms-full.txt`](/llms-full.txt) is the whole thing in one file. Reference pages render the operation straight from the contract. # Library (/library) A project's **library** is what the procedural steps build scenes from. It has four kinds of entity, nested by membership but addressed flat: | Kind | Lives at | Children | | ---------- | ------------------------------------------------------- | -------- | | **theme** | `/v1/projects/{project_id}/themes`, `/v1/themes/{id}` | designs | | **design** | `/v1/projects/{project_id}/designs`, `/v1/designs/{id}` | recipes | | **recipe** | `/v1/projects/{project_id}/recipes`, `/v1/recipes/{id}` | assets | | **asset** | `/v1/projects/{project_id}/assets`, `/v1/assets/{id}` | files | Each kind has the same five operations — list (on the project), create (on the project), get, update and delete (by id) — plus a link per parent: `POST /v1/themes/{theme_id}/designs`, `POST /v1/designs/{design_id}/recipes` and `POST /v1/recipes/{recipe_id}/assets`, each with a `DELETE …/{child_id}` to unlink. Creates accept the memberships up front (`design_ids`, `recipe_ids`, `asset_ids`, `theme_ids`) so one call can place a new entity. ## Files [#files] Binaries enter the API once, through `POST /v1/files` — multipart, one or many parts per request, 100 MB per request, typed by content (GLB, PNG, JPEG, WebP) — and are referenced by id afterwards: ```bash curl -s -X POST https://api.scenebrew.com/v1/files \ -H "Authorization: Bearer $SCENEBREW_API_KEY" \ -F "files=@rock.glb" -F "files=@rock_albedo.png" | jq '.items[] | {id, filename, content_type, size_bytes}' ``` A file id goes into an asset's `file_ids`, an existing asset's empty file role ([add a file to an asset](#add-a-file-to-an-asset)), a scene's `source_file_id`, or a message's `file_ids`. `GET /v1/files/{file_id}` reports the file with a short-lived `download_url`. A file nothing references expires after 24 hours. ## Designs and recipes have types [#designs-and-recipes-have-types] `GET /v1/design-types` and `GET /v1/recipe-types` are the catalogues a `create_design` / `create_recipe` needs (`type` is required). Both take `?project_id=` and, with `?with_schema=true`, include each type's parameter JSON Schema so a client can build a valid `parameters` object. ## From files to a scene [#from-files-to-a-scene] ```bash # an asset from an uploaded GLB (the meshes and textures inside it are extracted) ASSET_ID=$(curl -s -X POST "https://api.scenebrew.com/v1/projects/$PROJECT_ID/assets" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" -H "Content-Type: application/json" \ -d "{\"name\": \"Rock\", \"type\": \"mesh\", \"file_ids\": [\"$FILE_ID\"]}" | jq -r '.id') # a recipe of the first recipe type, with the asset bound into one of its operator slots at create RECIPE_ID=$(curl -s -X POST "https://api.scenebrew.com/v1/projects/$PROJECT_ID/recipes" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" -H "Content-Type: application/json" \ -d "{\"name\": \"Rocky ground\", \"type\": \"terrain_blockout_tiles\", \"parameters\": {}, \"assets\": {\"mesh:ground\": \"$ASSET_ID\"}}" | jq -r '.id') # a sample GLB of the recipe, synchronous curl -s -X POST "https://api.scenebrew.com/v1/recipes/$RECIPE_ID/preview" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" | jq '.download_url' # a theme, then the scene picks it up THEME_ID=$(curl -s -X POST "https://api.scenebrew.com/v1/projects/$PROJECT_ID/themes" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" -H "Content-Type: application/json" \ -d '{"name": "Mossy ruins"}' | jq -r '.id') curl -s -X PATCH "https://api.scenebrew.com/v1/scenes/$SCENE_ID" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" -H "Content-Type: application/json" \ -d "{\"theme_id\": \"$THEME_ID\"}" | jq '{id, name}' ``` ## Export a theme [#export-a-theme] `GET /v1/themes/{theme_id}/export` answers the theme and everything reachable from it — its designs, their recipes, the assets those slot and the assets *those* reference, embedded once each — as one Scenebrew entity container (`.sbe`, `application/vnd.scenebrew.entity+json`): the file a Unity project or the standalone Architect imports. The bytes are exactly what the library wrote; `X-Scenebrew-Content-Hash` carries the container's hash, which is the same whatever the attachment mode, and `Content-Disposition` names the file. ```bash curl -s "https://api.scenebrew.com/v1/themes/$THEME_ID/export?attachments=reference" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" -D headers.txt -o mossy-ruins.sbe grep -i x-scenebrew-content-hash headers.txt ``` `?attachments=` decides how asset bytes travel: `by-size` (the default — small files inline as gzip+base64, the rest by URI), `reference` (URIs only, the smallest file), `full` (everything inline, for offline import), `by-role` (inline only the file `roles` and recipe `slots` you name, both repeatable: `roles=mask&roles=height&slots=mesh:wall_*`). Every URI is this API's `GET /v1/assets/{asset_id}/files/{role}?sha256=`, which needs the same key, answers `307` to a short-lived download of exactly those bytes, and keeps resolving after the asset's file is replaced. A theme in another project answers `404`. The MCP server exposes the same call as [`export_theme`](/mcp-server). ## Add a file to an asset [#add-a-file-to-an-asset] An asset can be created without files and take them later, one *role* at a time — `mesh` on a mesh asset, `texture` on a texture asset, a channel (`albedo`, `normal`, `roughness`, `height`, `metallic`, `ao`, `emission`, `opacity`, `mask`) on a material. `POST /v1/assets/{asset_id}/files` fills an **empty** role from an uploaded file through the same ingest as `create_asset`, whether or not a recipe already references the asset: ```bash curl -s -X POST "https://api.scenebrew.com/v1/assets/$ASSET_ID/files" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" -H "Content-Type: application/json" \ -d "{\"file_id\": \"$FILE_ID\", \"role\": \"mesh\"}" -D headers.txt | jq '.files' grep -i etag headers.txt # "" — the role's digest ``` Filling and replacing are different actions, and there is no force flag: | Case | Answer | | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | the role is empty, referenced or not | `201`, the asset with the file under `files[role]` | | the same bytes again | `200`, nothing changed, `ETag` unchanged | | different bytes on a filled role | `409 role_filled` — `consumers` names the recipes (with their slots) and themes using the asset; nothing is stored | | different content for this generation only | create a new asset and rebind the slots that should use it (`create_asset`, then `link_recipe_asset`) | | a deliberate replacement | `PUT /v1/assets/{asset_id}/files/{role}` with `If-Match` on the digest the role holds (`files[role].sha256`): stale `412`, missing `428` | ```bash curl -s -X PUT "https://api.scenebrew.com/v1/assets/$ASSET_ID/files/mesh" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" -H "Content-Type: application/json" \ -H "If-Match: \"$CURRENT_SHA256\"" \ -d "{\"file_id\": \"$FILE_ID\"}" | jq '.files.mesh.sha256' ``` `If-Match` is what makes a replacement safe: you replace the bytes you looked at, never what someone else put there in between (`412 precondition_failed` carries `current_sha256`). Two concurrent fills of one empty role with different bytes leave exactly one winner; with identical bytes both succeed. A replaced digest keeps resolving through `GET /v1/assets/{asset_id}/files/{role}?sha256=` for the retention period, so an export made before the replacement still works, and it leaves your storage usage at once. The MCP server exposes both as [`add_asset_file` and `replace_asset_file`](/mcp-server). ## Generation [#generation] `POST /v1/projects/{project_id}/generations` asks the AI to create a library entity from a `brief`; it runs as an [operation](/operations) of kind `generation` and returns the entity in `result` when it succeeds. `kind: asset` runs today, billed by the bring-your-own-key generator behind the chosen `model`. `theme`, `design` and `recipe` are declared and answer `501` `not_implemented` until the agent's versions run outside a conversation. # MCP server (/mcp-server) `https://mcp.scenebrew.com/v1/mcp` is a remote [Model Context Protocol](https://modelcontextprotocol.io) server over the public API. It exposes the pipeline and everything around it as **primitives**: your agent orchestrates, Scenebrew runs the work. There is no "Scenebrew agent" tool — the host agent is the agent. (Scenebrew's own conversational agent is reachable too, as `create_session` and `send_session_message`.) | | | | --------- | ------------------------------------------------------------------------------------------------------------------- | | Endpoint | `https://mcp.scenebrew.com/v1/mcp` — add `?toolsets=core,library` for the library tools | | Transport | Streamable HTTP, protocol revision `2026-07-28` (stateless); the previous revision is accepted on the same endpoint | | Auth | `Authorization: Bearer sbk_live_…` — an API key at the **Read & write** level | | Billing | Identical to the API: AI tools deduct credits, procedural tools are free | | Tools | `_meta.version` **0.5.0** — 38 core tools, 30 more in the library toolset | ## Connect [#connect] ```bash claude mcp add --transport http scenebrew https://mcp.scenebrew.com/v1/mcp \ --header "Authorization: Bearer sbk_live_…" ``` Any host that speaks Streamable HTTP with a static bearer header works the same way — Codex, Cursor, Zed and others take a JSON entry: ```json { "scenebrew": { "type": "http", "url": "https://mcp.scenebrew.com/v1/mcp", "headers": { "Authorization": "Bearer sbk_live_…" } } } ``` Most hosts store the header in plain text. Keep that file out of version control, and use a key created for the purpose so you can revoke it alone. ## One name per action [#one-name-per-action] Tool names are the API's operation ids: `create_scene` on REST is `create_scene` as a tool, with the same arguments and the same result. Every tool call carries a Scenebrew id as an ordinary argument — a `project_id` for creates and lists, an entity id (`scene_id`, `session_id`, `operation_id`, …) as returned by earlier calls — which is how the `2026-07-28` revision prescribes cross-call state. There is no transport-level session. Three places the two surfaces differ, by design: the REST endpoint `run_scene_step` is one tool **per step** (`generate_scene_sdd`, `generate_scene_peg`, `generate_scene_blueprint`, `generate_scene_model`, plus the declared-but-not-yet-available `revise_scene_sdd`, `revise_scene_peg`, `analyze_scene_image`, `decorate_scene`); `generate_library_entity` is one tool per kind (`generate_theme`, `generate_design`, `generate_recipe`, `generate_asset`); and the three membership links collapse into `link_library_entities` / `unlink_library_entities`. ## Core tools [#core-tools] | Group | Tools | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | identity | `get_credits`, `list_models` | | projects | `list_projects`, `create_project`, `get_project`, `update_project`, `delete_project` | | files | `upload_files` (base64 inline, ≤ 8 MB per file — bigger files go through `POST /v1/files` and arrive as ids), `get_file` | | scenes | `list_scenes`, `create_scene`, `get_scene`, `update_scene`, `delete_scene`, `list_scene_sessions`, `list_scene_artifacts`, `get_scene_artifact`, `upload_scene_artifact` | | steps | `generate_scene_sdd` (AI), `generate_scene_peg` (AI), `generate_scene_blueprint`, `generate_scene_model`; declared, `501` until available: `revise_scene_sdd`, `revise_scene_peg`, `analyze_scene_image`, `decorate_scene` | | sessions | `list_sessions`, `create_session` (AI — born with its first message), `get_session`, `update_session`, `delete_session`, `list_session_messages`, `send_session_message` (AI), `link_session_context`, `unlink_session_context` | | operations | `get_operation`, `list_operations`, `cancel_operation` | The **library** toolset (`?toolsets=core,library`) adds `list/create/get/update/delete_{theme,design,recipe,asset}`, `link_library_entities` (an asset binds into a recipe `slot`), `unlink_library_entities`, `list_design_types`, `list_recipe_types`, `preview_recipe`, `export_theme`, `add_asset_file`, `replace_asset_file`, and the four `generate_*` tools. `export_theme(theme_id)` hands a theme to an engine: it wraps [`GET /v1/themes/{id}/export`](/library#export-a-theme) and returns `url` (the `/v1` route — fetch it with your API key to write the `.sbe` file), `content_hash` (the container's hash, the same whatever the attachment mode), `filename`, `media_type`, `size_bytes`, and the parsed `container` itself when it is at most 2 MiB (`inline: false` skips it). `attachments`, `roles` and `slots` are the endpoint's; every attachment URI inside points back at `/v1/assets/{id}/files/{role}?sha256=…` on the same API. `add_asset_file(asset_id, file_id, role)` fills an empty file role of an existing asset from an uploaded file (see [Library → Add a file to an asset](/library#add-a-file-to-an-asset)); the same bytes again are a no-op, different bytes on a filled role are refused with the recipes and themes using the asset named in the error. `replace_asset_file(asset_id, role, file_id, if_match)` replaces deliberately, with `if_match` the digest the role holds now (`files[role].sha256`); a stale value is a `412` naming `current_sha256`. There is no force flag. Every tool carries `_meta.version` (SemVer of the tool surface), `_meta.billed` on the AI ones, `_meta.runs_as: "operation"` on the ones that start work, and read-only / destructive / idempotent annotations, so a host can show what a call will do and cost before making it. ## Long-running tools [#long-running-tools] Every tool that starts work — the steps, `create_session`, `send_session_message`, the `generate_*` tools — returns an [operation](/operations) and takes a `wait` argument (default 300 seconds). The call stays open with progress notifications and returns the operation as it stands: terminal, or still running once `wait` has passed. `wait: 0` hands the queued operation back at once; `get_operation(operation_id, wait)` polls it, `cancel_operation` stops it, `list_operations(target_id)` is a scene's or session's history. A failed operation is a **normal result** with `status: "failed"` and `error.code` — the same catalogue as [Errors](/errors). Tool errors are reserved for the call itself: `401`, `403 insufficient_scope` (create a *Read & write* key), `404` (unknown id or another workspace's), `409 missing_sdd` / `missing_peg` / `missing_blueprint` / `step_in_progress` (a step called out of order), `422`, `501 not_implemented`. ## Resources [#resources] For hosts that surface MCP resources, the reads are also available as URIs: | URI | Content | | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `scenebrew://projects` | the projects the key can see | | `scenebrew://projects/{project_id}/scenes` | a project's scenes | | `scenebrew://projects/{project_id}/sessions` | a project's sessions | | `scenebrew://scenes/{scene_id}` | a scene with its current artifacts | | `scenebrew://scenes/{scene_id}/artifacts` | the artifacts with short-lived presigned download URLs | | `scenebrew://scenes/{scene_id}/artifacts/sdd` | the Scene Design Document as `text/markdown` | | `scenebrew://scenes/{scene_id}/artifacts/peg` | the Player Experience Graph as `application/json` | | `scenebrew://sessions/{session_id}` | a session with its context | | `scenebrew://sessions/{session_id}/messages` | the transcript | | `scenebrew://operations/{operation_id}` | one operation | | `scenebrew://themes/{theme_id}/export` | the theme as an entity container (`application/vnd.scenebrew.entity+json`), asset bytes by digest URI — library toolset | Tool and resource lists are cacheable for an hour; instance reads are not cached. ## Authorization details [#authorization-details] * Without a key the server answers `401` with `WWW-Authenticate: Bearer error="invalid_token", resource_metadata="…/.well-known/oauth-protected-resource/v1/mcp"`. * The RFC 9728 metadata names Scenebrew's authorization server, so OAuth-capable hosts fail cleanly today and will work unchanged once the OAuth 2.1 flow ships. Until then, bearer keys are the way in. ## Versioning [#versioning] `/v1` names a surface generation and is not bumped for protocol revisions, tool evolution (`_meta.version` per tool) or compatible changes. Tools 0.2.0 replaced the fifteen tools of 0.1.0 (`create_sdd`, `build_peg`, `generate_layout`, `brew_scene`, `get_turn`, …) with the names above; the old names are gone, not aliased; 0.4.0 keyed recipe assets by operator slot; 0.5.0 added `export_theme`; 0.6.0 added `add_asset_file` and `replace_asset_file`. A `/v2` appears only for a breaking change to the auth/transport generation, and runs side by side with `/v1` for at least twelve months. # Operations (/operations) Anything that takes longer than a request should — a pipeline step, a message the agent answers, an AI-generated library entity — runs as an **operation**. Three kinds share one resource, one status vocabulary and one set of calls: | `kind` | Started by | Target | | -------------- | ------------------------------------------------------------------------------------ | --------- | | `scene_step` | `POST /v1/scenes/{scene_id}/steps` | a scene | | `session_turn` | `POST /v1/projects/{project_id}/sessions`, `POST /v1/sessions/{session_id}/messages` | a session | | `generation` | `POST /v1/projects/{project_id}/generations` | a project | ```json { "id": "01a072e1-b04e-7490-8941-c13fdf3e231c", "kind": "scene_step", "status": "succeeded", "target": { "type": "scene", "id": "01a072e0-a18f-7dc6-a74b-93c7b32d5915" }, "input": { "step": "generate_scene_blueprint" }, "result": { "artifacts": [ { "kind": "blueprint", "version": 1 }, { "kind": "layout_preview", "version": 1 } ] }, "error": null, "credits_charged": 0, "model": null, "created_by": "api-key", "created_at": "2026-09-05T19:05:12Z", "started_at": "2026-09-05T19:05:12Z", "finished_at": "2026-09-05T19:05:14Z", "cancel_requested_at": null } ``` `status` moves `queued → running → succeeded | failed`, or through `canceling → canceled` when you stop it. A failure is **data, not an HTTP error**: the operation ends `failed` with `error.code`, `error.message` and `error.retryable`. `credits_charged` is final once the operation is terminal. ## Start [#start] Every write that starts work answers `202 Accepted` with the operation and a `Location` header naming it. Add `Prefer: wait=N` (RFC 7240, N ≤ 60) and the API holds the response until the operation is terminal or N seconds have passed: `200` with the finished operation, or the same `202` with it still running. `Preference-Applied: wait=N` on the response says the hold was honoured; `Prefer: respond-async` is the default and needs no header. ```bash curl -s -X POST "https://api.scenebrew.com/v1/scenes/$SCENE_ID/steps" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" \ -H "Content-Type: application/json" \ -H "Prefer: wait=60" \ -d '{"step": "generate_scene_model"}' -D - | grep -i -E '^(HTTP|location|preference-applied)' ``` An `Idempotency-Key` header on the same four calls makes a repeated request return the operation it already created (`200`) instead of starting another — use one per logical attempt so a retry after a lost response is safe. ## Poll [#poll] `GET /v1/operations/{operation_id}` reads an operation; `?wait=N` turns it into a bounded long-poll that returns as soon as the operation reaches a terminal state, or after N seconds with its current state. A loop therefore makes one call a minute, not one a second: ```bash until curl -s "https://api.scenebrew.com/v1/operations/$OP_ID?wait=60" \ -H "Authorization: Bearer $SCENEBREW_API_KEY" \ | tee /tmp/op.json \ | jq -e '.status | IN("succeeded", "failed", "canceled")' >/dev/null; do :; done jq '{status, credits_charged, error}' /tmp/op.json ``` A `202` that outran `Prefer: wait` and a `202` from `respond-async` are the same handle; poll them the same way. ## History [#history] `GET /v1/operations?target_id=…` lists the operations of one target — a scene's steps, a session's turns, a project's generations — newest first, [cursor-paginated](/pagination), with `kind` and `status` filters. It is the receipt for everything that ran: the credits ledger in `GET /v1/me/credits` names the operation behind each charge. ## Cancel [#cancel] `POST /v1/operations/{operation_id}/cancel` asks the worker to stop: `202` with the operation in `canceling`, and it ends `canceled` at the worker's next boundary (`error.code` is `canceled`). A canceled flat-priced step charges nothing; a canceled turn keeps the reply it had produced so far and pays for the model calls it made. Cancel on an operation that is already terminal is a no-op `200`. Asset generation cannot be stopped once it has started and answers `409` `not_cancelable`. ## Ordering [#ordering] The API refuses a step that cannot run yet, with a `409` you can branch on rather than a failed operation: `missing_sdd`, `missing_peg`, `missing_blueprint` for the pipeline order, `step_in_progress` (one step at a time per scene) and `turn_in_progress` (one turn at a time per session). See [Errors](/errors). ## Models [#models] AI steps, sessions and generations accept a `model` id from `GET /v1/models` — the models your workspace may use, with their `hosting` (`scenebrew` or `external`) and `billing` (`credits` or `byok`). The chosen model is echoed on the operation. Today the list holds the Scenebrew default and any bring-your-own-key asset generators configured in the app. # Pagination (/pagination) Every collection — projects, a project's scenes, sessions, library entities, an operation history — pages the same way: | Query parameter | Default | Meaning | | --------------- | ------- | ------------------------------------------------------------------- | | `limit` | `20` | Items per page, 1 – 100 | | `cursor` | — | The `next_cursor` of the previous page; omit it for the first page. | The response wraps the page in `items` and tells you whether there is more: ```json { "items": [], "next_cursor": "MjpwYWdlOjIw" } ``` `next_cursor` is `null` on the last page. Cursors are opaque: pass them back unchanged and do not build them yourself. A cursor that cannot be read answers `400` with code `invalid_cursor`; a cursor from another collection does the same. ```bash cursor="" while :; do body=$(curl -s "https://api.scenebrew.com/v1/projects?limit=100${cursor:+&cursor=$cursor}" \ -H "Authorization: Bearer $SCENEBREW_API_KEY") echo "$body" | jq -c '.items[]' cursor=$(echo "$body" | jq -r '.next_cursor // empty') [ -z "$cursor" ] && break done ``` There is no `total`: collections are read newest first and a loop stops when `next_cursor` is `null`. ## Transcripts [#transcripts] A session's messages page by **message id**, newest first, so a client that already holds part of the conversation asks only for what it lacks: | Query parameter | Meaning | | --------------- | ---------------------------------------- | | `before` | Only messages older than this message id | | `after` | Only messages newer than this message id | | `limit` | 1 – 100, default `20` | `GET /v1/sessions/{session_id}/messages` also carries `next_cursor`, so the generic loop above works for transcripts too; `before` and `after` are the shortcuts. ## Not paginated [#not-paginated] A scene's artifact listing and a tenant's models are bounded sets and come back whole. The credits ledger takes a `limit` (default 20) and no cursor: it is the most recent entries, not a history to walk. # Rate limits (/rate-limits) The API does not enforce per-key request limits yet. Fair use applies: AI steps are bounded by your credits, and the platform may shed load with `502` responses under pressure, which you should retry with backoff. When limits are introduced they will be announced in the [changelog](/changelog) ahead of enforcement and will follow the usual shape: * a `429 Too Many Requests` problem document with a `Retry-After` header; * per-key windows, so one busy integration does not throttle another. Write clients so that `429` and `502` are retried with exponential backoff and jitter, and nothing else changes when limits switch on. # SDKs and codegen (/sdks) ## The contract [#the-contract] `GET https://api.scenebrew.com/openapi.json` is the OpenAPI **3.2** document the API is built from — the same file this reference is rendered from. It needs no authentication. ```bash curl -s https://api.scenebrew.com/openapi.json | jq '.info' ``` Generate a client with the tool of your ecosystem (openapi-generator, `openapi-typescript`, `datamodel-codegen`, NSwag, …). Things worth knowing when you do: * Every error response is `application/problem+json`; the `default` response of each operation is the [problem document](/errors). * `size_bytes` is declared `int64`; pick a client type that does not truncate. * Operation ids are stable (`list_projects`, `run_scene_step`, `get_operation`, `create_session`, …), match the page names in this reference, and are the [MCP tool names](/mcp-server) too. * The four calls that start work answer `202` **or** `200` depending on `Prefer: wait`; both carry the same `Operation` schema, so model the response as one type. * Pin the contract version you generated from (`info.version`) and diff on upgrade; the [changelog](/changelog) lists what each version added. Pre-1.0 the minor carries breaks as well as additions, so read the entry before moving: 0.8.0 renamed every write operation, and a client generated from 0.7.0 or earlier needs regenerating, not patching. ## Official SDKs [#official-sdks] Language SDKs (Python, C# for Unity) are generated from this contract and are being regenerated for 0.9.1; they will be announced in the [changelog](/changelog) when they are publicly installable. Until then the contract above and the [MCP server](/mcp-server) are the supported integration surfaces. # API reference (/api-reference) {/* @scenebrew/public-api-spec@0.14.0 — generated by scripts/generate-api-reference.ts, do not edit. */} This reference is generated from the OpenAPI document the API itself serves at [`/openapi.json`](https://api.scenebrew.com/openapi.json). Contract version **0.14.0**, published as `@scenebrew/public-api-spec` 0.14.0. Every operation takes an API key as a Bearer token and answers errors as `application/problem+json` — see [Authentication](/authentication) and [Errors](/errors). Each page has a **playground** that sends real requests with your key from your browser to the API; nothing passes through this site. | Method | Path | Summary | | -------- | -------------------------------------------------------------------------------------------------------- | ----------------------- | | `GET` | [`/v1/me`](/api-reference/identity/get-me) | Get Me | | `GET` | [`/v1/me/credits`](/api-reference/identity/get-credits) | Get Credits | | `GET` | [`/v1/models`](/api-reference/identity/list-models) | List Models | | `GET` | [`/v1/projects`](/api-reference/projects/list-projects) | List Projects | | `POST` | [`/v1/projects`](/api-reference/projects/create-project) | Create Project | | `GET` | [`/v1/projects/{project_id}`](/api-reference/projects/get-project) | Get Project | | `PATCH` | [`/v1/projects/{project_id}`](/api-reference/projects/update-project) | Update Project | | `DELETE` | [`/v1/projects/{project_id}`](/api-reference/projects/delete-project) | Delete Project | | `POST` | [`/v1/files`](/api-reference/files/upload-files) | Upload Files | | `GET` | [`/v1/files/{file_id}`](/api-reference/files/get-file) | Get File | | `GET` | [`/v1/projects/{project_id}/scenes`](/api-reference/scenes/list-scenes) | List Scenes | | `POST` | [`/v1/projects/{project_id}/scenes`](/api-reference/scenes/create-scene) | Create Scene | | `GET` | [`/v1/scenes/{scene_id}`](/api-reference/scenes/get-scene) | Get Scene | | `PATCH` | [`/v1/scenes/{scene_id}`](/api-reference/scenes/update-scene) | Update Scene | | `DELETE` | [`/v1/scenes/{scene_id}`](/api-reference/scenes/delete-scene) | Delete Scene | | `GET` | [`/v1/scenes/{scene_id}/sessions`](/api-reference/scenes/list-scene-sessions) | List Scene Sessions | | `GET` | [`/v1/scenes/{scene_id}/artifacts`](/api-reference/scenes/list-scene-artifacts) | List Scene Artifacts | | `GET` | [`/v1/scenes/{scene_id}/artifacts/{kind}`](/api-reference/scenes/get-scene-artifact) | Get Scene Artifact | | `PUT` | [`/v1/scenes/{scene_id}/artifacts/{kind}`](/api-reference/scenes/upload-scene-artifact) | Upload Scene Artifact | | `GET` | [`/v1/scenes/{scene_id}/artifacts/{kind}/content`](/api-reference/scenes/download-scene-artifact) | Download Scene Artifact | | `POST` | [`/v1/scenes/{scene_id}/steps`](/api-reference/scenes/run-scene-step) | Run Scene Step | | `GET` | [`/v1/projects/{project_id}/sessions`](/api-reference/sessions/list-sessions) | List Sessions | | `POST` | [`/v1/projects/{project_id}/sessions`](/api-reference/sessions/create-session) | Create Session | | `GET` | [`/v1/sessions/{session_id}`](/api-reference/sessions/get-session) | Get Session | | `PATCH` | [`/v1/sessions/{session_id}`](/api-reference/sessions/update-session) | Update Session | | `DELETE` | [`/v1/sessions/{session_id}`](/api-reference/sessions/delete-session) | Delete Session | | `GET` | [`/v1/sessions/{session_id}/messages`](/api-reference/sessions/list-session-messages) | List Session Messages | | `POST` | [`/v1/sessions/{session_id}/messages`](/api-reference/sessions/send-session-message) | Send Session Message | | `POST` | [`/v1/sessions/{session_id}/context`](/api-reference/sessions/link-session-context) | Link Session Context | | `DELETE` | [`/v1/sessions/{session_id}/context/{kind}/{entity_id}`](/api-reference/sessions/unlink-session-context) | Unlink Session Context | | `GET` | [`/v1/operations/{operation_id}`](/api-reference/operations/get-operation) | Get Operation | | `GET` | [`/v1/operations`](/api-reference/operations/list-operations) | List Operations | | `POST` | [`/v1/operations/{operation_id}/cancel`](/api-reference/operations/cancel-operation) | Cancel Operation | | `GET` | [`/v1/projects/{project_id}/themes`](/api-reference/library/list-themes) | List Themes | | `POST` | [`/v1/projects/{project_id}/themes`](/api-reference/library/create-theme) | Create Theme | | `GET` | [`/v1/themes/{entity_id}`](/api-reference/library/get-theme) | Get Theme | | `PATCH` | [`/v1/themes/{entity_id}`](/api-reference/library/update-theme) | Update Theme | | `DELETE` | [`/v1/themes/{entity_id}`](/api-reference/library/delete-theme) | Delete Theme | | `GET` | [`/v1/projects/{project_id}/designs`](/api-reference/library/list-designs) | List Designs | | `POST` | [`/v1/projects/{project_id}/designs`](/api-reference/library/create-design) | Create Design | | `GET` | [`/v1/designs/{entity_id}`](/api-reference/library/get-design) | Get Design | | `PATCH` | [`/v1/designs/{entity_id}`](/api-reference/library/update-design) | Update Design | | `DELETE` | [`/v1/designs/{entity_id}`](/api-reference/library/delete-design) | Delete Design | | `GET` | [`/v1/projects/{project_id}/recipes`](/api-reference/library/list-recipes) | List Recipes | | `POST` | [`/v1/projects/{project_id}/recipes`](/api-reference/library/create-recipe) | Create Recipe | | `GET` | [`/v1/recipes/{entity_id}`](/api-reference/library/get-recipe) | Get Recipe | | `PATCH` | [`/v1/recipes/{entity_id}`](/api-reference/library/update-recipe) | Update Recipe | | `DELETE` | [`/v1/recipes/{entity_id}`](/api-reference/library/delete-recipe) | Delete Recipe | | `GET` | [`/v1/projects/{project_id}/assets`](/api-reference/library/list-assets) | List Assets | | `POST` | [`/v1/projects/{project_id}/assets`](/api-reference/library/create-asset) | Create Asset | | `GET` | [`/v1/assets/{entity_id}`](/api-reference/library/get-asset) | Get Asset | | `PATCH` | [`/v1/assets/{entity_id}`](/api-reference/library/update-asset) | Update Asset | | `DELETE` | [`/v1/assets/{entity_id}`](/api-reference/library/delete-asset) | Delete Asset | | `GET` | [`/v1/assets/{asset_id}/files/{role}`](/api-reference/library/download-asset-file) | Download Asset File | | `PUT` | [`/v1/assets/{asset_id}/files/{role}`](/api-reference/library/replace-asset-file) | Replace Asset File | | `POST` | [`/v1/assets/{asset_id}/files`](/api-reference/library/add-asset-file) | Add Asset File | | `GET` | [`/v1/themes/{theme_id}/export`](/api-reference/library/export-theme) | Export Theme | | `POST` | [`/v1/themes/{theme_id}/designs`](/api-reference/library/link-theme-design) | Link Theme Design | | `DELETE` | [`/v1/themes/{theme_id}/designs/{design_id}`](/api-reference/library/unlink-theme-design) | Unlink Theme Design | | `POST` | [`/v1/designs/{design_id}/recipes`](/api-reference/library/link-design-recipe) | Link Design Recipe | | `DELETE` | [`/v1/designs/{design_id}/recipes/{recipe_id}`](/api-reference/library/unlink-design-recipe) | Unlink Design Recipe | | `POST` | [`/v1/recipes/{recipe_id}/assets`](/api-reference/library/link-recipe-asset) | Link Recipe Asset | | `DELETE` | [`/v1/recipes/{recipe_id}/assets/{asset_id}`](/api-reference/library/unlink-recipe-asset) | Unlink Recipe Asset | | `DELETE` | [`/v1/recipes/{recipe_id}/slots/{slot}`](/api-reference/library/unlink-recipe-slot) | Unlink Recipe Slot | | `GET` | [`/v1/design-types`](/api-reference/library/list-design-types) | List Design Types | | `GET` | [`/v1/recipe-types`](/api-reference/library/list-recipe-types) | List Recipe Types | | `POST` | [`/v1/projects/{project_id}/generations`](/api-reference/library/generate-library-entity) | Generate Library Entity | | `POST` | [`/v1/recipes/{recipe_id}/preview`](/api-reference/library/preview-recipe) | Preview Recipe | # Get File (/api-reference/files/get-file) `GET /v1/files/{file_id}` One uploaded file with a fresh short-lived download URL. Requires the `read` scope. ## Parameters - `file_id` (path): string (uuid) ## Responses ### 200 — Successful Response FileOut - `id`: string (uuid) - `filename`: string - `content_type`: string - `size_bytes`: integer (int64) - `sha256`: string - `status`: string — uploaded (unreferenced, expires), referenced, or expired. - `download_url`: string | null (optional) - `download_expires_in`: integer | null (optional) - `download_expires_at`: string (date-time) | null (optional) - `created_at`: string (date-time) - `expires_at`: string (date-time) | null ### default # Upload Files (/api-reference/files/upload-files) `POST /v1/files` Upload files once and reference them by id afterwards — a scene's source model, an asset's files, a message's attachments. Unreferenced files expire after 24 hours. Requires the `write` scope. ## Responses ### 201 — Successful Response FilesOut - `items`: FileOut[] ### default # Add Asset File (/api-reference/library/add-asset-file) `POST /v1/assets/{asset_id}/files` Fill an empty file role of an existing asset from an uploaded file (`POST /v1/files`), through the same ingest as `create_asset` (digest, deduplicated storage, quota). The role names what the file is for: `mesh` on a mesh asset, `texture` on a texture asset, a channel role (`albedo`, `normal`, `roughness`, `height`, `metallic`, `ao`, `emission`, `opacity`, `mask`) on a material. Works whether or not a recipe already references the asset. The same bytes again answer `200` with the unchanged digest. A role that holds *different* bytes is never replaced here: `409` `role_filled` names the recipes (with their slots) and themes using the asset under `consumers` — replace deliberately with `replace_asset_file` and `If-Match`, or create a new asset and rebind the slots that should use it. Of two concurrent fills with different bytes exactly one succeeds. Requires the `write` scope. ## Parameters - `asset_id` (path): string (uuid) ## Request body `application/json` — AssetFileAddIn - `file_id`: string (uuid) — An uploaded file (POST /v1/files); its bytes fill the role. - `role`: string — `mesh` on a mesh asset, `texture` on a texture asset, or a channel role of a material: albedo, normal, roughness, height, metallic, ao, emission, opacity, mask. ## Responses ### 200 — The role already held exactly these bytes; nothing changed. AssetOut - `name`: string - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: object (optional) - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) | null (optional) - `thumbnail_url`: string | null (optional) - `created_at`: string (date-time) - `updated_at`: string (date-time) - `type`: string | null (optional) — mesh, material, texture, … - `category`: string | null (optional) - `files`: object (optional) — The stored bytes by role. - `attachment`: Attachment | null (optional) — A texture's image, as exports before SCBR-678 carried it; `files.texture` now. - `format`: string | null (optional) - `resolution`: integer[] | null (optional) - `shader_name`: string | null (optional) - `shader_parameters`: object | null (optional) - `texture_refs`: MaterialTextureRef[] | null (optional) - `meshes`: MeshSlotRecord[] | null (optional) - `materials`: object[] | null (optional) - `textures`: object[] | null (optional) - `stored_files`: AssetFileOut[] (optional) ### 201 — The role was empty and now holds the file. AssetOut - `name`: string - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: object (optional) - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) | null (optional) - `thumbnail_url`: string | null (optional) - `created_at`: string (date-time) - `updated_at`: string (date-time) - `type`: string | null (optional) — mesh, material, texture, … - `category`: string | null (optional) - `files`: object (optional) — The stored bytes by role. - `attachment`: Attachment | null (optional) — A texture's image, as exports before SCBR-678 carried it; `files.texture` now. - `format`: string | null (optional) - `resolution`: integer[] | null (optional) - `shader_name`: string | null (optional) - `shader_parameters`: object | null (optional) - `texture_refs`: MaterialTextureRef[] | null (optional) - `meshes`: MeshSlotRecord[] | null (optional) - `materials`: object[] | null (optional) - `textures`: object[] | null (optional) - `stored_files`: AssetFileOut[] (optional) ### default # Create Asset (/api-reference/library/create-asset) `POST /v1/projects/{project_id}/assets` Create a asset in the project. Membership ids given here are linked right after. Requires the `write` scope. ## Parameters - `project_id` (path): string (uuid) ## Request body `application/json` — AssetCreateIn - `name`: string - `type`: "mesh" | "material" | "texture" - `category`: string | null (optional) - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: object (optional) — Free-form; the prop convention puts `fit` (height_units, width_units, footprint, pivot, blocking, tier) and `aliases` here. - `file_ids`: string (uuid)[] (optional) — Uploaded files (POST /v1/files) that become the asset's files. ## Responses ### 201 — Successful Response AssetOut - `name`: string - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: object (optional) - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) | null (optional) - `thumbnail_url`: string | null (optional) - `created_at`: string (date-time) - `updated_at`: string (date-time) - `type`: string | null (optional) — mesh, material, texture, … - `category`: string | null (optional) - `files`: object (optional) — The stored bytes by role. - `attachment`: Attachment | null (optional) — A texture's image, as exports before SCBR-678 carried it; `files.texture` now. - `format`: string | null (optional) - `resolution`: integer[] | null (optional) - `shader_name`: string | null (optional) - `shader_parameters`: object | null (optional) - `texture_refs`: MaterialTextureRef[] | null (optional) - `meshes`: MeshSlotRecord[] | null (optional) - `materials`: object[] | null (optional) - `textures`: object[] | null (optional) - `stored_files`: AssetFileOut[] (optional) ### default # Create Design (/api-reference/library/create-design) `POST /v1/projects/{project_id}/designs` Create a design in the project. Membership ids given here are linked right after. Requires the `write` scope. ## Parameters - `project_id` (path): string (uuid) ## Request body `application/json` — DesignCreateIn - `name`: string - `type`: string — A design type id (GET /v1/design-types). - `description`: string | null (optional) - `parameters`: object (optional) - `tags`: string[] (optional) - `theme_ids`: string (uuid)[] (optional) - `recipe_ids`: string (uuid)[] (optional) ## Responses ### 201 — Successful Response DesignOut - `name`: string - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: object (optional) - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) | null (optional) - `thumbnail_url`: string | null (optional) - `created_at`: string (date-time) - `updated_at`: string (date-time) - `type`: string | null (optional) — terrain, bridge, fence, ladder, stair, wall, … - `recipes`: Reference[] (optional) — Ordered recipe references. - `parameters`: object | null (optional) - `recipe_ids`: string (uuid)[] (optional) - `recipe_count`: integer (optional) ### default # Create Recipe (/api-reference/library/create-recipe) `POST /v1/projects/{project_id}/recipes` Create a recipe in the project. Membership ids given here are linked right after. Requires the `write` scope. ## Parameters - `project_id` (path): string (uuid) ## Request body `application/json` — RecipeCreateIn - `name`: string - `type`: string — A recipe type id (GET /v1/recipe-types). - `description`: string | null (optional) - `operator_version`: string | null (optional) — Semver of the operator schema `parameters` were written against. - `parameters`: object (optional) - `tags`: string[] (optional) - `design_ids`: string (uuid)[] (optional) - `assets`: object (optional) — Operator slot → asset id, linked right after creation. Slots are the operator's vocabulary (material:ground, mesh:tree_a); one asset may fill several slots. ## Responses ### 201 — Successful Response RecipeOut - `name`: string - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: object (optional) - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) | null (optional) - `thumbnail_url`: string | null (optional) - `created_at`: string (date-time) - `updated_at`: string (date-time) - `operator`: string | null (optional) — Brewer node type (e.g. swf::terrain_blockout_tiles); null while the recipe type's operator is not ported. - `operator_version`: string | null (optional) — Semver of the operator schema the parameters were written against. - `parameters`: object (optional) - `assets`: object (optional) — Asset references keyed by operator slot. - `type`: string | null (optional) - `asset_ids`: string (uuid)[] (optional) - `asset_count`: integer (optional) ### default # Create Theme (/api-reference/library/create-theme) `POST /v1/projects/{project_id}/themes` Create a theme in the project. Membership ids given here are linked right after. Requires the `write` scope. ## Parameters - `project_id` (path): string (uuid) ## Request body `application/json` — ThemeCreateIn - `name`: string - `description`: string | null (optional) - `parameters`: object (optional) - `world`: World | null (optional) - `scene_parameters`: object (optional) - `tags`: string[] (optional) - `design_ids`: string (uuid)[] (optional) — Designs to link at create. ## Responses ### 201 — Successful Response ThemeOut - `name`: string - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: ThemeMetadata (optional) - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) | null (optional) - `thumbnail_url`: string | null (optional) - `created_at`: string (date-time) - `updated_at`: string (date-time) - `world`: World | null (optional) - `scene_parameters`: object (optional) - `designs`: Reference[] (optional) — Ordered design references. - `parameters`: object | null (optional) - `design_ids`: string (uuid)[] (optional) - `design_count`: integer (optional) ### default # Delete Asset (/api-reference/library/delete-asset) `DELETE /v1/assets/{entity_id}` Delete a asset. Requires the `write` scope. ## Parameters - `entity_id` (path): string (uuid) ## Responses ### 204 — Successful Response ### default # Delete Design (/api-reference/library/delete-design) `DELETE /v1/designs/{entity_id}` Delete a design. Requires the `write` scope. ## Parameters - `entity_id` (path): string (uuid) ## Responses ### 204 — Successful Response ### default # Delete Recipe (/api-reference/library/delete-recipe) `DELETE /v1/recipes/{entity_id}` Delete a recipe. Requires the `write` scope. ## Parameters - `entity_id` (path): string (uuid) ## Responses ### 204 — Successful Response ### default # Delete Theme (/api-reference/library/delete-theme) `DELETE /v1/themes/{entity_id}` Delete a theme. Requires the `write` scope. ## Parameters - `entity_id` (path): string (uuid) ## Responses ### 204 — Successful Response ### default # Download Asset File (/api-reference/library/download-asset-file) `GET /v1/assets/{asset_id}/files/{role}` One of the asset's stored files by role (`files` on `get_asset`: mesh, texture, albedo, normal, …): a 307 to a short-lived presigned URL. With `sha256` this is the digest-addressed form an exported entity container carries as an attachment's `uri` — it keeps resolving after the file was replaced, and never expires itself; the URL it redirects to does. A stored filename (`texture.png`) is accepted in place of the role for exports made before roles existed. Requires the `read` scope. ## Parameters - `asset_id` (path): string (uuid) - `role` (path): string - `sha256` (query): string | null (optional) — The digest of the bytes wanted. Served while those bytes are retained, even after the asset's file was replaced; omitted, the current file. ## Responses ### 307 — Redirect to the presigned file bytes. ### default # Export Theme (/api-reference/library/export-theme) `GET /v1/themes/{theme_id}/export` The theme and everything reachable from it — its designs, their recipes, the assets those slot and the assets *those* reference — as one self-contained Scenebrew entity container (`.sbe`, JSON), the file a Unity project or the standalone Architect imports. Each asset's bytes ride inline or as an attachment whose `uri` is this API's `download_asset_file` route by digest (`/v1/assets/{id}/files/{role}?sha256=`), which keeps resolving after the asset's file is replaced; `attachments` decides which. `X-Scenebrew-Content-Hash` is the container's hash, the same in every mode. Bytes are exactly what the library wrote — hash the body to verify. A theme in another project is a 404. Requires the `read` scope. ## Parameters - `theme_id` (path): string (uuid) - `attachments` (query): "full" | "by-size" | "by-role" | "reference" (optional) — How asset bytes travel: `full` (every attachment inline), `by-size` (inline up to the server's threshold, else by digest URI), `by-role` (inline only the `roles` and `slots` named), `reference` (never bytes — URIs only). - `roles` (query): string[] | null (optional) — `by-role`: file roles to inline (`mask`, `height`, `mesh`, …). - `slots` (query): string[] | null (optional) — `by-role`: glob patterns over recipe slots to inline (`mesh:wall_*`). ## Responses ### 200 — The theme as a Scenebrew entity container (`.sbe`). ### default # Generate Library Entity (/api-reference/library/generate-library-entity) `POST /v1/projects/{project_id}/generations` Ask the AI for a library entity, as an Operation (202 + `Location`). One contract for themes, designs, recipes and assets: `kind` names what is made, `model` where inference runs and who is billed. Today `asset` runs — a material or prop generator from `GET /v1/models`, billed by its vendor (BYOK) — and `result.entity` names the new asset; the other kinds answer 501 until available. Requires the `write` scope. ## Parameters - `project_id` (path): string (uuid) - `Idempotency-Key` (header): string | null (optional) — Optional. Repeating a request with the same key returns what it already created (200) instead of running it again. - `Prefer` (header): string | null (optional) — RFC 7240. `wait=N` (N ≤ 60) holds the response until the operation is terminal or N seconds passed: 200 with the finished operation, or the same 202 as without it. `respond-async` (the default) returns the queued operation at once. ## Request body `application/json` — GenerationCreateIn - `kind`: "theme" | "design" | "recipe" | "asset" - `brief`: string - `model`: string | null (optional) — Required for assets; see GET /v1/models. - `name`: string | null (optional) - `category`: string | null (optional) - `tags`: string[] (optional) - `reference_file_ids`: string (uuid)[] (optional) — Uploaded reference images for guided prop generation. - `params`: object (optional) — Generator-specific parameters. - `theme_id`: string (uuid) | null (optional) - `design_id`: string (uuid) | null (optional) - `recipe_type`: string | null (optional) ## Responses ### 200 — The operation finished within `Prefer: wait`, or the Idempotency-Key replayed an operation that already exists. OperationOut - `id`: string (uuid) - `kind`: "scene_step" | "session_turn" | "generation" - `status`: "queued" | "running" | "canceling" | "succeeded" | "failed" | "canceled" - `target`: OperationTargetOut - `input`: object (optional) - `progress`: object | null (optional) - `result`: object | null (optional) - `error`: OperationErrorOut | null (optional) - `credits_charged`: integer (optional) - `model`: OperationModelOut | null (optional) - `created_by`: string (uuid) - `created_at`: string (date-time) | null - `started_at`: string (date-time) | null - `finished_at`: string (date-time) | null - `cancel_requested_at`: string (date-time) | null (optional) ### 202 — Successful Response OperationOut - `id`: string (uuid) - `kind`: "scene_step" | "session_turn" | "generation" - `status`: "queued" | "running" | "canceling" | "succeeded" | "failed" | "canceled" - `target`: OperationTargetOut - `input`: object (optional) - `progress`: object | null (optional) - `result`: object | null (optional) - `error`: OperationErrorOut | null (optional) - `credits_charged`: integer (optional) - `model`: OperationModelOut | null (optional) - `created_by`: string (uuid) - `created_at`: string (date-time) | null - `started_at`: string (date-time) | null - `finished_at`: string (date-time) | null - `cancel_requested_at`: string (date-time) | null (optional) ### default # Get Asset (/api-reference/library/get-asset) `GET /v1/assets/{entity_id}` One asset with its memberships. Requires the `read` scope. ## Parameters - `entity_id` (path): string (uuid) ## Responses ### 200 — Successful Response AssetOut - `name`: string - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: object (optional) - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) | null (optional) - `thumbnail_url`: string | null (optional) - `created_at`: string (date-time) - `updated_at`: string (date-time) - `type`: string | null (optional) — mesh, material, texture, … - `category`: string | null (optional) - `files`: object (optional) — The stored bytes by role. - `attachment`: Attachment | null (optional) — A texture's image, as exports before SCBR-678 carried it; `files.texture` now. - `format`: string | null (optional) - `resolution`: integer[] | null (optional) - `shader_name`: string | null (optional) - `shader_parameters`: object | null (optional) - `texture_refs`: MaterialTextureRef[] | null (optional) - `meshes`: MeshSlotRecord[] | null (optional) - `materials`: object[] | null (optional) - `textures`: object[] | null (optional) - `stored_files`: AssetFileOut[] (optional) ### default # Get Design (/api-reference/library/get-design) `GET /v1/designs/{entity_id}` One design with its memberships. Requires the `read` scope. ## Parameters - `entity_id` (path): string (uuid) ## Responses ### 200 — Successful Response DesignOut - `name`: string - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: object (optional) - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) | null (optional) - `thumbnail_url`: string | null (optional) - `created_at`: string (date-time) - `updated_at`: string (date-time) - `type`: string | null (optional) — terrain, bridge, fence, ladder, stair, wall, … - `recipes`: Reference[] (optional) — Ordered recipe references. - `parameters`: object | null (optional) - `recipe_ids`: string (uuid)[] (optional) - `recipe_count`: integer (optional) ### default # Get Recipe (/api-reference/library/get-recipe) `GET /v1/recipes/{entity_id}` One recipe with its memberships. Requires the `read` scope. ## Parameters - `entity_id` (path): string (uuid) ## Responses ### 200 — Successful Response RecipeOut - `name`: string - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: object (optional) - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) | null (optional) - `thumbnail_url`: string | null (optional) - `created_at`: string (date-time) - `updated_at`: string (date-time) - `operator`: string | null (optional) — Brewer node type (e.g. swf::terrain_blockout_tiles); null while the recipe type's operator is not ported. - `operator_version`: string | null (optional) — Semver of the operator schema the parameters were written against. - `parameters`: object (optional) - `assets`: object (optional) — Asset references keyed by operator slot. - `type`: string | null (optional) - `asset_ids`: string (uuid)[] (optional) - `asset_count`: integer (optional) ### default # Get Theme (/api-reference/library/get-theme) `GET /v1/themes/{entity_id}` One theme with its memberships. Requires the `read` scope. ## Parameters - `entity_id` (path): string (uuid) ## Responses ### 200 — Successful Response ThemeOut - `name`: string - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: ThemeMetadata (optional) - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) | null (optional) - `thumbnail_url`: string | null (optional) - `created_at`: string (date-time) - `updated_at`: string (date-time) - `world`: World | null (optional) - `scene_parameters`: object (optional) - `designs`: Reference[] (optional) — Ordered design references. - `parameters`: object | null (optional) - `design_ids`: string (uuid)[] (optional) - `design_count`: integer (optional) ### default # Link Design Recipe (/api-reference/library/link-design-recipe) `POST /v1/designs/{design_id}/recipes` Attach a recipe to a design (same project). Requires the `write` scope. ## Parameters - `design_id` (path): string (uuid) ## Request body `application/json` — DesignRecipeLinkIn - `recipe_id`: string (uuid) ## Responses ### 204 — Successful Response ### default # Link Recipe Asset (/api-reference/library/link-recipe-asset) `POST /v1/recipes/{recipe_id}/assets` Bind an asset into one operator slot of a recipe (same project). The slot is the link's identity: a slot holds one asset (409 when filled), one asset may fill several slots. Requires the `write` scope. ## Parameters - `recipe_id` (path): string (uuid) ## Request body `application/json` — RecipeAssetLinkIn - `asset_id`: string (uuid) - `slot`: string — The operator's asset slot the asset fills — the link's identity. A slot holds one asset; one asset may fill several slots (mesh:tree_a and mesh:tree_b). ## Responses ### 204 — Successful Response ### default # Link Theme Design (/api-reference/library/link-theme-design) `POST /v1/themes/{theme_id}/designs` Attach a design to a theme (same project). Requires the `write` scope. ## Parameters - `theme_id` (path): string (uuid) ## Request body `application/json` — ThemeDesignLinkIn - `design_id`: string (uuid) ## Responses ### 204 — Successful Response ### default # List Assets (/api-reference/library/list-assets) `GET /v1/projects/{project_id}/assets` The project's assets, most recently changed first. Filters: q, tags, type, category, recipe_id. Requires the `read` scope. ## Parameters - `project_id` (path): string (uuid) - `q` (query): string | null (optional) — Name search. - `tags` (query): string[] | null (optional) - `type` (query): string | null (optional) - `category` (query): string | null (optional) - `theme_id` (query): string (uuid) | null (optional) - `design_id` (query): string (uuid) | null (optional) - `recipe_id` (query): string (uuid) | null (optional) - `limit` (query): integer (optional) — Items per page - `cursor` (query): string | null (optional) — The `next_cursor` of the previous page; omit for the first page. ## Responses ### 200 — Successful Response CursorPage_AssetOut_ - `items`: AssetOut[] - `next_cursor`: string | null (optional) - `total`: integer | null (optional) — Size of the whole collection, when the platform knows it. ### default # List Design Types (/api-reference/library/list-design-types) `GET /v1/design-types` The design types with their compatible recipe types — what a valid `create_design` needs. The catalogue is platform-wide; a visible project is needed to read it. Requires the `read` scope. ## Parameters - `project_id` (query): string (uuid) — Any project the key can see. ## Responses ### 200 — Successful Response DesignTypeOut[] ### default # List Designs (/api-reference/library/list-designs) `GET /v1/projects/{project_id}/designs` The project's designs, most recently changed first. Filters: q, tags, type, category, theme_id. Requires the `read` scope. ## Parameters - `project_id` (path): string (uuid) - `q` (query): string | null (optional) — Name search. - `tags` (query): string[] | null (optional) - `type` (query): string | null (optional) - `category` (query): string | null (optional) - `theme_id` (query): string (uuid) | null (optional) - `design_id` (query): string (uuid) | null (optional) - `recipe_id` (query): string (uuid) | null (optional) - `limit` (query): integer (optional) — Items per page - `cursor` (query): string | null (optional) — The `next_cursor` of the previous page; omit for the first page. ## Responses ### 200 — Successful Response CursorPage_DesignOut_ - `items`: DesignOut[] - `next_cursor`: string | null (optional) - `total`: integer | null (optional) — Size of the whole collection, when the platform knows it. ### default # List Recipe Types (/api-reference/library/list-recipe-types) `GET /v1/recipe-types` The recipe types; with `with_schema`, each carries the JSON Schema of its parameters, which an agent needs before `create_recipe`. Requires the `read` scope. ## Parameters - `project_id` (query): string (uuid) — Any project the key can see. - `category` (query): string | null (optional) - `with_schema` (query): boolean (optional) — Include each type's parameter JSON Schema. ## Responses ### 200 — Successful Response RecipeTypeOut[] ### default # List Recipes (/api-reference/library/list-recipes) `GET /v1/projects/{project_id}/recipes` The project's recipes, most recently changed first. Filters: q, tags, type, category, design_id. Requires the `read` scope. ## Parameters - `project_id` (path): string (uuid) - `q` (query): string | null (optional) — Name search. - `tags` (query): string[] | null (optional) - `type` (query): string | null (optional) - `category` (query): string | null (optional) - `theme_id` (query): string (uuid) | null (optional) - `design_id` (query): string (uuid) | null (optional) - `recipe_id` (query): string (uuid) | null (optional) - `limit` (query): integer (optional) — Items per page - `cursor` (query): string | null (optional) — The `next_cursor` of the previous page; omit for the first page. ## Responses ### 200 — Successful Response CursorPage_RecipeOut_ - `items`: RecipeOut[] - `next_cursor`: string | null (optional) - `total`: integer | null (optional) — Size of the whole collection, when the platform knows it. ### default # List Themes (/api-reference/library/list-themes) `GET /v1/projects/{project_id}/themes` The project's themes, most recently changed first. Filters: q, tags. Requires the `read` scope. ## Parameters - `project_id` (path): string (uuid) - `q` (query): string | null (optional) — Name search. - `tags` (query): string[] | null (optional) - `type` (query): string | null (optional) - `category` (query): string | null (optional) - `theme_id` (query): string (uuid) | null (optional) - `design_id` (query): string (uuid) | null (optional) - `recipe_id` (query): string (uuid) | null (optional) - `limit` (query): integer (optional) — Items per page - `cursor` (query): string | null (optional) — The `next_cursor` of the previous page; omit for the first page. ## Responses ### 200 — Successful Response CursorPage_ThemeOut_ - `items`: ThemeOut[] - `next_cursor`: string | null (optional) - `total`: integer | null (optional) — Size of the whole collection, when the platform knows it. ### default # Preview Recipe (/api-reference/library/preview-recipe) `POST /v1/recipes/{recipe_id}/preview` Cook a sample of the recipe — with the given parameters, or its own — and get a short-lived URL of the GLB. The one synchronous cook, bounded by the small sample. Requires the `write` scope. ## Parameters - `recipe_id` (path): string (uuid) ## Request body `application/json` — RecipePreviewIn | null ## Responses ### 200 — Successful Response PreviewOut - `download_url`: string - `download_expires_in`: integer - `download_expires_at`: string (date-time) | null (optional) - `content_type`: string - `size_bytes`: integer (int64) ### default # Replace Asset File (/api-reference/library/replace-asset-file) `PUT /v1/assets/{asset_id}/files/{role}` Replace the bytes of a filled role deliberately, even when recipes and themes use the asset. `If-Match` must name the digest the role holds now (`412` `precondition_failed` with `current_sha256` when it does not, `428` without the header, `412` on an empty role — fill that with `add_asset_file`), so a caller replaces what it looked at and never what someone else put there in between. The same bytes again are a no-op `200`. The replaced digest keeps resolving through `download_asset_file?sha256=` for the retention period, so exports made before still work; it leaves the workspace's storage usage now. Requires the `write` scope. ## Parameters - `asset_id` (path): string (uuid) - `role` (path): string — `mesh` on a mesh asset, `texture` on a texture asset, a channel role (`albedo`, `normal`, `roughness`, `height`, `metallic`, `ao`, `emission`, `opacity`, `mask`) on a material - `If-Match` (header): string | null (optional) — The digest the role holds now — `files[role].sha256` of the asset you looked at, quoted or bare. Required: a stale value answers `412`, a missing one `428`. ## Request body `application/json` — AssetFileReplaceIn - `file_id`: string (uuid) — An uploaded file (POST /v1/files); its bytes replace the role's. ## Responses ### 200 — The role holds the new bytes (or already held them). AssetOut - `name`: string - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: object (optional) - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) | null (optional) - `thumbnail_url`: string | null (optional) - `created_at`: string (date-time) - `updated_at`: string (date-time) - `type`: string | null (optional) — mesh, material, texture, … - `category`: string | null (optional) - `files`: object (optional) — The stored bytes by role. - `attachment`: Attachment | null (optional) — A texture's image, as exports before SCBR-678 carried it; `files.texture` now. - `format`: string | null (optional) - `resolution`: integer[] | null (optional) - `shader_name`: string | null (optional) - `shader_parameters`: object | null (optional) - `texture_refs`: MaterialTextureRef[] | null (optional) - `meshes`: MeshSlotRecord[] | null (optional) - `materials`: object[] | null (optional) - `textures`: object[] | null (optional) - `stored_files`: AssetFileOut[] (optional) ### default # Unlink Design Recipe (/api-reference/library/unlink-design-recipe) `DELETE /v1/designs/{design_id}/recipes/{recipe_id}` Detach a recipe from a design. Requires the `write` scope. ## Parameters - `design_id` (path): string (uuid) - `recipe_id` (path): string (uuid) ## Responses ### 204 — Successful Response ### default # Unlink Recipe Asset (/api-reference/library/unlink-recipe-asset) `DELETE /v1/recipes/{recipe_id}/assets/{asset_id}` Detach an asset from every slot it fills on a recipe. Requires the `write` scope. ## Parameters - `recipe_id` (path): string (uuid) - `asset_id` (path): string (uuid) ## Responses ### 204 — Successful Response ### default # Unlink Recipe Slot (/api-reference/library/unlink-recipe-slot) `DELETE /v1/recipes/{recipe_id}/slots/{slot}` Empty one operator slot of a recipe (404 when it is empty). Requires the `write` scope. ## Parameters - `recipe_id` (path): string (uuid) - `slot` (path): string ## Responses ### 204 — Successful Response ### default # Unlink Theme Design (/api-reference/library/unlink-theme-design) `DELETE /v1/themes/{theme_id}/designs/{design_id}` Detach a design from a theme. Requires the `write` scope. ## Parameters - `theme_id` (path): string (uuid) - `design_id` (path): string (uuid) ## Responses ### 204 — Successful Response ### default # Update Asset (/api-reference/library/update-asset) `PATCH /v1/assets/{entity_id}` Change a asset; omitted fields stay. Requires the `write` scope. ## Parameters - `entity_id` (path): string (uuid) ## Request body `application/json` — LibraryUpdateIn - `name`: string | null (optional) - `description`: string | null (optional) - `parameters`: object | null (optional) - `world`: World | null (optional) - `scene_parameters`: object | null (optional) - `operator_version`: string | null (optional) - `tags`: string[] | null (optional) - `category`: string | null (optional) - `texture_refs`: object[] | null (optional) - `shader_parameters`: object | null (optional) - `metadata`: object | null (optional) — Merged key by key into the entity's metadata (null removes a key). A theme's `style` must match `ThemeStyle`. ## Responses ### 200 — Successful Response AssetOut - `name`: string - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: object (optional) - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) | null (optional) - `thumbnail_url`: string | null (optional) - `created_at`: string (date-time) - `updated_at`: string (date-time) - `type`: string | null (optional) — mesh, material, texture, … - `category`: string | null (optional) - `files`: object (optional) — The stored bytes by role. - `attachment`: Attachment | null (optional) — A texture's image, as exports before SCBR-678 carried it; `files.texture` now. - `format`: string | null (optional) - `resolution`: integer[] | null (optional) - `shader_name`: string | null (optional) - `shader_parameters`: object | null (optional) - `texture_refs`: MaterialTextureRef[] | null (optional) - `meshes`: MeshSlotRecord[] | null (optional) - `materials`: object[] | null (optional) - `textures`: object[] | null (optional) - `stored_files`: AssetFileOut[] (optional) ### default # Update Design (/api-reference/library/update-design) `PATCH /v1/designs/{entity_id}` Change a design; omitted fields stay. Requires the `write` scope. ## Parameters - `entity_id` (path): string (uuid) ## Request body `application/json` — LibraryUpdateIn - `name`: string | null (optional) - `description`: string | null (optional) - `parameters`: object | null (optional) - `world`: World | null (optional) - `scene_parameters`: object | null (optional) - `operator_version`: string | null (optional) - `tags`: string[] | null (optional) - `category`: string | null (optional) - `texture_refs`: object[] | null (optional) - `shader_parameters`: object | null (optional) - `metadata`: object | null (optional) — Merged key by key into the entity's metadata (null removes a key). A theme's `style` must match `ThemeStyle`. ## Responses ### 200 — Successful Response DesignOut - `name`: string - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: object (optional) - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) | null (optional) - `thumbnail_url`: string | null (optional) - `created_at`: string (date-time) - `updated_at`: string (date-time) - `type`: string | null (optional) — terrain, bridge, fence, ladder, stair, wall, … - `recipes`: Reference[] (optional) — Ordered recipe references. - `parameters`: object | null (optional) - `recipe_ids`: string (uuid)[] (optional) - `recipe_count`: integer (optional) ### default # Update Recipe (/api-reference/library/update-recipe) `PATCH /v1/recipes/{entity_id}` Change a recipe; omitted fields stay. Requires the `write` scope. ## Parameters - `entity_id` (path): string (uuid) ## Request body `application/json` — LibraryUpdateIn - `name`: string | null (optional) - `description`: string | null (optional) - `parameters`: object | null (optional) - `world`: World | null (optional) - `scene_parameters`: object | null (optional) - `operator_version`: string | null (optional) - `tags`: string[] | null (optional) - `category`: string | null (optional) - `texture_refs`: object[] | null (optional) - `shader_parameters`: object | null (optional) - `metadata`: object | null (optional) — Merged key by key into the entity's metadata (null removes a key). A theme's `style` must match `ThemeStyle`. ## Responses ### 200 — Successful Response RecipeOut - `name`: string - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: object (optional) - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) | null (optional) - `thumbnail_url`: string | null (optional) - `created_at`: string (date-time) - `updated_at`: string (date-time) - `operator`: string | null (optional) — Brewer node type (e.g. swf::terrain_blockout_tiles); null while the recipe type's operator is not ported. - `operator_version`: string | null (optional) — Semver of the operator schema the parameters were written against. - `parameters`: object (optional) - `assets`: object (optional) — Asset references keyed by operator slot. - `type`: string | null (optional) - `asset_ids`: string (uuid)[] (optional) - `asset_count`: integer (optional) ### default # Update Theme (/api-reference/library/update-theme) `PATCH /v1/themes/{entity_id}` Change a theme; omitted fields stay. Requires the `write` scope. ## Parameters - `entity_id` (path): string (uuid) ## Request body `application/json` — LibraryUpdateIn - `name`: string | null (optional) - `description`: string | null (optional) - `parameters`: object | null (optional) - `world`: World | null (optional) - `scene_parameters`: object | null (optional) - `operator_version`: string | null (optional) - `tags`: string[] | null (optional) - `category`: string | null (optional) - `texture_refs`: object[] | null (optional) - `shader_parameters`: object | null (optional) - `metadata`: object | null (optional) — Merged key by key into the entity's metadata (null removes a key). A theme's `style` must match `ThemeStyle`. ## Responses ### 200 — Successful Response ThemeOut - `name`: string - `description`: string | null (optional) - `tags`: string[] (optional) - `metadata`: ThemeMetadata (optional) - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) | null (optional) - `thumbnail_url`: string | null (optional) - `created_at`: string (date-time) - `updated_at`: string (date-time) - `world`: World | null (optional) - `scene_parameters`: object (optional) - `designs`: Reference[] (optional) — Ordered design references. - `parameters`: object | null (optional) - `design_ids`: string (uuid)[] (optional) - `design_count`: integer (optional) ### default # Cancel Operation (/api-reference/operations/cancel-operation) `POST /v1/operations/{operation_id}/cancel` Ask the worker to stop: 202 with the operation in `canceling`; it ends `canceled` at the worker's next boundary. A turn keeps its partial reply and pays for the calls it made; a flat-priced step pays nothing. Cancel on a terminal operation is a no-op 200. Asset generation cannot be stopped once started (409 `not_cancelable`). Requires the `write` scope. ## Parameters - `operation_id` (path): string (uuid) ## Responses ### 200 — The operation was already terminal; nothing changed. OperationOut - `id`: string (uuid) - `kind`: "scene_step" | "session_turn" | "generation" - `status`: "queued" | "running" | "canceling" | "succeeded" | "failed" | "canceled" - `target`: OperationTargetOut - `input`: object (optional) - `progress`: object | null (optional) - `result`: object | null (optional) - `error`: OperationErrorOut | null (optional) - `credits_charged`: integer (optional) - `model`: OperationModelOut | null (optional) - `created_by`: string (uuid) - `created_at`: string (date-time) | null - `started_at`: string (date-time) | null - `finished_at`: string (date-time) | null - `cancel_requested_at`: string (date-time) | null (optional) ### 202 — Successful Response OperationOut - `id`: string (uuid) - `kind`: "scene_step" | "session_turn" | "generation" - `status`: "queued" | "running" | "canceling" | "succeeded" | "failed" | "canceled" - `target`: OperationTargetOut - `input`: object (optional) - `progress`: object | null (optional) - `result`: object | null (optional) - `error`: OperationErrorOut | null (optional) - `credits_charged`: integer (optional) - `model`: OperationModelOut | null (optional) - `created_by`: string (uuid) - `created_at`: string (date-time) | null - `started_at`: string (date-time) | null - `finished_at`: string (date-time) | null - `cancel_requested_at`: string (date-time) | null (optional) ### default # Get Operation (/api-reference/operations/get-operation) `GET /v1/operations/{operation_id}` The operation's state; with `wait`, a bounded long-poll (≤ 60 s) that answers as soon as it is terminal. Requires the `read` scope. ## Parameters - `operation_id` (path): string (uuid) - `wait` (query): integer (optional) — Seconds to hold the request open waiting for a terminal state (bounded long-poll). ## Responses ### 200 — Successful Response OperationOut - `id`: string (uuid) - `kind`: "scene_step" | "session_turn" | "generation" - `status`: "queued" | "running" | "canceling" | "succeeded" | "failed" | "canceled" - `target`: OperationTargetOut - `input`: object (optional) - `progress`: object | null (optional) - `result`: object | null (optional) - `error`: OperationErrorOut | null (optional) - `credits_charged`: integer (optional) - `model`: OperationModelOut | null (optional) - `created_by`: string (uuid) - `created_at`: string (date-time) | null - `started_at`: string (date-time) | null - `finished_at`: string (date-time) | null - `cancel_requested_at`: string (date-time) | null (optional) ### default # List Operations (/api-reference/operations/list-operations) `GET /v1/operations` The operations on one target, newest first: a scene's step history, a session's turn history, a project's generations — receipts, not content. Requires the `read` scope. ## Parameters - `target_id` (query): string (uuid) — A scene, session or project id. - `kind` (query): "scene_step" | "session_turn" | "generation" | null (optional) - `status` (query): "queued" | "running" | "canceling" | "succeeded" | "failed" | "canceled" | null (optional) - `limit` (query): integer (optional) — Items per page - `cursor` (query): string | null (optional) — The `next_cursor` of the previous page; omit for the first page. ## Responses ### 200 — Successful Response CursorPage_OperationOut_ - `items`: OperationOut[] - `next_cursor`: string | null (optional) - `total`: integer | null (optional) — Size of the whole collection, when the platform knows it. ### default # Create Project (/api-reference/projects/create-project) `POST /v1/projects` Create a project owned by the key's user. Plan quotas apply as in the app (402 when exhausted). Requires the `write` scope. ## Request body `application/json` — ProjectCreateIn - `name`: string - `description`: string | null (optional) ## Responses ### 201 — Successful Response ProjectDetailOut - `id`: string (uuid) - `slug`: string - `name`: string - `description`: string | null - `status`: string - `created_at`: string (date-time) - `updated_at`: string (date-time) - `access_level`: string | null (optional) - `scene_count`: integer (optional) - `asset_count`: integer (optional) ### default # Delete Project (/api-reference/projects/delete-project) `DELETE /v1/projects/{project_id}` Delete the project. It and everything in it are hidden at once and purged — rows, stored artifacts, uploaded files and conversation transcripts — 30 days later; until then a Scenebrew user can restore it from the app's trash. The app's rule stands: the last active project cannot be deleted (409). Requires the `write` scope. ## Parameters - `project_id` (path): string (uuid) ## Responses ### 204 — Successful Response ### default # Get Project (/api-reference/projects/get-project) `GET /v1/projects/{project_id}` One project with its counts. Requires the `read` scope. ## Parameters - `project_id` (path): string (uuid) ## Responses ### 200 — Successful Response ProjectDetailOut - `id`: string (uuid) - `slug`: string - `name`: string - `description`: string | null - `status`: string - `created_at`: string (date-time) - `updated_at`: string (date-time) - `access_level`: string | null (optional) - `scene_count`: integer (optional) - `asset_count`: integer (optional) ### default # List Projects (/api-reference/projects/list-projects) `GET /v1/projects` The tenant's active projects the key's user can see. Requires the `read` scope. ## Parameters - `limit` (query): integer (optional) — Items per page - `cursor` (query): string | null (optional) — The `next_cursor` of the previous page; omit for the first page. ## Responses ### 200 — Successful Response CursorPage_ProjectOut_ - `items`: ProjectOut[] - `next_cursor`: string | null (optional) - `total`: integer | null (optional) — Size of the whole collection, when the platform knows it. ### default # Update Project (/api-reference/projects/update-project) `PATCH /v1/projects/{project_id}` Rename, re-describe, archive or unarchive. Requires the `write` scope. ## Parameters - `project_id` (path): string (uuid) ## Request body `application/json` — ProjectUpdateIn - `name`: string | null (optional) - `description`: string | null (optional) - `status`: "active" | "archived" | null (optional) ## Responses ### 200 — Successful Response ProjectDetailOut - `id`: string (uuid) - `slug`: string - `name`: string - `description`: string | null - `status`: string - `created_at`: string (date-time) - `updated_at`: string (date-time) - `access_level`: string | null (optional) - `scene_count`: integer (optional) - `asset_count`: integer (optional) ### default # Create Scene (/api-reference/scenes/create-scene) `POST /v1/projects/{project_id}/scenes` Create a scene in a project — the container for everything the pipeline produces. Run the `generate_scene_*` steps against it, or bring your own SDD and PEG with `upload_scene_artifact`. Requires the `write` scope. ## Parameters - `project_id` (path): string (uuid) ## Request body `application/json` — SceneCreateIn - `name`: string - `workflow`: "t2s" | "i2s" | "s2s" (optional) — Pipeline family. t2s (text-to-scene) is what the API steps drive. - `description`: string | null (optional) - `theme_id`: string (uuid) | null (optional) - `source_file_id`: string (uuid) | null (optional) — An uploaded GLB (POST /v1/files) recorded as the scene's first model version — the starting point of an s2s scene. ## Responses ### 201 — Successful Response SceneOut - `id`: string (uuid) - `slug`: string - `name`: string - `description`: string | null - `workflow`: string - `status`: string - `triangle_count`: integer | null - `asset_count`: integer - `created_at`: string (date-time) - `updated_at`: string (date-time) ### default # Delete Scene (/api-reference/scenes/delete-scene) `DELETE /v1/scenes/{scene_id}` Delete the scene: hidden at once, purged with its artifacts after 30 days. A step still running on it is asked to stop. Requires the `write` scope. ## Parameters - `scene_id` (path): string (uuid) ## Responses ### 204 — Successful Response ### default # Download Scene Artifact (/api-reference/scenes/download-scene-artifact) `GET /v1/scenes/{scene_id}/artifacts/{kind}/content` The artifact's bytes: a 307 to a short-lived presigned URL. Requires the `read` scope. ## Parameters - `scene_id` (path): string (uuid) - `kind` (path): "sdd" | "peg" | "blueprint" | "model" | "layout_preview" | "thumbnail" - `version` (query): integer | null (optional) ## Responses ### 307 — Redirect to the presigned artifact bytes. ### default # Get Scene Artifact (/api-reference/scenes/get-scene-artifact) `GET /v1/scenes/{scene_id}/artifacts/{kind}` One artifact: the current version, or `version`. With `inline`, the SDD or PEG text travels in the record — what a host agent wants to read. Requires the `read` scope. ## Parameters - `scene_id` (path): string (uuid) - `kind` (path): "sdd" | "peg" | "blueprint" | "model" | "layout_preview" | "thumbnail" - `version` (query): integer | null (optional) — A specific version; the current one when omitted. - `inline` (query): boolean (optional) — Include the text of an SDD or PEG in `content`. ## Responses ### 200 — Successful Response SceneArtifactDetailOut - `id`: string (uuid) - `kind`: string — The artifact's role: sdd, peg, blueprint, model (the GLB), layout_preview, thumbnail. - `version`: integer - `is_current`: boolean - `content_type`: string | null - `size_bytes`: integer (int64) | null - `content_hash`: string | null - `created_at`: string (date-time) | null - `download_url`: string — Short-lived presigned URL for the artifact bytes. Fetch it promptly; re-list to obtain a fresh one after expiry. - `download_expires_in`: integer — Seconds until download_url stops working. - `download_expires_at`: string (date-time) | null (optional) — Absolute expiry (RFC 3339) of download_url — the same instant. - `content`: string | null (optional) ### default # Get Scene (/api-reference/scenes/get-scene) `GET /v1/scenes/{scene_id}` One scene with its current artifacts (presigned). Requires the `read` scope. ## Parameters - `scene_id` (path): string (uuid) ## Responses ### 200 — Successful Response SceneDetailOut - `id`: string (uuid) - `slug`: string - `name`: string - `description`: string | null - `workflow`: string - `status`: string - `triangle_count`: integer | null - `asset_count`: integer - `created_at`: string (date-time) - `updated_at`: string (date-time) - `project_id`: string (uuid) - `artifacts`: SceneArtifactOut[] (optional) ### default # List Scene Artifacts (/api-reference/scenes/list-scene-artifacts) `GET /v1/scenes/{scene_id}/artifacts` The scene's artifacts — current versions unless `all_versions` — each with a short-lived presigned download URL. Requires the `read` scope. ## Parameters - `scene_id` (path): string (uuid) - `kind` (query): "sdd" | "peg" | "blueprint" | "model" | "layout_preview" | "thumbnail" | null (optional) — Only this kind. - `all_versions` (query): boolean (optional) — Every version, not only the current one of each kind. ## Responses ### 200 — Successful Response SceneArtifactsResponse - `items`: SceneArtifactOut[] ### default # List Scene Sessions (/api-reference/scenes/list-scene-sessions) `GET /v1/scenes/{scene_id}/sessions` The conversations that refer to this scene. Requires the `read` scope. ## Parameters - `scene_id` (path): string (uuid) ## Responses ### 200 — Successful Response SceneSessionOut[] ### default # List Scenes (/api-reference/scenes/list-scenes) `GET /v1/projects/{project_id}/scenes` The project's scenes; 404 when the project is not visible to the key. Requires the `read` scope. ## Parameters - `project_id` (path): string (uuid) - `limit` (query): integer (optional) — Items per page - `cursor` (query): string | null (optional) — The `next_cursor` of the previous page; omit for the first page. ## Responses ### 200 — Successful Response CursorPage_SceneOut_ - `items`: SceneOut[] - `next_cursor`: string | null (optional) - `total`: integer | null (optional) — Size of the whole collection, when the platform knows it. ### default # Run Scene Step (/api-reference/scenes/run-scene-step) `POST /v1/scenes/{scene_id}/steps` Run one step on the scene as an Operation (202 + `Location`). `generate_scene_sdd` writes the Scene Design Document from `brief`; `generate_scene_peg` builds the Player Experience Graph from the current SDD (409 `missing_sdd`); `generate_scene_blueprint` turns the current PEG into a blueprint (the swfbpx package) and a layout preview (409 `missing_peg`); `generate_scene_model` cooks the current blueprint into the model (409 `missing_blueprint`). Rerunning a step versions its artifact. The AI steps deduct credits once they complete; the procedural steps are free. One step at a time per scene (409 `step_in_progress`). `Prefer: wait=N` holds the response. Requires the `write` scope. ## Parameters - `scene_id` (path): string (uuid) - `Idempotency-Key` (header): string | null (optional) — Optional. Repeating a request with the same key returns what it already created (200) instead of running it again. - `Prefer` (header): string | null (optional) — RFC 7240. `wait=N` (N ≤ 60) holds the response until the operation is terminal or N seconds passed: 200 with the finished operation, or the same 202 as without it. `respond-async` (the default) returns the queued operation at once. ## Request body `application/json` — SceneStepCreateIn - `step`: "generate_scene_sdd" | "revise_scene_sdd" | "generate_scene_peg" | "revise_scene_peg" | "generate_scene_blueprint" | "generate_scene_model" | "analyze_scene_image" | "decorate_scene" — generate_scene_sdd and generate_scene_peg are AI steps and cost credits; generate_scene_blueprint and generate_scene_model are procedural and free. revise_scene_sdd, revise_scene_peg, analyze_scene_image and decorate_scene are declared and answer 501 until available. - `brief`: string | null (optional) — The design brief. Required for generate_scene_sdd. - `instructions`: string | null (optional) — Edit instructions for the revise_* steps and decorate_scene. - `file_id`: string (uuid) | null (optional) — The uploaded reference image for analyze_scene_image. - `model`: string | null (optional) — The model an AI step runs on (GET /v1/models); the default when omitted. ## Responses ### 200 — The operation finished within `Prefer: wait`, or the Idempotency-Key replayed an operation that already exists. OperationOut - `id`: string (uuid) - `kind`: "scene_step" | "session_turn" | "generation" - `status`: "queued" | "running" | "canceling" | "succeeded" | "failed" | "canceled" - `target`: OperationTargetOut - `input`: object (optional) - `progress`: object | null (optional) - `result`: object | null (optional) - `error`: OperationErrorOut | null (optional) - `credits_charged`: integer (optional) - `model`: OperationModelOut | null (optional) - `created_by`: string (uuid) - `created_at`: string (date-time) | null - `started_at`: string (date-time) | null - `finished_at`: string (date-time) | null - `cancel_requested_at`: string (date-time) | null (optional) ### 202 — Successful Response OperationOut - `id`: string (uuid) - `kind`: "scene_step" | "session_turn" | "generation" - `status`: "queued" | "running" | "canceling" | "succeeded" | "failed" | "canceled" - `target`: OperationTargetOut - `input`: object (optional) - `progress`: object | null (optional) - `result`: object | null (optional) - `error`: OperationErrorOut | null (optional) - `credits_charged`: integer (optional) - `model`: OperationModelOut | null (optional) - `created_by`: string (uuid) - `created_at`: string (date-time) | null - `started_at`: string (date-time) | null - `finished_at`: string (date-time) | null - `cancel_requested_at`: string (date-time) | null (optional) ### default # Update Scene (/api-reference/scenes/update-scene) `PATCH /v1/scenes/{scene_id}` Rename, re-describe or re-theme a scene. Only the fields sent change; `theme_id: null` un-themes. Requires the `write` scope. ## Parameters - `scene_id` (path): string (uuid) ## Request body `application/json` — SceneUpdateIn - `name`: string | null (optional) - `description`: string | null (optional) - `theme_id`: string (uuid) | null (optional) ## Responses ### 200 — Successful Response SceneOut - `id`: string (uuid) - `slug`: string - `name`: string - `description`: string | null - `workflow`: string - `status`: string - `triangle_count`: integer | null - `asset_count`: integer - `created_at`: string (date-time) - `updated_at`: string (date-time) ### default # Upload Scene Artifact (/api-reference/scenes/upload-scene-artifact) `PUT /v1/scenes/{scene_id}/artifacts/{kind}` Bring your own document: write a new version of the SDD (Markdown), PEG (JSON) or blueprint (swfbpx zip) from the request body, then run only the free steps. Identical content is not a new version. Requires the `write` scope. ## Parameters - `scene_id` (path): string (uuid) - `kind` (path): "sdd" | "peg" | "blueprint" ## Request body `application/json` — object ## Responses ### 201 — Successful Response SceneArtifactDetailOut - `id`: string (uuid) - `kind`: string — The artifact's role: sdd, peg, blueprint, model (the GLB), layout_preview, thumbnail. - `version`: integer - `is_current`: boolean - `content_type`: string | null - `size_bytes`: integer (int64) | null - `content_hash`: string | null - `created_at`: string (date-time) | null - `download_url`: string — Short-lived presigned URL for the artifact bytes. Fetch it promptly; re-list to obtain a fresh one after expiry. - `download_expires_in`: integer — Seconds until download_url stops working. - `download_expires_at`: string (date-time) | null (optional) — Absolute expiry (RFC 3339) of download_url — the same instant. - `content`: string | null (optional) ### default # Create Session (/api-reference/sessions/create-session) `POST /v1/projects/{project_id}/sessions` Open a conversation with its first message. The context named in the request is attached before the agent runs; the reply arrives on the returned `initial_turn` operation, which `Prefer: wait=N` waits for. If that turn fails the session and the message stay — retry with `send_session_message`. Requires the `write` scope. ## Parameters - `project_id` (path): string (uuid) - `Idempotency-Key` (header): string | null (optional) — Optional. Repeating a request with the same key returns what it already created (200) instead of running it again. - `Prefer` (header): string | null (optional) — RFC 7240. `wait=N` (N ≤ 60) holds the response until the operation is terminal or N seconds passed: 200 with the finished operation, or the same 202 as without it. `respond-async` (the default) returns the queued operation at once. ## Request body `application/json` — SessionCreateIn - `text`: string - `title`: string | null (optional) - `context`: SessionContextRefIn[] (optional) - `file_ids`: string (uuid)[] (optional) — Uploaded files the first message refers to. - `model`: string | null (optional) ## Responses ### 201 — Successful Response SessionCreatedOut - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) - `title`: string | null - `status`: string - `pinned`: boolean (optional) - `model`: string - `turn_count`: integer - `last_message_preview`: string | null (optional) - `context`: SessionContextItemOut[] (optional) - `created_at`: string (date-time) | null - `updated_at`: string (date-time) | null - `last_activity_at`: string (date-time) | null - `initial_turn`: OperationOut ### default # Delete Session (/api-reference/sessions/delete-session) `DELETE /v1/sessions/{session_id}` Delete the conversation: hidden at once, its transcript purged after 30 days; a running turn is asked to stop first. Scenes and artifacts it produced survive. Requires the `write` scope. ## Parameters - `session_id` (path): string (uuid) ## Responses ### 204 — Successful Response ### default # Get Session (/api-reference/sessions/get-session) `GET /v1/sessions/{session_id}` The session with its context — the scenes and library entities it refers to. Requires the `read` scope. ## Parameters - `session_id` (path): string (uuid) ## Responses ### 200 — Successful Response SessionOut - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) - `title`: string | null - `status`: string - `pinned`: boolean (optional) - `model`: string - `turn_count`: integer - `last_message_preview`: string | null (optional) - `context`: SessionContextItemOut[] (optional) - `created_at`: string (date-time) | null - `updated_at`: string (date-time) | null - `last_activity_at`: string (date-time) | null ### default # Link Session Context (/api-reference/sessions/link-session-context) `POST /v1/sessions/{session_id}/context` Pull a scene, theme, design, recipe or asset into the conversation's context. Attaching twice is a 409. Requires the `write` scope. ## Parameters - `session_id` (path): string (uuid) ## Request body `application/json` — SessionContextRefIn - `kind`: "scene" | "theme" | "design" | "recipe" | "asset" - `id`: string (uuid) - `role`: "referenced" | "created" | "modified" (optional) ## Responses ### 201 — Successful Response SessionContextItemOut - `kind`: string - `role`: string - `id`: string (uuid) - `name`: string | null (optional) ### default # List Session Messages (/api-reference/sessions/list-session-messages) `GET /v1/sessions/{session_id}/messages` A page of the conversation, newest first, as the app shows it. `next_cursor` is the id to pass as `before` for the next page. Requires the `read` scope. ## Parameters - `session_id` (path): string (uuid) - `limit` (query): integer (optional) - `before` (query): string | null (optional) — Only messages older than this message id. - `after` (query): string | null (optional) — Only messages newer than this message id. ## Responses ### 200 — Successful Response SessionMessagesOut - `session_id`: string (uuid) - `items`: MessageOut[] - `next_cursor`: string | null (optional) ### default # List Sessions (/api-reference/sessions/list-sessions) `GET /v1/projects/{project_id}/sessions` The project's sessions — the designers' conversations — pinned first, then most recently active, each with the entities it refers to. Requires the `read` scope. ## Parameters - `project_id` (path): string (uuid) - `status` (query): "active" | "archived" | null (optional) — Only this status. - `q` (query): string | null (optional) — Title search. - `limit` (query): integer (optional) — Items per page - `cursor` (query): string | null (optional) — The `next_cursor` of the previous page; omit for the first page. ## Responses ### 200 — Successful Response CursorPage_SessionOut_ - `items`: SessionOut[] - `next_cursor`: string | null (optional) - `total`: integer | null (optional) — Size of the whole collection, when the platform knows it. ### default # Send Session Message (/api-reference/sessions/send-session-message) `POST /v1/sessions/{session_id}/messages` Post a message in the key's user's name and run one agent turn on it, as an Operation (202 + `Location`). The agent answers as it would in the app — reading the session's context, running tools, writing artifacts to the scenes it touches — and the turn is billed like a web turn. One turn at a time per session (409 `turn_in_progress`). `Prefer: wait=N` holds the response; `result.reply` then carries the answer. Requires the `write` scope. ## Parameters - `session_id` (path): string (uuid) - `Idempotency-Key` (header): string | null (optional) — Optional. Repeating a request with the same key returns what it already created (200) instead of running it again. - `Prefer` (header): string | null (optional) — RFC 7240. `wait=N` (N ≤ 60) holds the response until the operation is terminal or N seconds passed: 200 with the finished operation, or the same 202 as without it. `respond-async` (the default) returns the queued operation at once. ## Request body `application/json` — MessageCreateIn - `text`: string - `file_ids`: string (uuid)[] (optional) ## Responses ### 200 — The operation finished within `Prefer: wait`, or the Idempotency-Key replayed an operation that already exists. OperationOut - `id`: string (uuid) - `kind`: "scene_step" | "session_turn" | "generation" - `status`: "queued" | "running" | "canceling" | "succeeded" | "failed" | "canceled" - `target`: OperationTargetOut - `input`: object (optional) - `progress`: object | null (optional) - `result`: object | null (optional) - `error`: OperationErrorOut | null (optional) - `credits_charged`: integer (optional) - `model`: OperationModelOut | null (optional) - `created_by`: string (uuid) - `created_at`: string (date-time) | null - `started_at`: string (date-time) | null - `finished_at`: string (date-time) | null - `cancel_requested_at`: string (date-time) | null (optional) ### 202 — Successful Response OperationOut - `id`: string (uuid) - `kind`: "scene_step" | "session_turn" | "generation" - `status`: "queued" | "running" | "canceling" | "succeeded" | "failed" | "canceled" - `target`: OperationTargetOut - `input`: object (optional) - `progress`: object | null (optional) - `result`: object | null (optional) - `error`: OperationErrorOut | null (optional) - `credits_charged`: integer (optional) - `model`: OperationModelOut | null (optional) - `created_by`: string (uuid) - `created_at`: string (date-time) | null - `started_at`: string (date-time) | null - `finished_at`: string (date-time) | null - `cancel_requested_at`: string (date-time) | null (optional) ### default # Unlink Session Context (/api-reference/sessions/unlink-session-context) `DELETE /v1/sessions/{session_id}/context/{kind}/{entity_id}` Remove an entity from context, addressed by what it is. Requires the `write` scope. ## Parameters - `session_id` (path): string (uuid) - `kind` (path): "scene" | "theme" | "design" | "recipe" | "asset" - `entity_id` (path): string (uuid) ## Responses ### 204 — Successful Response ### default # Update Session (/api-reference/sessions/update-session) `PATCH /v1/sessions/{session_id}` Rename, pin, archive, or switch the model the next turns run on. Requires the `write` scope. ## Parameters - `session_id` (path): string (uuid) ## Request body `application/json` — SessionUpdateIn - `title`: string | null (optional) - `pinned`: boolean | null (optional) - `model`: string | null (optional) - `status`: "active" | "archived" | null (optional) ## Responses ### 200 — Successful Response SessionOut - `id`: string (uuid) - `slug`: string - `project_id`: string (uuid) - `title`: string | null - `status`: string - `pinned`: boolean (optional) - `model`: string - `turn_count`: integer - `last_message_preview`: string | null (optional) - `context`: SessionContextItemOut[] (optional) - `created_at`: string (date-time) | null - `updated_at`: string (date-time) | null - `last_activity_at`: string (date-time) | null ### default # Get Credits (/api-reference/identity/get-credits) `GET /v1/me/credits` The tenant's credit balance and its most recent ledger entries, so a caller can assert what an operation cost. Requires the `read` scope. ## Parameters - `limit` (query): integer (optional) — Ledger entries to include ## Responses ### 200 — Successful Response CreditsOut - `balance`: integer - `plan`: string | null - `period_ends_at`: string (date-time) | null - `ledger`: LedgerEntryOut[] ### default # Get Me (/api-reference/identity/get-me) `GET /v1/me` Who the presented API key acts as. ## Responses ### 200 — Successful Response MeResponse - `tenant`: TenantOut - `user`: UserOut - `key`: ApiKeyOut - `scopes`: string[] - `plan`: string ### default # List Models (/api-reference/identity/list-models) `GET /v1/models` The models this tenant may pick — the same list the web UI offers: Scenebrew's default (credits) and the vendors the tenant stored keys for (BYOK). Their ids are what `model` arguments accept. Requires the `read` scope. ## Responses ### 200 — Successful Response ModelsOut - `items`: ModelOut[] ### default