Content Studio — Proteus API Reference¶
Summary¶
The Proteus API is how an organization authors reusable universal assets — characters,
locations, looks, voices, props and products — and casts them into productions by reference.
It covers four route families: org-level asset authoring (/org-assets), world bundles
(/mythos), brands and their rights (/brands), and product placement
(/productions/:id/placements), plus production deletion.
For scenes, beats, coverage, ensembles and entity versions — how a script becomes shots — see the companion Production Pipeline API reference.
Feature architecture: Proteus — Universal Asset System.
Quick Links¶
- Authentication
- Token cost
- Org assets
- Worlds (Mythos)
- Brands (Kleos)
- Placements (Kleos)
- Productions
- Errors
Authentication¶
All endpoints require a bearer JWT and resolve the caller's organization.
The organization may also be supplied as the organization_id query parameter or the
magieva-selected-org-id cookie. Precedence is header → query → cookie.
⚠️ Mutating requests require an explicit organization.
POST,PATCH,PUTandDELETEare refused with 400no organization selectedwhen no organization resolves. There is no default organization — a request that does not say which organization it is acting for is rejected rather than being applied to whichever one happens to be first.GETrequests are not subject to this gate.
Every resource is organization-scoped. An id belonging to another organization reads as 404, not 403 — existence is not disclosed.
Token cost¶
Most endpoints are metadata operations and are quota-exempt — they write rows, flip flags or record decisions and call no AI provider. Two endpoints spend intelligence tokens and are gated by the standard quota middleware.
| Endpoint | Cost |
|---|---|
POST /org-assets/describe |
Token-gated — one short LLM turn |
POST /org-assets/:uaId/options/generate |
Token-gated — image generation, estimate scales with iterations |
| Everything else in this reference | Quota-exempt |
A token-gated call that exceeds the organization's quota returns 402 from the quota gate before the handler runs. See Intelligence Tokens.
Org assets¶
The org-level authoring substrate. An asset is authored as a draft at organization scope (no production), runs the concept → approve → clearance discipline, and is then published into the organization's library.
POST /org-assets¶
Create a draft. Quota-exempt.
Request
{
"asset_type": "character",
"name": "Aurora",
"appearance": "mid-thirties, close-cropped silver hair, field jacket",
"license": "org-internal",
"tags": ["lead", "season-1"],
"brand_id": null
}
| Field | Type | Required | Notes |
|---|---|---|---|
asset_type |
string | ✅ | character, location, set, prop, fx, style, voice, music, camera, product |
name |
string | ✅ | |
appearance |
string | The generation prompt. A blank one produces a generic asset. | |
license |
string | Defaults to org-internal |
|
tags |
string[] | Defaults to [] |
|
brand_id |
uuid | Kleos. Must be a brand owned by this organization, else 404 | |
metadata |
object | Studio bookkeeping — production code, readiness, blockers, source anchor, sheets. Stored verbatim; returned on every list row; merged on PATCH |
Response 200
GET /org-assets¶
List assets. Quota-exempt.
| Query param | Default | Notes |
|---|---|---|
status |
draft |
draft, published, archived |
asset_type |
— | Filter by type |
q |
— | Substring match on name or appearance |
all_versions |
false |
See below |
One row per asset, not per version. Results are collapsed by lineage_id, and each row
carries versions (count) and lineage_usage_count (total reuse across the lineage) so a row
can show "3 versions" and total reuse without a second round trip. Pass all_versions=true for
the flat, uncollapsed list.
PATCH /org-assets/:uaId¶
Rename a draft or rewrite its description. Quota-exempt. Sets updated_at.
{ "name": "Aurora Vale", "appearance": "…", "brand_id": "…", "metadata": { "keyframe_readiness": "PREVIS READY" }, "tags": ["gna2", "hero"] }
All fields optional. brand_id is set-only in the current slice — clearing a brand ships
with the brand picker. name, appearance and brand_id are draft-only (a published
asset is pinned by the productions that imported it — use New version). metadata (merged,
top-level keys win) and tags (replaced wholesale) are bookkeeping, not identity, and are
accepted on published assets too. Archived assets are refused (409).
POST /org-assets/describe¶
Token-gated. Turn a name into a description worth generating from.
POST /org-assets/:uaId/options/generate¶
Token-gated. Generate candidate concepts. iterations controls how many; the quota
estimate scales with it.
GET /org-assets/:uaId/options¶
List generated options. Quota-exempt.
POST /org-assets/:uaId/options/:taskId/approve¶
Pick an option. Quota-exempt — the option was already paid for at generation time.
POST /org-assets/:uaId/set-reference¶
Bring your own canon. Turn an already-uploaded image (a partner turntable, a keyframe
reference sheet) into a Looks version of the asset — and, with approve, into its reference
image in the same call. Quota-exempt: it records an upload that already happened.
{ "asset_id": "…", "approve": true, "label": "CHR2-001 · MODEL + WARDROBE", "metadata": { "sheet_type": "MODEL + WARDROBE" } }
| Field | Type | Required | Notes |
|---|---|---|---|
asset_id |
uuid | ✅ | An assets row this organization owns (upload it via POST /api/storage/upload first). Another org's asset reads as 400 asset not found |
approve |
bool | Also set reference_image_url and flip the concept gate to approved |
|
label |
string | Shown on the version card | |
metadata |
object | Stored on the synthetic task and the asset link |
Response 200
{ "success": true, "universal_asset_id": "…", "task_id": "…", "asset_id": "…", "version": 2, "approved": true, "reference_image_url": "https://…" }
The upload appears in GET /org-assets/:uaId/options as a completed image_to_image version
(source: manual_upload), so it can also be approved later through the normal approve route.
Both routes share one write, so a generated look and an uploaded sheet cannot drift.
Refuses with 409 when the asset has no readable URL yet or the org asset is archived.
GET /org-assets/:uaId/pipeline¶
The asset's pipeline stages: stage, status, artifact_asset_id, approved_by,
approved_at, notes, metadata, updated_at.
POST /org-assets/:uaId/pipeline/:stage¶
Set a stage. Quota-exempt — flips a gate flag.
Upserts on (entity_type, entity_id, stage).
GET /org-assets/:uaId/clearance¶
{
"clearances": [ { "rights_type": "likeness", "status": "granted", "holder": "…" } ],
"blocked": false,
"block_reason": ""
}
POST /org-assets/:uaId/clearance¶
Record a rights decision. Quota-exempt.
{
"rights_type": "likeness",
"status": "granted",
"holder": "…",
"reference": "…",
"expires_at": "2027-01-01T00:00:00Z",
"notes": ""
}
POST /org-assets/:uaId/publish¶
Publish a draft into the organization library. Quota-exempt — publishes what is already built.
Clearance-gated. Returns 403 rights not cleared: <reason> if any clearance blocks.
Only a draft can be published. Republishing, or publishing another organization's asset,
affects zero rows and returns 404.
Deleting a published asset archives it.
statusbecomesarchived: hidden from the library and from pickers, unusable in worlds, but the row survives so every production that imported it keeps its provenance.
Worlds (Mythos)¶
A world bundles published assets — cast, standing locations, look, voices — under one name, so a new production can start already populated. All quota-exempt: naming a world and listing what is in it generates nothing.
POST /mythos¶
{ "name": "Hollow Pines", "description": "…", "metadata": { "production": "…", "cover_asset_id": "…" } }
201. Returns 409 if a world of that name already exists in the organization.
metadata is the world's bible — source folder, registers, cover, counts, standards
documents. It is bookkeeping, never authoritative for membership. PATCH /mythos/:id
accepts metadata too and merges it (top-level keys win).
GET /mythos¶
List the organization's worlds.
GET /mythos¶
Each world carries member_count, published_count (what a production can start from),
draft_count (still in development) and metadata.
GET /mythos/:mythosId¶
A world and its members. Each member carries what a board needs without a second round trip:
lineage_id, version, status, appearance, reference_image_url, apose_url,
rigged_glb_url, lora_url, trigger_word, has_rig, has_lora, tags, metadata,
gates (stage → status from asset_pipeline_stages), usage_count, updated_at; plus
members_by_type, published_count and draft_count on the world.
POST /mythos/:mythosId/members¶
A published asset always joins. A draft joins only with allow_draft: true — the world
is then the bible under development, not just the shelf of finished identities. Production
instantiation (castAtCreation) imports published members only, so admitting a draft never
hands a production a half-finished identity. Archived assets are refused (409). Returns
409 this world has no backing group if the world's backing group is missing.
DELETE /mythos/:mythosId/members/:uaId¶
Remove a member. The asset itself is untouched.
DELETE /mythos/:mythosId¶
Delete the world. Removes the membership structure only — member assets are not deleted. Backing-group teardown is best-effort.
Instantiating a production from a world¶
Worlds are consumed at production creation, not through this route family. POST /productions
accepts three casting variables:
| Variable | Meaning |
|---|---|
variables.cast[] |
Universal asset ids — import each |
variables.selected_library_ids[] |
Group ids — import their universal-asset members |
variables.mythos_ids[] |
World ids — resolve to groups, then as above |
The 201 response body includes a cast object reporting what actually happened:
{
"cast": {
"imported": [ { "universal_asset_id": "…", "name": "Aurora" } ],
"failed": [ { "universal_asset_id": "…", "error": "…" } ],
"warnings": ["representation copy failed: un-posable"]
}
}
Casting is best-effort — a failed import never fails production creation — but it is never silent. A world that contributed nothing says so, rather than yielding a normal-looking empty production.
Brands (Kleos)¶
POST /brands¶
Quota-exempt — naming a brand generates nothing.
{
"name": "Halcyon",
"brand_kind": "fictional",
"guidelines": {},
"consent_ref": null,
"parent_brand_id": null
}
| Field | Required | Notes |
|---|---|---|
name |
✅ | Unique per organization, case-insensitive |
brand_kind |
✅ | real or fictional — anything else is 400 |
guidelines |
Free-form object | |
consent_ref |
Pointer to the consent artifact | |
parent_brand_id |
Sub-brand hierarchy |
201. Returns 409 on a duplicate name within the organization.
Fictional brands are born with rights granted. Real brands start uncleared and must be cleared before use.
GET /brands¶
Any asset type may carry a brand_id — a product, a jingle, a look, a flagship location — so
brands are not product-only.
POST /brands/:brandId/rights¶
Quota-exempt — records a rights decision. This is the transition the clearance gate depends on: grant when the licence lands, revoke when it lapses.
| Field | Required | Notes |
|---|---|---|
rights_status |
✅ | none | pending | granted | revoked. Anything else is 400 naming the options |
consent_ref |
Pointer to the consent artifact. Omitted or empty leaves the existing value | |
notes |
Free text, stored on the brand |
200
409 on a fictional brand:
{
"error": "a fictional brand has no external rights to record",
"detail": "Fictional brands are cleared by construction. Only 'real' brands carry a rights status."
}
The refusal is deliberate rather than accepting a write the gate would then ignore — a UI showing
revoked on a brand that still renders is worse than not offering the control at all.
Revoking is not retroactive, and not a delete. Work already delivered under a licence stays valid; only the next render is refused. That is why this writes a status rather than unpicking placements. Both ends of the transition are logged at INFO, because "who could film this brand, and from when" cannot be answered from the current value alone.
DELETE /brands/:brandId¶
Quota-exempt. A hard delete — unlike a published asset's archive, a brand carries no provenance of its own; it is a label.
409 while anything still wears it:
{
"error": "this brand is still in use",
"detail": "Deleting it would leave assets and placements pointing at a brand that no longer exists, and a missing brand reads as 'no brand' at the clearance gate — which would unblock them. Revoke its rights instead, or detach it from what carries it first.",
"assets": 3,
"placements": 1,
"sub_brands": 0
}
The count includes archived assets — an archived asset's provenance still names its brand.
Placements (Kleos)¶
A placement answers "which Product fills which prop-role in this production, under which
terms". It reuses the cast shape exactly: a role in the story (production_props) ← the cast
identity (a Product asset lineage), with UNIQUE(version_id, prop_id) enforcing one product per
prop-role per version, the way casting enforces one performer per character.
brand_id is denormalized from the product at write time, so terms and reporting read it without
joining back through the asset system — and it records the brand as it was when the deal was
struck.
POST /productions/:id/placements¶
Quota-exempt — naming a placement generates nothing and calls no provider.
{
"prop_id": "…",
"product_lineage_id": "…",
"placement_terms": { "exclusivity": "category", "territory": "NA" }
}
| Field | Required | Notes |
|---|---|---|
prop_id |
✅ | Must belong to this production |
product_lineage_id |
✅ | Must resolve to a published product asset in the caller's org |
placement_terms |
Free-form object, stored as jsonb |
201
{
"placement_id": "…",
"prop_id": "…",
"product_lineage_id": "…",
"product_name": "Halcyon Field Watch",
"brand_id": "…"
}
Both halves of the pairing are validated at write time. A caller who may write to production
A must not be able to name a prop belonging to production B, and a product from another
organization must not become filmable here just because the production is ours. Either failure
reads as 404 (prop / published product) — the caller learns nothing about what they cannot
see. Draft products are not castable; archived ones are gone.
409 when the prop-role is already filled:
Not gated on clearance, deliberately. A placement is the record of a deal, and deals get recorded while the licence is still being negotiated. Refusing the write would also make the placement panel's "blocked" badge unreachable, since the row it renders could never exist. Enforcement belongs on the path that turns a placement into imagery.
GET /productions/:id/placements¶
The production's placements for its current version, with product and brand resolved so the Assets phase renders a row without N follow-up calls.
DELETE /productions/:id/placements/:placementId¶
Quota-exempt.
Productions¶
DELETE /productions/:id¶
Delete a production and everything under it.
Returns 409 while an assembly is actively compiling:
{ "error": "an assembly is compiling — wait for it to finish (or for the reaper to fail it) before deleting" }
A stuck assembly is failed automatically after 45 minutes, so this state resolves on its own.
200
Errors¶
| Status | Meaning |
|---|---|
| 400 | Malformed id or body; or a mutating request with no organization selected |
| 401 | Missing or invalid authentication |
| 402 | Token quota exceeded (token-gated endpoints only) |
| 403 | rights not cleared: <reason> — publish blocked by clearance |
| 404 | Not found — including resources owned by another organization |
| 409 | Conflict — duplicate name, assembly compiling, or a precondition unmet |
| 500 | Server error |
Error bodies are { "error": "…" }, sometimes with a detail field.
Related Documentation¶
- Universal assets overview — how assets, versions and worlds fit together
- Intelligence tokens — how token-gated endpoints are billed and quoted