Library
Themes, designs, recipes and assets — the reusable pieces a scene draws on — and files, the way binaries enter the API.
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
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:
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), 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
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
# 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
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.
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=<digest>, 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.
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:
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 # "<sha256>" — the role's digestFilling 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 |
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=<digest> 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.
Generation
POST /v1/projects/{project_id}/generations asks the AI to create a library
entity from a brief; it runs as an operation 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.