HTTP API for managing workspaces, projects, scenes, and renders programmatically — same surface area the editor UI uses.
**Attribution.** Every write records who made it: the API key's name, plus the agent session the caller declares with the optional `X-Typecut-Session: <id>` header (1–128 chars of `[A-Za-z0-9._:@/+=-]`, stable for the life of the session) and `X-Typecut-Session-Label: <text>` (≤ 80 chars, percent-encode non-ASCII). A person with the project open in the editor sees the label as the agent's presence, and `GET /projects/{id}/changes` lists each write with its `actor`. Malformed values are ignored, never rejected.
Base URL
https://app.typecut.ai/api/v1
Authentication
Bearer API keys. Create one in Settings → API keys (available on Creator plans and up), then send it on every request:
Authorization: Bearer tc_live_…
GET/projects
List projects
Returns the projects the authenticated API key can access. Account-level keys see all projects in the account; workspace-scoped keys see only projects in their workspace. Cursor-paginated (created_at DESC, id DESC).
Parameters
Name
In
Description
cursor
query
limit
query
Response — 200
Field
Type
Description
datarequired
Project[]
next_cursorrequired
string | null
200 A page of projects.400 Invalid query parameters.401 Missing or invalid API key.
POST/projects
Create a project
Create a new project. Mode-discriminated by `mode`:
- `templates` — runs the full plan → scaffold → generate pipeline from a template + a topic (`prompt`) or your own `script` (markdown-structured supported). The main path.
- `blank` — an empty project with one chapter, no planning; edit it via later API calls.
For `templates`, call GET /v1/projects/requirements first to learn the required inputs (and preview a script's parsed structure).
Request body
Variant 1 — CreateTemplateProjectRequest
Field
Type
Description
moderequired
"templates"
Creation mode "templates" runs the full plan → scaffold → generate pipeline from a template + a topic (prompt) or your own script.
templateSlugrequired
string
Template to generate from (its scene vocabulary + pacing). Required.
workspaceId
string | null
Workspace to place the project in. Required when the account has multiple workspaces (else a 422 lists the candidates); omitted for workspace-scoped keys.
prompt
string
A topic/brief — the model writes the script. Provide this OR `script`.
sources
object[]
Reference links to ground the generation. We read each (article text, YouTube transcript, PDF, or a website's pages) into a structured digest that feeds the script. The `kind` is detected server-side.
script
string
Your own finished narration, used as the source. Provide this OR `prompt`. May be a markdown document (see `scriptFormat`): `# Title`, `## Chapters`, paragraphs become beats. Text is used verbatim (see `scriptFidelity`).
scriptFidelity
"verbatim" | "adapt"
How faithfully to keep your `script`. 'verbatim' (segment only, never reword) or 'adapt' (default — the model may restructure/rephrase).
scriptFormat
"markdown" | "plaintext" | "auto"
How to parse `script` into chapters/blocks. 'auto' (default) detects markdown headings (`#`/`##`); 'markdown' forces it; 'plaintext' treats it as flat prose. With markdown headings, `##` define chapters and `#` becomes the project title.
name
string
Project name. Defaults to the script `# H1`, else the topic, else the template.
usePresets
boolean
Fill unset fields from the workspace presets (voice, captions, etc.). Default true.
bulkGenerate
boolean
Run generation (TTS + scenes) after scaffolding. Default true. False = plan only.
ttsPresetId
string | null
Voice (tts_presets id). Required unless the workspace has a default voice.
durationSeconds
integer
Target length (prompt mode). Ignored in verbatim script mode (length is implied).
aspectRatio
"16:9" | "9:16" | "4:3" | "1:1"
Aspect ratio. Defaults to the workspace preset, else 16:9.
musicTrackId
string | null
Optional background music (music_tracks id; see GET /v1/catalogs/music).
audio
object
Audio mix masters (0–2), mirroring the editor VO/Music/SFX faders. Each is applied multiplicatively in the render mix. Omit any to keep the editor-neutral default (vo 1, music 0.3, sfx 0.7). Read back via GET /v1/projects/{id} → `audio`.
logo
object
Channel logo / watermark — a static image pinned to a corner of every scene. `scale` is logo width as a fraction of video width (default 0.12). Omit to inherit the workspace branding preset. Read back via GET /v1/projects/{id} → `logo`.
Variant 2 — CreateBlankProjectRequest
Field
Type
Description
moderequired
"blank"
Creation mode. "blank" creates an empty project with one chapter, no LLM planning. Additional modes (e.g. "prompt") will be added in a future release.
name
string
Project name. Defaults to "Untitled Project".
workspaceId
string | null
Workspace to place the project in. Required for workspace-scoped API keys (may be omitted — the scoped workspace is used).
aspectRatio
"16:9" | "9:16" | "4:3" | "1:1"
Project aspect ratio. Defaults to "16:9".
ttsPresetId
string | null
Optional default TTS preset for the project.
Response — 201
Field
Type
Description
datarequired
object
201 Project created.400 Invalid request body.401 Missing or invalid API key.402 Plan limit reached (free tier project cap, lapsed subscription, etc).403 API key cannot create projects in the requested workspace.
GET/media
List media assets
Returns the media library assets the API key can access (confirmed, excluding scene thumbnails). RLS-scoped to the account; workspace-scoped keys see only their workspace. Cursor-paginated (created_at DESC, id DESC). Optional `project_id` filter.
Parameters
Name
In
Description
cursor
query
limit
query
project_id
query
Response — 200
Field
Type
Description
datarequired
MediaAsset[]
next_cursorrequired
string | null
200 A page of media assets.400 Invalid query parameters.401 Missing or invalid API key.
POST/media
Add media to the library
Record an already-hosted asset URL into the project media library. This is for URLs that are already reachable (e.g. your own hosted file); it does not upload a file. Rate-limited per account.
Request body
Field
Type
Description
urlrequired
string
Absolute http(s) URL of an already-hosted asset.
content_typerequired
string
MIME type, e.g. image/png.
category
"upload" | "archival"
file_name
string
project_id
string | null
source_url
string | null
width
integer | null
height
integer | null
Response — 201
Field
Type
Description
datarequired
object
201 Media asset recorded.400 Invalid request body.401 Missing or invalid API key.403 project_id is outside the key's workspace scope.404 project_id not found for this account.429 Rate limit exceeded.
POST/media/import
Import an external asset
Download an EXTERNAL url (a discovery result, a file on the web) and re-host it on Typecut storage as a library asset with provenance + dimensions, captioned on ingest. Free. Use this, not the raw provider url, before building a scene on found material — provider links expire. Contrast POST /media, which records a url you host yourself without copying it.
Request body
Field
Type
Description
urlrequired
string
Full-resolution external asset url (not a thumbnail).
project_id
string | null
file_name
string | null
provider
string | null
Where it came from, recorded as provenance.
source_url
string | null
width
integer | null
height
integer | null
duration_sec
number | null
Response — 201
Field
Type
Description
datarequired
object
201 Asset imported.400 Invalid request body or unreachable url.401 Missing or invalid API key.403 project_id is outside the key's workspace scope.404 project_id not found for this account.429 Rate limit exceeded.
POST/uploads
Get a link to upload a file
A short-lived signed link to PUT ONE file from your own disk, so its bytes never pass through an agent's conversation. Text (html, js, css, json; UTF-8, up to 2,000,000 bytes): pass public_url as a scene's src_url or reference it from scenes with <script src>. AUDIO you rendered yourself (wav, mp3, ogg, m4a, flac; up to 50 MB, counted against the storage quota) goes straight to storage: PUT it to the presigned upload_url with the returned headers (Content-Type is signed), then POST complete_url, which checks the stored bytes' magic number, records the file in the media library and as a music track that is transcoded and tempo/beat-analysed like any other (kind "sfx": library entry only), and returns track_id; the returned curl does both. Then PATCH /projects/{id}/music with trackId (or url: public_url) makes it the project music. Same access as landing authored compositions. Images and video also go straight to storage (see complete_url). Free.
Request body
Field
Type
Description
file_namerequired
string
The file's name with its extension; it becomes the last part of public_url. Text: .html, .htm, .js, .mjs, .css, .json. Audio: .wav, .mp3, .ogg, .m4a, .flac. Fonts: .woff2, .woff, .ttf, .otf. Images: .png, .jpg, .jpeg, .webp, .gif, .svg. Video: .mp4, .mov, .webm.
kind
"music" | "sfx"
Audio only: "sfx" stores a sound effect (no music track).
bytes
integer
Images and video: the file size in bytes. Required for video; above 100 MB the upload goes in 64 MB parts.
project_id
string | null
Images and video: file the asset under this project. A project (CLI) key files into its own project.
Response — 200
Field
Type
Description
datarequired
object
200 Upload link issued.400 Invalid body or an unsupported extension.401 Missing or invalid API key.403 The account may not land authored work.
PUT/uploads/{token}
Send the file to an upload link
The upload_url POST /uploads returned. Authorised by the signed token in the path, not an API key: send the raw bytes (curl -X PUT --data-binary @file). Text and fonts only: text must be UTF-8, a font must carry its container's magic number (otherwise 415 type_mismatch). An audio, image or video token is refused here (400 wrong_door): those go to their presigned upload_url, then complete_url.
Parameters
Name
In
Description
tokenrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 Stored.400 Empty body, text that is not UTF-8, or an audio/image/video token (wrong_door).403 The link is invalid or has expired.413 Over the size cap.415 The font bytes are not the format the file name promised.
POST/uploads/{token}/complete
Finish an image, video or audio upload
After the bytes of an image/video/audio upload are in storage (one PUT to upload_url, or every part), POST here. Assembles a multipart upload, checks the stored file (its magic number must match the extension; size caps; the plan's storage) and files it in the media library through the ingest pipeline. Audio also becomes a music track (transcoded + tempo/beat-analysed; not for kind "sfx") and the response is ReceivedUploadResponse (track_id, asset_id, analysis). A refused file is deleted. Authorised by the signed token in the path. Idempotent.
Parameters
Name
In
Description
tokenrequired
path
Response — 200
Variant 1 — CompletedMediaUploadResponse
Field
Type
Description
datarequired
object
Variant 2 — ReceivedUploadResponse
Field
Type
Description
datarequired
object
200 Filed. Image/video: CompletedMediaUploadResponse; audio: ReceivedUploadResponse.400 Empty file.403 The link is invalid or has expired.409 Nothing uploaded yet, or parts missing (listed).413 Over the size cap, or the plan's storage is full.415 The bytes are not the format the file name promised.
GET/uploads/{token}/parts/{n}
Get the upload URL for one part
For a multipart image/video upload: the presigned storage URL for part n (1..part_count), as plain text (JSON with Accept: application/json). PUT bytes [(n-1)·part_size, n·part_size) of the file to it. Authorised by the signed token in the path.
Parameters
Name
In
Description
tokenrequired
path
nrequired
path
200 The part URL.400 Not a multipart upload, or n out of range.403 The link is invalid or has expired.
POST/projects/{id}/cli-tokens
Mint a key scoped to one project
A long-lived API key (default 30 days, up to 90) for a CLI session on this project (typecut dev / typecut add). It reaches only /projects/{id}/… and POST /uploads (which files into this project), plus GET /me; it cannot mint keys. Listed and revocable in Settings → API keys. Free.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
ttl_days
integer
label
string
Response — 201
Field
Type
Description
datarequired
object
201 Minted.403 Called with a project key.404 No such project.
POST/projects/{id}/cli-tokens/{keyId}/revoke
Revoke a project CLI key
Parameters
Name
In
Description
idrequired
path
keyIdrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 Revoked.404 No live CLI key with that id on this project.
POST/projects/{id}/listen
Hear a stretch of the film (the mix, measured)
The export's own mix of a window on the film clock (at most 120 s), rendered whole and per group — dialogue (narration + scene speech), music (cues and beds), effects (SFX + scene sounds) — and measured: short-term LUFS every 0.5 s per group, integrated loudness and true peak against -14 LUFS / -1 dBTP, sounds more than outlier_db (default 6) above their surroundings named by cue / sound / scene, music or effects within mask_db (default 6) of the voice in 1-2 / 2-4 kHz while it speaks, the voice-over-bed ratio, and verdicts in words. bounce: true also hosts an m4a of the window. Free.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
from_sec
number
to_sec
number
bounce
boolean
mask_db
number
outlier_db
number
Response — 200
Field
Type
Description
datarequired
object
200 Measured.400 Bad window.404 No such project.
POST/media/ingest-url
Ingest a video link (admin-only)
ADMIN-ONLY. Download a public video from a link — X/Twitter, TikTok, Reddit, Instagram, a direct file, or a file already on Typecut storage — and host it as a library video with poster, subtitles and source metadata, ready for analysis. Free. Asynchronous: the first call returns 202 `status: "pending"`; call again with the same url to poll (200, same asset). A failed ingest is retried by calling again. YouTube is refused (422 youtube_unsupported): it blocks server downloads.
Request body
Field
Type
Description
urlrequired
string
A public video link (X, TikTok, Reddit, Instagram, a direct file) or a storage URL of a file already uploaded. Not YouTube.
project_id
string | null
Response — 200
Field
Type
Description
datarequired
object
200 Existing ingest for this url, in its current state.202 Ingest queued.400 Invalid request body.401 Missing or invalid API key.403 Not an admin, or project_id outside the key's workspace scope.404 project_id not found for this account.422 YouTube url (youtube_unsupported).
GET/projects/{id}/sources
Read a project’s sources and transcripts
What the project was grounded in and what was said in it. Each source reports its state and, when something went wrong, the reason in words — the read half of transcription, without which a caller can start work it cannot observe. Transcripts come back as timed SEGMENTS rather than words: a quote and a clip window are the same object, and words are 180 KB nobody can hold. Free.
Parameters
Name
In
Description
idrequired
path
source_id
query
from_sec
query
to_sec
query
limit
query
include_text
query
Response — 200
Field
Type
Description
datarequired
object
200 The project’s sources.401 Missing or invalid API key.404 Project not found for this account.
GET/projects/{id}/shelf
Read the project’s asset shelf
Everything the film could use — the user’s files and what discovery and stock found for the topic — each with a licence, a relevance score and a decision (auto · kept · pinned · rejected). Decided items come first. Free.
Parameters
Name
In
Description
idrequired
path
decision
query
kind
query
origin
query
limit
query
Response — 200
Field
Type
Description
datarequired
object
200 The shelf.401 Missing or invalid API key.404 Project not found for this account.
POST/projects/{id}/shelf
Put found material on the asset shelf
Add up to 50 candidates an agent found — an imported clip, a figure, a post — with what it is (caption, entities), the beat it serves, relevance, licence and credit, for a person to keep, pin or reject. Each item needs media_asset_id (preferred: hosted, cannot expire) or url. Unknown licence is allowed; such items are placed only when pinned. Idempotent on url: a re-add refreshes the description and never changes a decision. Free.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
itemsrequired
object[]
Response — 201
Field
Type
Description
datarequired
object
201 Items added (and any already on the shelf refreshed); skipped lists items that could not be added and why.400 Invalid body.401 Missing or invalid API key.404 Project not found for this account.
PATCH/projects/{id}/shelf
Keep, pin or reject shelf items
Set decisions on up to 50 shelf items. pinned = must be placed in the film (the note says how, e.g. "use for the intro"); kept = may be used; rejected = never used; auto = back to undecided. Free.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
changesrequired
object[]
Response — 200
Field
Type
Description
datarequired
object
200 The updated items, and any ids not on this shelf.400 Invalid body.401 Missing or invalid API key.404 Project not found for this account.
POST/media/{mediaId}/transcribe
Transcribe a recording in the library
Read the words out of an audio or video asset already in the library and record them as a project SOURCE, with word-level timings. The second door to speech-to-text: `sources` on POST /projects only exists at creation time and only takes a url, so a file already in a project was otherwise unreachable. SPENDS CREDITS by duration (~1 credit per minute of audio, from a generation_pricing row). Asynchronous — 202 with a status to poll; a repeat call on the same asset reports state rather than queueing a second pass, and costs nothing.
Parameters
Name
In
Description
mediaIdrequired
path
Request body
Field
Type
Description
project_id
string | null
Which project the transcript grounds. Defaults to the asset's own project.
role
"subject" | "supporting" | "style"
Response — 200
Field
Type
Description
datarequired
object
200 Already transcribed.202 Transcription started or already running.400 Not an audio/video asset, or no project to ground.401 Missing or invalid API key.402 Not enough credits for a recording of this length.404 Media asset or project not found for this account.429 Rate limit exceeded.
POST/voices/clones
Clone a voice from a recording
Start cloning a voice from a library asset (media_id) or a url (upload_url). The recording is checked (at least 10s of speech, at least 40% of it speech, not too noisy or clipped); the best 10–30s stretch becomes the reference clip and is transcribed. On success a voice (tts_presets row) is created for the account. Asynchronous — 202 with an id; poll GET /voices/clones/{cloneId}. Requires consent.attested = true. Disabled unless the deployment enables voice cloning (403 feature_disabled).
Request body
Field
Type
Description
media_id
string | null
A library audio/video asset to clone from. Exactly one of media_id / upload_url.
upload_url
string | null
An http(s) url to import into the library first, then clone from.
namerequired
string
What the voice is called in pickers.
language
string | null
Language of the recording ('en', 'fr-fr'). Omit to use the language detected from the audio. A clone speaks with the accent of its reference.
consentrequired
object
REQUIRED, attested must be true: the person in the recording has agreed to their voice being cloned (or it is your own voice).
model
"fish" | "chatterbox"
What the clone is made on: 'fish' (default — Fish S2.1 Pro, premium) or 'chatterbox' (Resemble Chatterbox on Replicate — standard tier; Turbo for English, Multilingual otherwise).
Response — 202
Field
Type
Description
datarequired
object
202 Accepted — processing.400 Invalid request, or consent.attested is not true (consent_required).401 Missing or invalid API key.403 Voice cloning is not enabled (feature_disabled).404 Media asset not found for this account.429 Rate limit exceeded.
GET/voices/clones/{cloneId}
Get a voice clone
The state of a voice clone: processing, ready (with tts_preset_id to narrate with) or rejected (with reject_reason), and the intake measures. Free.
Parameters
Name
In
Description
cloneIdrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The voice clone.401 Missing or invalid API key.403 Voice cloning is not enabled (feature_disabled).404 Voice clone not found for this account.
POST/voices/perform
Take to voice
Listen to a recorded take (words and timings, sounds, mood), write it as Drama 3 expression tags and pauses, and speak it in a chosen voice. Asynchronous: 202 with an id; poll GET /voices/perform/{performanceId}. dry_run quotes. Pacing, pauses, sighs and mood survive; singing and real laughs do not. Credits: listening per second of take, plus the voice; refunded on failure.
Request body
Field
Type
Description
take_media_idrequired
string
The recorded take: a library audio or video asset (up to 60 s). No voice clone is needed.
voice_id
string | null
A voice from GET /voices. Exactly one of voice_id / clone_id.
clone_id
string | null
One of your voice clones (POST /voices/clones).
mode
string | null
'tags' (default): the take's delivery written as expression tags. 'convert' is not available yet.
line_hint
string | null
What you are after, in a few words. Passed to the tag writer.
project_id
string | null
workspace_id
string | null
dry_run
boolean | null
Quote only: spends nothing, returns the credits.
Response — 200
Field
Type
Description
datarequired
object
200 dry_run: the quote.202 Accepted: listening.400 Invalid request.401 Missing or invalid API key.402 Not enough credits.404 Take or voice not found for this account.429 Rate limit exceeded.
GET/voices/perform/{performanceId}
Get a take-to-voice run
Where a run is: queued, listening, writing, speaking, ready (url, text_with_tags, heard) or failed (error; credits refunded). Free. The call that finds the tags written also speaks the line, so it can take a few seconds.
Parameters
Name
In
Description
performanceIdrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The run.401 Missing or invalid API key.404 Not found for this account.
POST/video-analyses
Analyse a reference video
Break any video into what an agent can read, in one call: frame-accurate shots (ffmpeg scene cuts, adaptive dark-scene cuts and dips to black) with duration, motion energy, a 480p keyframe, a class (talking head, B-roll, screen recording, motion graphics, title card…), on-screen text, graphics and voice-over; contact sheets of up to 16 shots with the shot number and time range burned in; the transcript; the sound (speech / music / singing segments, timed sound effects, and the music reading only when there is music); and `condensed`, the whole video as one readable timeline. The first 10 minutes are analysed. Asynchronous — 202 with an analysis at queued; poll GET /video-analyses/{analysisId}. 200 when the same source + options already has an analysis (returned instead of a new job). Free.
Request body
Field
Type
Description
url
string | null
A direct http(s) url of the video file. Exactly one of url / media_asset_id. YouTube links work only where YouTube ingest is enabled.
media_asset_id
string | null
A library video (media_assets id).
max_shots
integer | null
Keep at most this many shots; the strongest cuts win. Default 120.
sheet_cols
integer | null
Columns per contact sheet (default 4). Each sheet holds up to 16 shots.
threshold
number | null
Hard-cut sensitivity (ffmpeg scene score, default 0.3). Dark-scene cuts and dips to black are detected regardless.
transcribe
boolean | null
Transcribe the audio (default true).
summarize
boolean | null
One cheap vision-model pass over the contact sheets (default false).
sound
boolean | null
Analyse the sound: speech/music/singing when, sound effects, the music reading when there is music (default true).
Also plan a replication blueprint (chapters → blocks → scenes, create_project script) — one Gemini pass (default false).
blueprint_model
"flash" | "pro"
Blueprint model tier: 'flash' (default) or 'pro'.
force
boolean | null
Run again even when this source was already analysed with these options.
Response — 200
Field
Type
Description
datarequired
object
200 An existing analysis of this source (ready or still running).202 Accepted — queued.400 Invalid request (not exactly one source, not a video, bad url).401 Missing or invalid API key.404 Media asset not found for this account.429 Rate limit exceeded.
GET/video-analyses/{analysisId}
Get a video analysis
The state of a video analysis: queued / processing (poll again), ready (result) or failed (error). ?words=true adds the word-level transcript. ?t0=&t1= (seconds) cuts the result to one stretch — shots, words, sound segments, sfx and music marks inside it. result.motion says what moves inside shots (events/min, static share, blank runs, recommended_probes); its events page by t0/t1. ?detail=events|strips (with t0/t1, optional fps) re-senses the window: exact motion events, or labelled dense frames (window at most 30 s). Free; CPU, seconds.
Parameters
Name
In
Description
analysisIdrequired
path
words
query
t0
query
t1
query
detail
query
fps
query
Response — 200
Field
Type
Description
datarequired
object
200 The analysis.400 The id is not a uuid.401 Missing or invalid API key.404 Analysis not found for this account.
GET/video-analyses/{analysisId}/clips
Find the best clips in an analysed video
The best self-contained reels in an analysed video, ranked 0-100, each with snapped cut points (start on a sentence start with a short head; end on a sentence end with a 0.45 s tail past the last word), the hook line, a cold-open suggestion when the strongest line is not the first, the transcript, the signals and the reasons. Needs a ready analysis with a speech transcript. A transparent heuristic, not a view prediction. Free; CPU, seconds.
Parameters
Name
In
Description
analysisIdrequired
path
count
query
min_sec
query
max_sec
query
Response — 200
Field
Type
Description
datarequired
object
200 Ranked clip candidates.400 Bad id/options, or the analysis has no usable speech.401 Missing or invalid API key.404 Analysis not found for this account.409 The analysis is not ready yet.
GET/video-analyses/{analysisId}/frames
A picture of an analysed source at any time, width and crop
One hosted JPEG per requested time of the analysed source: "the reference at 47.3 s, 640 wide, the lower third" without re-sensing a window. The urls are named by content, so the same request returns the same urls and a repeat decodes nothing. A time past the end returns the last frame (noted). Free; CPU, seconds.
Parameters
Name
In
Description
analysisIdrequired
path
trequired
query
width
query
crop
query
Response — 200
Field
Type
Description
datarequired
object
200 The frames.400 Bad id, times, width or crop; or the analysis failed.401 Missing or invalid API key.404 Analysis not found for this account.
POST/audio-analyses
Analyse audio or a music track
What a recording contains, as data: detect (music / speech / singing / silence timeline and tags) and, for music, the tracked beats and bars, drum hits with accent, bass and lead notes, key, sections with energy and the salient moments. Up to 20 minutes. Asynchronous — 202 with an analysis at queued; poll GET /audio-analyses/{analysisId}. 200 when the same source already has an analysis (returned, free). PRICED per second of audio (about 1 credit per minute); library tracks are free; a failed analysis is refunded.
Request body
Field
Type
Description
media_asset_id
string | null
An uploaded audio or video asset. Exactly one of media_asset_id / track_id.
track_id
string | null
A music track (library or the account's own).
force
boolean | null
Run again even if this source was already analysed (charged again).
Response — 200
Field
Type
Description
datarequired
object
200 An existing analysis of this source (ready or still running).202 Accepted — queued and charged.400 Invalid request (not exactly one source, not audio/video, too long, still ingesting).401 Missing or invalid API key.402 Not enough credits.404 Media asset or track not found for this account.429 Rate limit exceeded.
GET/audio-analyses/{analysisId}
Get an audio analysis
The state of an audio analysis: queued / processing (poll again), ready (result) or failed (error; refunded). ?events=hit.snare,downbeat&t0=&t1= adds those event lists windowed to seconds. Free.
Parameters
Name
In
Description
analysisIdrequired
path
events
query
t0
query
t1
query
Response — 200
Field
Type
Description
datarequired
object
200 The analysis.400 The id is not a uuid.401 Missing or invalid API key.404 Analysis not found for this account.500 The event file could not be read.
POST/voices/audition
Audition voices
Synthesise the same short line with up to 4 voices — library presets or your own clones — and get a playable clip per voice. Synchronous. A voice that fails comes back with `error` beside the others. Free; rate limited per account (429 with retry_after).
Request body
Field
Type
Description
voicesrequired
object[]
Up to 4 voices; each has exactly one of voice_id / clone_id.
text
string | null
The line to speak (≤400 chars). Omit for a short natural sample in `language` (or each voice's own language).
language
string | null
Language to speak, e.g. 'fr' or 'en-gb'.
Response — 200
Field
Type
Description
datarequired
object
200 One clip (or error) per voice.400 Invalid request (no voices, more than 4, text over 400 characters).401 Missing or invalid API key.429 Audition rate limit reached.
POST/voices/speak
Speak a line into the library
Speak ONE line (≤1500 characters) with ONE voice — a library preset or your own clone — and keep it as a library audio asset with word timings. Synchronous. Charged at the voice-over rate (per character, the voice provider's price); refunded if the voice fails. Rate limited per account (429 with retry_after).
Request body
Field
Type
Description
voice_id
string | null
tts_preset id (from list voices).
clone_id
string | null
Voice clone id.
textrequired
string
The line to speak.
language
string | null
workspace_id
string | null
Workspace to file the asset under.
Response — 201
Field
Type
Description
datarequired
SpokenLine
201 The spoken line.400 Invalid request.401 Missing or invalid API key.402 Not enough credits.404 No such voice.429 Rate limit reached.502 The voice provider failed (credits refunded).
POST/media/generate
Generate or edit a picture
Make ONE standalone image — text-to-image, or an edit of the given reference image(s) — into the project media library. SPENDS CREDITS (generation type `image-chat`). Not a scene element: use the returned url as a scene background, as a `reference_image_url`, or as the input of another edit. Waits up to ~50s by default; a slower image returns 202 with `status: "generating"` — poll GET /generations/{id}. A failed job refunds its credits.
Request body
Field
Type
Description
project_idrequired
string
prompt
string
What to make, or what to change about the reference(s). Give this or prompts.
prompts
string[]
A batch: one image per prompt in one job (≤10), priced per image. Use instead of prompt.
reference_image_urls
string[]
Image urls to edit or match (image-to-image): up to 4, or the model's max_refs (GET /catalogs/models: Muse Image 10, GPT-Image-2.5 16). Omit for text-to-image.
reference_mode
"edit" | "identity"
With references: edit that picture (default), or keep its subject (identity) in a new one.
A registry model id from GET /catalogs/models?tool=generate_image (e.g. openai:gpt-image@2, meta:muse@image, openai:gpt-image@2.5-flare), each listed with its price, max_refs and sizes.
seed
integer
Provider seed for a repeatable take; batch item i uses seed + i.
experimental
boolean
STAFF ONLY: run a `model` the catalog does not list yet, at the flat `image-experimental` price per image. Other accounts get 400 "experimental models are staff-only".
Hold the request open up to ~50s for the result (default true). false returns 202 immediately.
Response — 200
Field
Type
Description
datarequired
object
200 Finished (or failed after starting — see `status`).202 Still generating; poll GET /generations/{id}.400 Invalid request body.401 Missing or invalid API key.402 Insufficient credits.404 project_id not found for this account.
POST/media/video
Generate a video clip
ONE standalone clip (≤ 8 s) into the media library: image-to-video from asset_id / image_url, or text-to-video from the prompt. SPENDS CREDITS per second (generation type `video-gen`); dry_run quotes. Clips take 30 s to a few minutes: waits ~50 s, then 202 `status: "generating"` — poll GET /generations/{id} or POST /status. A failed job refunds its credits.
Request body
Field
Type
Description
project_idrequired
string
promptrequired
string
The motion and scene.
asset_id
string
First frame: a library image. Or image_url. Neither = text-to-video.
image_url
string
duration
number
Seconds (default 5; rounded up to whole seconds).
aspect
"16:9" | "9:16" | "1:1"
Text-to-video only.
model
string
An allow-listed video model id (default prunaai:p-video@0).
seed
integer
audio
boolean
Generate a soundtrack (models that make one).
dry_run
boolean
wait
boolean
Response — 200
Field
Type
Description
datarequired
object
200 Finished, failed after starting, or a dry-run quote.202 Still running; poll GET /generations/{id}.400 Invalid request body.401 Missing or invalid API key.402 Insufficient credits.404 project_id or asset_id not found for this account.
POST/media/talking-head
Make a face speak
A face (library image or video, or a public https url) lip-synced to a voice (a library audio asset such as a speak result, or a public https audio url with audio_seconds). At most 60 s of voice per clip. SPENDS CREDITS per second of voice (generation type `talking-head`); dry_run quotes. Takes a minute or more: waits ~50 s, then 202 `status: "generating"` — poll GET /generations/{id} or POST /status. A failed job refunds its credits.
Request body
Field
Type
Description
project_idrequired
string
face_asset_id
string
The face: a library image or video. Or face_url.
face_url
string
The face: a public https image or video url.
audio_asset_id
string
The voice: a library audio asset (a speak result). Or audio_url.
audio_url
string
The voice: a public https audio url; then audio_seconds is required.
audio_seconds
number
Length of the voice, up to 60 s. Required with audio_url; prices the clip.
model
string
A talking-head model id from the allow-list (replicate:…).
seed
integer
dry_run
boolean
wait
boolean
Response — 200
Field
Type
Description
datarequired
object
200 Finished, failed after starting, or a dry-run quote.202 Still running; poll GET /generations/{id}.400 Invalid request body.401 Missing or invalid API key.402 Insufficient credits.404 project_id or asset_id not found for this account.
POST/media/upscale
Upscale a picture
One picture at 2× or 4× the pixels, re-hosted as a PNG library asset. SPENDS CREDITS (generation type `image-upscale`); dry_run quotes. Waits up to ~50s. `model` runs one upscaler (GET /catalogs/models?modality=upscale) instead of the deployment's chain in order.
Request body
Field
Type
Description
project_idrequired
string
asset_id
string
A library image. Pass this OR url.
url
string
factor
number | number
Default 2.
model
string
One upscaler id, e.g. runware:501@1 (GET /catalogs/models?modality=upscale). Omit for the chain.
dry_run
boolean
wait
boolean
Response — 200
Field
Type
Description
datarequired
object
200 Finished, failed after starting, or a dry-run quote.202 Still running; poll GET /generations/{id}.400 Invalid request body.401 Missing or invalid API key.402 Insufficient credits.404 project_id or asset_id not found for this account.
POST/media/grid
Generate a storyboard or sprite sheet
MANY pictures from ONE image call, each cell hosted as its own library asset plus a contact sheet. mode `panels`: a cols × rows storyboard (≤ 4 × 4) of consecutive beats on one set with one cast — drawn together they keep the set, characters and story state consistent, for one call. mode `items`: a sprite sheet of ≤ 12 labelled props, each cut out. mode `sprite`: an animation strip of one action. SPENDS CREDITS: the model's per-call price for the sheet (generation type `image-grid` for models without one) + the `image-upscale` price per cell with `upscale`; dry_run quotes. Waits ~50 s, then 202 `status: "generating"` — poll GET /generations/{id}, whose `result` is the same compact grid. 503 `not_available` while the sheet job is rolling out.
Request body
Field
Type
Description
project_idrequired
string
moderequired
"panels" | "items" | "sprite"
sprite
object
mode `sprite`: one action as 2–12 poses, cut into ONE strip (result.sprite: strip_url, frames, frame_w, frame_h, fps, loop). Default model openai:gpt-image@2.6-flare.
panels
object
items
object[]
background
"transparent" | "white"
model
string
A generate_image model (GET /catalogs/models?tool=generate_image).
reference_image_urls
string[]
seed
integer
upscale
object
dry_run
boolean
wait
boolean
Response — 200
Field
Type
Description
datarequired
object
200 Finished (`result` {mode, model, contact_sheet_url, cells[{index, label?, url, asset_id, width, height, box}]}), failed after starting, or a dry-run quote {credits, cells, available}.202 Still running; poll GET /generations/{id}.400 Invalid request body.401 Missing or invalid API key.402 Insufficient credits.404 project_id not found for this account.503 not_available: the sheet job is not on production yet.
POST/media/segment
Find the named parts of a picture
Per named part: a mask PNG (alpha), box [x,y,w,h], polygon, SVG path, centroid and quad. SPENDS CREDITS (generation type `image-segment`); dry_run quotes. Waits up to ~50s; a slower call returns 202 with `status: "generating"` — poll GET /generations/{id}, whose `result` carries the same output. A failed job refunds its credits.
Request body
Field
Type
Description
project_idrequired
string
asset_id
string
A library image. Pass this OR url.
url
string
A public https image url. Pass this OR asset_id.
parts
string | object[]
Named parts ("tail" or {name, hint}). Required for segment and plate; cutout defaults to the main subject; depth returns each part z.
bands
integer
depth: parallax bands (default 3).
dilate
number
plate: grow the hole by this fraction of the long side (default 0.016).
crop
boolean
cutout: trim to the part box (default full frame).
dry_run
boolean
Quote only; charges nothing.
wait
boolean
Hold up to ~50s for the result (default true).
Response — 200
Field
Type
Description
datarequired
object
200 Finished, failed after starting, or a dry-run quote.202 Still running; poll GET /generations/{id}.400 Invalid request body.401 Missing or invalid API key.402 Insufficient credits.404 project_id or asset_id not found for this account.
POST/media/depth
Depth map of a picture
A 16-bit depth PNG (white = near) and parallax bands with z and a mask each. SPENDS CREDITS (generation type `image-depth`); dry_run quotes. Waits up to ~50s; a slower call returns 202 with `status: "generating"` — poll GET /generations/{id}, whose `result` carries the same output. A failed job refunds its credits.
Request body
Field
Type
Description
project_idrequired
string
asset_id
string
A library image. Pass this OR url.
url
string
A public https image url. Pass this OR asset_id.
parts
string | object[]
Named parts ("tail" or {name, hint}). Required for segment and plate; cutout defaults to the main subject; depth returns each part z.
bands
integer
depth: parallax bands (default 3).
dilate
number
plate: grow the hole by this fraction of the long side (default 0.016).
crop
boolean
cutout: trim to the part box (default full frame).
dry_run
boolean
Quote only; charges nothing.
wait
boolean
Hold up to ~50s for the result (default true).
Response — 200
Field
Type
Description
datarequired
object
200 Finished, failed after starting, or a dry-run quote.202 Still running; poll GET /generations/{id}.400 Invalid request body.401 Missing or invalid API key.402 Insufficient credits.404 project_id or asset_id not found for this account.
POST/media/plate
Clean plate
The picture with the named parts inpainted away, same size as the source. SPENDS CREDITS (generation type `image-plate`); dry_run quotes. Waits up to ~50s; a slower call returns 202 with `status: "generating"` — poll GET /generations/{id}, whose `result` carries the same output. A failed job refunds its credits.
Request body
Field
Type
Description
project_idrequired
string
asset_id
string
A library image. Pass this OR url.
url
string
A public https image url. Pass this OR asset_id.
parts
string | object[]
Named parts ("tail" or {name, hint}). Required for segment and plate; cutout defaults to the main subject; depth returns each part z.
bands
integer
depth: parallax bands (default 3).
dilate
number
plate: grow the hole by this fraction of the long side (default 0.016).
crop
boolean
cutout: trim to the part box (default full frame).
dry_run
boolean
Quote only; charges nothing.
wait
boolean
Hold up to ~50s for the result (default true).
Response — 200
Field
Type
Description
datarequired
object
200 Finished, failed after starting, or a dry-run quote.202 Still running; poll GET /generations/{id}.400 Invalid request body.401 Missing or invalid API key.402 Insufficient credits.404 project_id or asset_id not found for this account.
POST/media/cutout
Cut a part out of a picture
The named parts (default: the main subject) on transparency. SPENDS CREDITS (generation type `image-cutout`); dry_run quotes. Waits up to ~50s; a slower call returns 202 with `status: "generating"` — poll GET /generations/{id}, whose `result` carries the same output. A failed job refunds its credits.
Request body
Field
Type
Description
project_idrequired
string
asset_id
string
A library image. Pass this OR url.
url
string
A public https image url. Pass this OR asset_id.
parts
string | object[]
Named parts ("tail" or {name, hint}). Required for segment and plate; cutout defaults to the main subject; depth returns each part z.
bands
integer
depth: parallax bands (default 3).
dilate
number
plate: grow the hole by this fraction of the long side (default 0.016).
crop
boolean
cutout: trim to the part box (default full frame).
dry_run
boolean
Quote only; charges nothing.
wait
boolean
Hold up to ~50s for the result (default true).
Response — 200
Field
Type
Description
datarequired
object
200 Finished, failed after starting, or a dry-run quote.202 Still running; poll GET /generations/{id}.400 Invalid request body.401 Missing or invalid API key.402 Insufficient credits.404 project_id or asset_id not found for this account.
GET/generations/{id}
Get a generation
Poll ONE credit-spending job by the `generation_id` the starting call returned (POST /media/generate, POST /scenes/{sceneId}/elements, …): status, error, and for an image the finished asset. GET /projects/{id}/resolution answers for a whole project; this is the per-job receipt. 404 (not 403) when not accessible. `wait` (seconds, ≤ 120) holds the answer until the job is completed or failed, or the wait is up — one call instead of a poll loop.
Parameters
Name
In
Description
idrequired
path
wait
query
Response — 200
Field
Type
Description
datarequired
Generation
200 The generation.400 Malformed id.401 Missing or invalid API key.404 Generation not found or not accessible.
POST/generations/{id}/cancel
Cancel a running generation
Stop a render-project job (more job types later; others answer 422). Queued scenes are dropped, in-flight GPU scene jobs are aborted and the film stitch is skipped. Billing: only scenes already rendered are charged (cached scenes are free); the remaining credits are refunded once through the credit ledger, never twice. Idempotent, and safe on a job that already finished: it returns the final state unchanged. Only the account that owns the generation can cancel it (404 otherwise). GET /generations/{id} then shows `status: cancelled`, `charged_credits` and `scenes_rendered`.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
CancelGeneration
200 The generation after the cancel (or its final state).400 Malformed id.401 Missing or invalid API key.404 Generation not found or not accessible.422 This generation type cannot be cancelled yet.503 Render service unavailable; the cancel was not delivered. Retry.
GET/projects/{id}
Get a project
Fetch one project by id. Returns 404 (not 403) when the project exists but is not accessible by the API key — so callers cannot probe for existence.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
Project
A video project. Belongs to one workspace (or workspace_id null). Status drives the FE lifecycle.
200 Project found.401 Missing or invalid API key.404 Project not found or not accessible.
GET/projects/{id}/trace
Get a project generation trace
A token-light audit of how a project was planned and generated — what template resolved, whether it drove the plan, the pacing decision, per-scene/model summaries, and LLM step summaries. Compact by default; opt into detail with ?include=raw (full structure_config/prompt_config/catalog + per-element model list) and/or ?include=prompts (full LLM prompt blobs). 404 (not 403) when not accessible.
Parameters
Name
In
Description
idrequired
path
include
query
Response — 200
Field
Type
Description
data
object
200 The project trace digest.401 Missing or invalid API key.404 Project not found or not accessible.
GET/workspaces
List workspaces
Returns the workspaces the API key can access. Account-level keys see all workspaces; workspace-scoped keys see only the one they are scoped to. Archived workspaces are omitted. Cursor-paginated.
Parameters
Name
In
Description
cursor
query
limit
query
Response — 200
Field
Type
Description
datarequired
Workspace[]
next_cursorrequired
string | null
200 A page of workspaces.400 Invalid query parameters.401 Missing or invalid API key.
POST/workspaces
Create a workspace
Create a workspace: one per brand, channel or client, carrying the defaults its projects inherit. Account-level keys only (workspace-scoped keys get 403). Limited by the plan (max_channels); archived workspaces do not count. Over the limit returns 402 plan_limit_reached with `limit`, `current` and `upgrade_url`.
Request body
Field
Type
Description
namerequired
string
description
string | null
language
string
aspectRatio
"16:9" | "9:16" | "4:3" | "1:1"
contentContext
string | null
Response — 201
Field
Type
Description
datarequired
object
201 Workspace created.400 Invalid body.401 Missing or invalid API key.402 Plan workspace limit reached, or subscription inactive.403 The key is scoped to one workspace.
GET/workspaces/{id}
Get a workspace
Fetch one workspace by id. Returns 404 (not 403) when the workspace exists but is not accessible by the API key.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
Workspace
A workspace — per-show preset container. Holds defaults (voice, visual style, transitions, captions), brand assets, and editorial context. Projects belong to workspaces. (The underlying table is `channels`.)
200 Workspace found.401 Missing or invalid API key.404 Workspace not found or not accessible.
POST/discovery/search
Search open & archival assets
Entity-grounded asset search across open / archival sources (Wikimedia Commons, NASA, ...). Resolves the query to a real-world entity, fans out to the sources, filters to commercially-usable licenses, and returns ranked candidates with license + provenance. Read-only; no asset is downloaded or stored by this call.
Request body
Field
Type
Description
queryrequired
string
Subject to search — a topic, person, place, event, or object.
kind
"image" | "video"
Media kind. Defaults to "image" (entity-specific video is sparse).
limit
integer
Max results to return. Defaults to 24.
Response — 200
Field
Type
Description
datarequired
object
200 Ranked, license-cleared candidates.400 Invalid request body.401 Missing or invalid API key.502 Upstream discovery engine error.
GET/projects/{id}/render
Get render status
Poll the latest render job for a project: phase, per-scene progress, stitch progress, and the final MP4 URL (`render_url` / `job.output_url`) once completed.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 Current render status.404 Project not found or not accessible.
POST/projects/{id}/render
Render a project to video
Trigger a full project video render. Validates readiness first: every chapter that has narration/dialogue must have its voiceover (a chapter with nothing spoken — music only, or silent — needs none; nor does a project with no voice) and every scene must have visual content. On readiness failure the 422 response carries machine-readable `blockers` (chapter ids missing audio, scene ids missing visuals) so callers can resolve them programmatically. Credits are deducted up front and refunded automatically if the render cannot be queued. Poll GET /projects/{id}/render for progress and the final MP4 URL.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
renderMode
"canvas" | "hyperframe"
Render backend. Defaults to hyperframe.
Response — 202
Field
Type
Description
datarequired
object
202 Render queued.402 Insufficient credits.404 Project not found or not accessible.409 Project is already rendering.422 Project not ready to render — `blockers` lists the chapters missing audio and scenes missing visuals.503 Render queue unavailable (credits were refunded).
GET/projects/{id}/thumbnails
List thumbnail options
The project's current thumbnail, every generated option, and thumbnail generations still running.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 Thumbnails.404 Project not found or not accessible.
POST/projects/{id}/thumbnails
Generate thumbnail options
Design 1–3 YouTube thumbnail options, each a different concept, styled after a frame of the video unless `reference_url` is given. Charged per option up front; options that fail are refunded. Asynchronous — poll GET /projects/{id}/thumbnails until `pending` is empty. Never replaces a thumbnail the project already has; choose one with PUT.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
count
integer
How many options, each a different concept. Default 1.
prompt
string
Direction for the art director.
reference_url
string
A thumbnail to learn from. Without one, a frame of the video sets the style.
reference_influence
"inspired" | "balanced" | "close"
How closely to follow reference_url. `inspired` (default) borrows only its strategy.
Response — 202
Field
Type
Description
datarequired
object
202 Generation queued.400 Invalid body.402 Insufficient credits.404 Project not found or not accessible.503 Queue unavailable (nothing was charged).
PUT/projects/{id}/thumbnails
Set the project thumbnail
Make one image the project thumbnail: a generated option, or a file already in your library.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
urlrequired
string
A candidate url, or a file in your library.
Response — 200
Field
Type
Description
datarequired
object
200 Thumbnail set.400 Not a candidate or library file.404 Project not found or not accessible.
GET/projects/{id}/resolution
Get resolution status
What's resolved vs unresolved in a project, and the estimated credits to resolve the rest. A scene is resolved once it has renderable content; voice-over is tracked per chapter. Read-only counterpart to POST /projects/{id}/resolve — the counts match the editor's readiness UI exactly (shared logic).
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 Resolution status + cost quote.404 Project not found or not accessible.
POST/projects/{id}/resolve
Resolve (generate) unresolved work
Generate everything unresolved in a scope — its unresolved scenes (composition) plus voice-over. Mirrors the editor's "Generate everything" actions; each underlying generation charges and refunds itself. Scenes with nothing to generate from are skipped (`skipped_unresolvable`). Pass `dryRun:true` for the credit quote without firing anything (identical numbers to GET /projects/{id}/resolution).
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
scope
"project" | "chapter" | "block"
What to resolve. `chapter` requires `chapterId`; `block` requires `blockId`.
chapterId
string
blockId
string
dryRun
boolean
When true, returns the credit quote without firing any generation.
includeStale
boolean
Also regenerate stale voice / scenes (inputs changed since generation).
voiceOnly
boolean
Voiceover and word timings only — no scene is composed, quoted or billed. For callers that author their own HTML.
Response — 200
Field
Type
Description
datarequired
object
200 Dry-run quote — nothing was fired.202 Resolution started — generations were queued.400 Invalid scope (e.g. chapter scope without chapterId).403 Subscription inactive (past_due / unpaid).404 Project, chapter, or block not found.
GET/catalogs/{catalog}
List a value catalog
The discoverability layer: every value project creation/editing accepts by id or slug is listable here, straight from the live registries. Catalogs: `transitions` (scene transition slugs + render mode + default duration), `caption-styles` (caption style ids + preview clips), `voices` (TTS presets — system + your own; each with provider, model, tier, supported_languages, is_clone and a preview in the `language` asked for when baked; filter with ?language= and ?provider=), `music` (published music tracks + license info). Pass the returned slug/id values to the matching creation or editing fields.
Parameters
Name
In
Description
catalogrequired
path
Response — 200
Field
Type
Description
datarequired
object[]
Catalog rows; the shape depends on the catalog (see description).
200 Catalog rows.404 Unknown catalog.
POST/scenes/{sceneId}/attach-media
Attach media to a scene
Set a scene's background to an image or video URL. The first editor-mutation primitive on the API: it shares the editor's media-attach service, so the change appears live in any open editor (via Realtime, applied in place) and behaves identically to picking media in the UI. Scene ids are globally unique, so the flat path is unambiguous.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
media_urlrequired
string
Public URL of the image or video to use as the scene background.
is_video
boolean
Set true when media_url points to a video.
thumbnail_url
string | null
Optional pre-generated thumbnail written to the scene chip.
Response — 200
Field
Type
Description
datarequired
object
200 Media attached; scene background updated.400 Invalid request body.401 Missing or invalid API key.403 Plan does not include API access.404 Scene not found or not accessible.
GET/formulas
List every look you can pick or author
Brand-mark motion (built-ins marked `built_in`, active authored rows, your own held drafts) and caption motion (the published catalog plus your own private ones). Narrow with `?slot=`.
Parameters
Name
In
Description
slot
query
Response — 200
Field
Type
Description
datarequired
object[]
200 The catalog.401 Missing or invalid API key.
POST/formulas
Author a formula
A formula is a look expressed as INGREDIENTS in a row, linted at write, instead of a code drop. Two systems carry them and this is the one place to see and write both.
**slot `bug`** — the brand mark's motion. `spec` is `{recipe, params}` over three carriers: `entry-exit`, `beat-envelope` and `trigger-lift`; `on` names a ruler (bar, beat, 2beat, hit, cam). You choose the slug. The row is born `held` and reaches no render until it is promoted to `active`. A spec that could strobe or reach outside its own element is refused (422).
**slot `caption`** — how words move: entry/life/exit/field ingredients. The id is MINTED from the title, because the renderer keys the compiled template on the row id, so a slug is not honoured and the reply carries the id that was used. Saved private; reaching the shared catalog is moderated rather than a status flip.
Both are validated by composing the real thing and checking what comes out, not by matching a schema: either grammar can be valid JSON and still name an ingredient that does not exist, which renders as a duller look with no error anywhere.
Request body
Field
Type
Description
slot
"bug" | "caption"
Default "bug".
slug
string
bug only — a caption formula mints its id from the title.
titlerequired
string
hint
string
specrequired
object
tiers
"raster" | "svg" | "lockup" | "template"[]
status
"held" | "active" | "disabled"
Response — 201
Field
Type
Description
datarequired
object
201 Stored.400 Invalid body.401 Missing or invalid API key.409 Reserved built-in slug, or an operation the slot does not support.422 The spec failed its linter; the message lists every problem.
PUT/formulas/{slug}
Author or replace a brand-mark formula
A formula is a look expressed as INGREDIENTS in a row, linted at write, instead of a code drop. Two systems carry them and this is the one place to see and write both.
**slot `bug`** — the brand mark's motion. `spec` is `{recipe, params}` over three carriers: `entry-exit`, `beat-envelope` and `trigger-lift`; `on` names a ruler (bar, beat, 2beat, hit, cam). You choose the slug. The row is born `held` and reaches no render until it is promoted to `active`. A spec that could strobe or reach outside its own element is refused (422).
**slot `caption`** — how words move: entry/life/exit/field ingredients. The id is MINTED from the title, because the renderer keys the compiled template on the row id, so a slug is not honoured and the reply carries the id that was used. Saved private; reaching the shared catalog is moderated rather than a status flip.
Both are validated by composing the real thing and checking what comes out, not by matching a schema: either grammar can be valid JSON and still name an ingredient that does not exist, which renders as a duller look with no error anywhere.
This route is the `bug` slot only; author a caption formula with POST /formulas.
Parameters
Name
In
Description
slugrequired
path
Request body
Field
Type
Description
slot
"bug" | "caption"
Default "bug".
slug
string
bug only — a caption formula mints its id from the title.
titlerequired
string
hint
string
specrequired
object
tiers
"raster" | "svg" | "lockup" | "template"[]
status
"held" | "active" | "disabled"
Response — 200
Field
Type
Description
datarequired
object
200 Stored, with what the spec emits.400 Invalid body or slug.401 Missing or invalid API key.403 The slug belongs to another account.409 The slug is a reserved built-in.422 The spec failed the linter.
PATCH/formulas/{slug}
Promote, park or retire a brand-mark formula
Move a formula between `held`, `active` and `disabled`. Promotion re-lints the stored spec, because `active` is the moment it starts reaching renders and a row may have been written under an older contract. A disabled formula leaves existing projects rendering a static mark.
The `bug` slot only: a caption formula has no held/active machine, because publishing to the shared catalog is moderated.
Parameters
Name
In
Description
slugrequired
path
Request body
Field
Type
Description
statusrequired
"held" | "active" | "disabled"
Response — 200
Field
Type
Description
datarequired
object
200 Status changed.401 Missing or invalid API key.403 The formula belongs to another account.404 No such formula.409 The caption slot does not support a status change.422 The stored spec no longer passes the linter.
GET/scenes/{sceneId}/context
What is true at this moment of the project
The read to make BEFORE authoring anything that has to land ON something — a brand-mark formula, a cue, a reactive overlay.
It answers, in one call, the questions that decide whether an authored thing will actually fire:
- **narration** — the words this scene speaks and the second each is HEARD. Already on the timeline clock, so pauses and silence trims are applied; `retimed` says whether this block has any. A cue addressed at a word from this list will resolve.
- **rulers** — the grid a formula resolves against, and the cues already resolved to `hit:<name>`. `source: "archetype"` means the 100 BPM fallback, not the track.
- **cues** — each authored cue with `resolvesAt`. `null` means it will NEVER fire, which is otherwise invisible until you watch the export and see nothing happen.
- **signals** — whether an audio signal exists to bind to, and its channels.
- **mark** — what the bug is set to now, and which slugs it can take.
Assembled from the renderer's own functions, so what you read here and what the MP4 does are one computation rather than two that ought to agree. It does not restate structure or composition — GET /scenes/{sceneId} returns the source.
Parameters
Name
In
Description
sceneIdrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The scene's resolved temporal and signal facts.401 Missing or invalid API key.404 Scene not found.
GET/scenes/{sceneId}/cues
Read a scene's named moments
Named moments on a scene. A cue does NOTHING on its own — it is an ADDRESS, so something else can be told to fire on it.
Today the brand mark reads them: PUT /projects/{id}/branding with `formula: "ghost"` rests the mark low and lifts it on `hit:<name>`, which is how a subscribe bump lands on the exact word rather than on a beat.
`at` is seconds from the scene start, or a moment on the scene's own rulers — `word:subscribe`, `word:#4`, `bar:3`, `beat:7`, `cam:hero`, `end-2`, with `+0.25` / `+1b` / `+2B` offsets. Prefer the moment form: a cue pinned to a word survives a re-cut of the narration and one pinned to 4.2s does not. It resolves at render against the scene's measured grid, and a cue that does not resolve is dropped rather than firing at zero.
The list is REPLACED, not merged.
Parameters
Name
In
Description
sceneIdrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The scene's cues.401 Missing or invalid API key.404 Scene not found.
PUT/scenes/{sceneId}/cues
Replace a scene's named moments
Named moments on a scene. A cue does NOTHING on its own — it is an ADDRESS, so something else can be told to fire on it.
Today the brand mark reads them: PUT /projects/{id}/branding with `formula: "ghost"` rests the mark low and lifts it on `hit:<name>`, which is how a subscribe bump lands on the exact word rather than on a beat.
`at` is seconds from the scene start, or a moment on the scene's own rulers — `word:subscribe`, `word:#4`, `bar:3`, `beat:7`, `cam:hero`, `end-2`, with `+0.25` / `+1b` / `+2B` offsets. Prefer the moment form: a cue pinned to a word survives a re-cut of the narration and one pinned to 4.2s does not. It resolves at render against the scene's measured grid, and a cue that does not resolve is dropped rather than firing at zero.
The list is REPLACED, not merged.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
cuesrequired
object[]
The complete cue list for this scene. [] clears them.
Response — 200
Field
Type
Description
datarequired
object
200 Cues replaced.400 Invalid body, an unaddressable cue name, or a duplicate.401 Missing or invalid API key.404 Scene not found.
GET/projects/{id}/branding
Read a project's brand mark
The pinned brand mark — the corner "bug" — on one project: image, corner, size, opacity, and a MOTION FORMULA.
Before this endpoint the mark was reachable only from the settings panel and the creation wizard, so a film built through the API came out unbranded even for a channel that has a logo configured.
Every field is optional and only what you send changes: branding shares `projects.settings` with captions, audio and theme, so the write is a merge.
`formula` is what makes the mark live. `none` is a static stamp; `tick` squashes it on every downbeat of the project's own measured grid; `ghost` rests it low and lifts it on an authored scene cue. The motion compiles against the same bars the composition uses, so it lands with the music rather than near it. Amplitudes are capped and ticks carry a cooldown — a mark cannot be made to strobe. An unknown slug is refused rather than silently rendering static.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The effective branding, with render-time defaults filled in.401 Missing or invalid API key.404 Project not found.
PUT/projects/{id}/branding
Set a project's brand mark and its motion
The pinned brand mark — the corner "bug" — on one project: image, corner, size, opacity, and a MOTION FORMULA.
Before this endpoint the mark was reachable only from the settings panel and the creation wizard, so a film built through the API came out unbranded even for a channel that has a logo configured.
Every field is optional and only what you send changes: branding shares `projects.settings` with captions, audio and theme, so the write is a merge.
`formula` is what makes the mark live. `none` is a static stamp; `tick` squashes it on every downbeat of the project's own measured grid; `ghost` rests it low and lifts it on an authored scene cue. The motion compiles against the same bars the composition uses, so it lands with the music rather than near it. Amplitudes are capped and ticks carry a cooldown — a mark cannot be made to strobe. An unknown slug is refused rather than silently rendering static.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
enabled
boolean
Turn the mark on or off without touching the image.
url
string | null
Logo image URL. null clears it, which also takes the mark off. Supplying one for an unbranded project turns the mark on.
The project's `edit_version` as you last read it (GET /projects/{id} returns it; every project-level write answers with the new one). When sent, the write is a compare-and-set: if anyone changed the project settings since, NOTHING is written and the response is a 409 `conflict` carrying `current_version` and `last_writer`. Omit it to write unconditionally.
Response — 200
Field
Type
Description
datarequired
object
200 Branding updated; the effective values are returned.400 Invalid body, or an unknown motion formula.401 Missing or invalid API key.404 Project not found.409 expected_version is stale: the project settings changed since you read them. Nothing was written; re-read and reapply.
PUT/scenes/{sceneId}/transition
Set the transition on the cut before a scene
Set — or clear — the transition on ONE scene boundary. A seam is named by its INCOMING scene, which is how the timeline stores it and how the editor addresses it. On a chapter's first scene the seam is the CHAPTER JOIN, which only a geometric transition (render_mode `geometric`) can cross (400 otherwise); the project's first scene has no seam (409).
This is the per-seam control. `POST /projects`'s `transitions` list only cycles slugs across every boundary at plan time, so before this endpoint there was no way to say "this cut, ink, 700ms" — or to change a transition after generation.
Slugs are validated against the same curated set GET /catalogs/transitions offers, so an unknown slug is refused rather than written and rendered as a silent cross-dissolve. Omitting `duration_ms` takes the transition's own default, which is usually right: a textural transition needs materially longer than a crossfade before it reads at all.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
transition
string
Transition slug from GET /catalogs/transitions, or "none" for a hard cut. Omit to change only the sound.
duration_ms
integer
Seam length. Omit to take the transition's own registry default.
length
string
Seam length in music on the project's tempo (settings.score): '1/16', '1/8', '1/4', '1/2', '2 beats', '1 bar' (max 2 bars). Wins over duration_ms. The picture window is centred on the cut.
sfx_mode
"auto" | "always" | "never"
sound
string
A synthesized seam sound: stutter-build, suck-hit, reverse-cymbal, boom, braam, riser-drop, downlifter, snare-build, stinger, shimmer, sub-drop, scratch, zap, shutter, swell, chapter — or "none". Builds lead into the cut, hits sit on it; pitched parts follow the project key.
sound_energy
"subtle" | "medium" | "strong"
lead
number | string
How long a build runs before the cut: seconds, or musical ('1 bar'). Default 1 bar on a tempo, else 1.6 s.
Response — 200
Field
Type
Description
datarequired
object
200 Seam updated; the effective transition is returned.400 Invalid body, or an unknown transition slug.401 Missing or invalid API key.404 Scene not found, or not placed on its chapter timeline.409 The scene is first in its chapter, so there is no boundary before it.
GET/projects/{id}/groove
Read the project's grooves
A project's GROOVES are synthesized loops, one per scene, that play on the PROJECT's musical clock (its tempo map): bar n is bar n in every scene, so neighbouring grooves stay in phase across a join and a seam sound (PUT /scenes/{sceneId}/transition) lands on it. Each scene plays its own pick, else the project default. Patterns carry no tempo or key — the project grid wins.
`render=true` also renders each playing scene's slice and returns its file, its hits (`trains`, every hit per voice in scene-local seconds) and named `marks` — what visuals sync to. Repeat `scene_id` to render only some scenes.
Parameters
Name
In
Description
idrequired
path
render
query
scene_id
query
Response — 200
Field
Type
Description
datarequired
object
200 The grooves, the grid, and what every scene plays.401 Missing or invalid API key.404 Project not found.
PUT/projects/{id}/groove
Set the project's grooves
A project's GROOVES are synthesized loops, one per scene, that play on the PROJECT's musical clock (its tempo map): bar n is bar n in every scene, so neighbouring grooves stay in phase across a join and a seam sound (PUT /scenes/{sceneId}/transition) lands on it. Each scene plays its own pick, else the project default. Patterns carry no tempo or key — the project grid wins.
One atomic update, applied in order: add_presets, upsert, remove, default_id, scenes, gain_db, enabled. Patterns are validated (voice, steps grammar, ranges) and refused with a message the author can act on.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
add_presets
string[]
Copy presets into the library: glass-acid, grain-garage, pluck-bells, stutter-house.
upsert
object[]
remove
string[]
Library ids to delete; the default and scene picks that use them are cleared.
default_id
string | null
The groove every scene without an override plays; a preset id is copied in on first use.
scenes
object[]
gain_db
number
enabled
boolean
expected_version
integer
The project's `edit_version` as you last read it (GET /projects/{id} returns it; every project-level write answers with the new one). When sent, the write is a compare-and-set: if anyone changed the project settings since, NOTHING is written and the response is a 409 `conflict` carrying `current_version` and `last_writer`. Omit it to write unconditionally.
Response — 200
Field
Type
Description
datarequired
object
200 Updated; the new state is returned.400 Invalid body or pattern, an unknown groove id, or a scene not in the project.401 Missing or invalid API key.404 Project not found.409 expected_version is stale: the project settings changed since you read them. Nothing was written; re-read and reapply.
GET/projects/{id}/tempo
Read the project's tempo
The project's musical grid (settings.tempo): bpm, meter, key, where bar 1 falls, and tempo sections. It is what grid-bound motion, sound cues, grooves and music loops are timed against. Falls back to the grid the film was generated at (`stored: false`); null when there is neither.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The tempo.401 Missing or invalid API key.404 Project not found.
PATCH/projects/{id}/tempo
Change the project's tempo
The project's musical grid (settings.tempo): bpm, meter, key, where bar 1 falls, and tempo sections. It is what grid-bound motion, sound cues, grooves and music loops are timed against.
Does what the editor's tempo chip does: every grid-bound sound cue moves to the same bar:beat on the new grid, and a music LOOP is re-conformed (time-stretched) to it. A full-length track keeps its own tempo; returning to a loop's own tempo restores its original file. Idempotent. A FLAT edit: a project with tempo ramps (a conductor map) has them flattened into steps (`flattened_ramps: true`); edit ramps with PATCH /projects/{id}/conductor. Free.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
bpm
number
meter
integer | object
A bare number means n/4.
keyScale
string | null
originSec
number
sections
object[]
Tempo/meter/key changes from a point onward. Replaces the list when sent; [] clears it.
source
"user" | "track" | "film" | "default"
expected_version
integer
The project's `edit_version` as you last read it (GET /projects/{id} returns it; every project-level write answers with the new one). When sent, the write is a compare-and-set: if anyone changed the project settings since, NOTHING is written and the response is a 409 `conflict` carrying `current_version` and `last_writer`. Omit it to write unconditionally.
Response — 200
Field
Type
Description
datarequired
object
200 Saved; the stored tempo and what moved.400 Invalid tempo (out of range, unknown field, or nothing to change).401 Missing or invalid API key.404 Project not found.409 expected_version is stale: the project settings changed since you read them. Nothing was written; re-read and reapply.
GET/projects/{id}/conductor
Read the project's conductor (tempo map)
The project's musical grid as a conductor: tempo points with curved ramps, signature changes, key changes, markers and an arrangement lane, anchored in musical position (q = quarter-notes from bar 1, or bar + beat) so seconds are derived and an edit at bar 5 does not slide bar 9. PATCH /tempo (set_tempo) stays for flat edits and flattens ramps. Falls back to the v1 tempo migrated, then the grid the film was generated at, then 120 bpm 4/4. Includes `edit_version`.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The conductor.401 Missing or invalid API key.404 Project not found.
PUT/projects/{id}/conductor
Replace the project's whole conductor
The project's musical grid as a conductor: tempo points with curved ramps, signature changes, key changes, markers and an arrangement lane, anchored in musical position (q = quarter-notes from bar 1, or bar + beat) so seconds are derived and an edit at bar 5 does not slide bar 9. PATCH /tempo (set_tempo) stays for flat edits and flattens ramps.
The body `conductor` is the document GET returns (`version` may be left out); it is validated and canonicalised. Same response, `dry_run` and `expected_version` as PATCH.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
conductorrequired
object
The conductor document (see GET).
dry_run
boolean
ripple
"ripple" | "hold"
expected_version
integer
The project's `edit_version` as you last read it (GET /projects/{id} returns it; every project-level write answers with the new one). When sent, the write is a compare-and-set: if anyone changed the project settings since, NOTHING is written and the response is a 409 `conflict` carrying `current_version` and `last_writer`. Omit it to write unconditionally.
Response — 200
Field
Type
Description
datarequired
object
200 Replaced (or previewed).400 Invalid conductor.401 Missing or invalid API key.404 Project not found.409 expected_version is stale: the project settings changed since you read them. Nothing was written; re-read and reapply.
PATCH/projects/{id}/conductor
Edit the project's conductor by id-addressed ops
The project's musical grid as a conductor: tempo points with curved ramps, signature changes, key changes, markers and an arrangement lane, anchored in musical position (q = quarter-notes from bar 1, or bar + beat) so seconds are derived and an edit at bar 5 does not slide bar 9. PATCH /tempo (set_tempo) stays for flat edits and flattens ramps.
Ops apply in order, all or nothing; items are addressed by id, so concurrent edits to different items merge. `dry_run: true` writes nothing and returns the diff in seconds (bars that move, music-span scenes, sound cues). A real write stores `settings.conductor` and the derived `settings.tempo` together; a grid change retimes bound sound cues and re-conforms music loops like PATCH /tempo. Scene windows are NOT re-fitted. Send `expected_version` for a strict compare-and-set (stale = 409 `conflict`). Free.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
opsrequired
ConductorOp[]
dry_run
boolean
Validate and return the diff; write nothing.
ripple
"ripple" | "hold"
'ripple': music-span scenes keep their bars and their windows are rewritten in the same call (changeset op or PATCH); 'hold' (default): everything stays on seconds.
expected_version
integer
The project's `edit_version` as you last read it (GET /projects/{id} returns it; every project-level write answers with the new one). When sent, the write is a compare-and-set: if anyone changed the project settings since, NOTHING is written and the response is a 409 `conflict` carrying `current_version` and `last_writer`. Omit it to write unconditionally.
Response — 200
Field
Type
Description
datarequired
object
200 Applied (or, with dry_run, previewed): the conductor, what changed and the diff.400 Invalid op (the message names its index), or the result is invalid (bpm range, ramp too steep).401 Missing or invalid API key.404 Project not found.409 expected_version is stale: the project settings changed since you read them. Nothing was written; re-read and reapply.
GET/projects/{id}/audio-conform
Status of audio-follows-tempo for the project's music
Audio follows tempo (conductor P2). With the conductor's `warp.mode` "on" (`set_warp` op, default off) each non-looping music bed is time-stretched (Rubber Band R3, pitch kept) so the song's beats land on the tempo map. It is baked once on the worker's CPU lane and hosted under a content-addressed key; the cue then plays that file (`assetUrl` = the bake, `warp` = source file, offset and key), so the editor preview, the share page and the export play the same audio. Warned past +-20% stretch, left as the song past +-50%. Re-baked when the map or the track changes; a stale bake is never played. Exports wait for running bakes and refuse (`audio_pending`, `audio_failed`) rather than play stale or un-conformed audio. Free. This read never starts a bake.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 Per-bed state.401 Missing or invalid API key.404 Project not found.
POST/projects/{id}/audio-conform
Ask for the music bakes the tempo map needs
Audio follows tempo (conductor P2). With the conductor's `warp.mode` "on" (`set_warp` op, default off) each non-looping music bed is time-stretched (Rubber Band R3, pitch kept) so the song's beats land on the tempo map. It is baked once on the worker's CPU lane and hosted under a content-addressed key; the cue then plays that file (`assetUrl` = the bake, `warp` = source file, offset and key), so the editor preview, the share page and the export play the same audio. Warned past +-20% stretch, left as the song past +-50%. Re-baked when the map or the track changes; a stale bake is never played. Exports wait for running bakes and refuse (`audio_pending`, `audio_failed`) rather than play stale or un-conformed audio. Free. Idempotent: the same map and song are a cache hit; poll until `pending` is false.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 Per-bed state after the request.401 Missing or invalid API key.404 Project not found.
POST/scenes/{sceneId}/elements
Add an element to a scene
Append one element — motion design, an image, or a text box — to an existing composition. SPENDS CREDITS: the new line goes through the scene-generation path, which compiles, persists, and dispatches a resolve job. Asynchronous; poll the scene. Contrast PATCH on the same path, which edits existing elements for free.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
kindrequired
"motion-design" | "image" | "text"
promptrequired
string
Creative prompt for motion-design/image, or the literal copy for text.
at_seconds
number
area
object
reference_scene_id
string
motion-design only: author the element to MATCH that resolved scene's design.
reference_image_url
string
motion-design only: a picture the author should look at. Keep `prompt` to intent (what to keep, what to change) rather than describing the picture.
Response — 202
Field
Type
Description
datarequired
object
202 Element accepted and generating.400 Invalid request body or parameters.401 Missing or invalid API key.402 Insufficient credits.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
PATCH/scenes/{sceneId}/elements
Edit elements in a scene
Element-level edits on a scene, expressed as patches against its scene source. This is the same vocabulary and the same applier the editor canvas uses for a drag, resize, retime, recolour or restyle, so an API caller and a person on the canvas produce identical results. Read the scene first to get current line numbers. Spends no credits — an edit is a source rewrite plus a transpile, not a generation. Scene ids are globally unique, so the flat path is unambiguous. The response carries `claimed_by` when another actor holds a live claim on the scene (the write is not blocked).
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
patchesrequired
object[]
expect_lines
object[]
Optimistic concurrency. Patches address source by line number, so pass the exact text you expect at the lines you are touching; a mismatch returns 409 rather than silently applying nothing. Omit to skip the check.
expected_version
integer
Optimistic concurrency at the ROW level — the scene's composition_version as you last read it. expect_lines guards the APPLY (are my line numbers still right?); this guards the WRITE (has anyone rewritten this scene since?). Without it, a user with the scene open in the editor can be silently overwritten, or overwrite you. A mismatch returns 409 and nothing is written. The response carries the resulting compositionVersion for your next call.
Response — 200
Field
Type
Description
successrequired
true
datarequired
object
200 Patches applied; scene recompiled and persisted.400 Invalid request body, or no patch matched the source.401 Missing or invalid API key.403 Plan does not include API access.404 Scene not found or not accessible.409 Stale write: a line in expect_lines no longer matches, expected_version is behind, or the row changed while the edit was being recompiled. Nothing was written.422 The patched script failed to compile.
POST/themes
Create a theme
Author a private theme owned by your account — a palette (colours keyed by role) and an optional type pairing. Reference the returned `slug` as `themeSlug` when creating a project. Only the safe design fields are settable here; image-generation routing and prompt directives are managed by the system.
Request body
Field
Type
Description
namerequired
string
Display name for the theme.
colorsrequired
object
Palette keyed by semantic role. bg, ink and accent are required; the rest optional.
fonts
object
Type pairing. Each font must be a published font family (see GET /v1/catalogs/fonts).
defaultMode
"light" | "dark"
How the palette reads. Default 'light'.
Response — 201
Field
Type
Description
datarequired
object
201 The created theme.400 Invalid body.401 Missing or invalid API key.
GET/scenes/{sceneId}/genhtml
Describe a scene's gen:html sub-compositions
Return the visual-unit map of every resolved gen:html sub-composition in a scene — the same "what's inside this composition" structure the editor canvas exposes on double-click, but read statically from the hosted blob (no browser). Each unit carries its id/label, whether it is a timed `.clip` layer, and its animation lanes (properties + times); engine fills also return the parsed window.__DATA__ (their editable surface) with confidence "partial". Read-only. Scene ids are globally unique, so the flat path is unambiguous.
Parameters
Name
In
Description
sceneIdrequired
path
element_id
query
Describe only the composition with this element id (the id returned in `element_id`, derived from the blob's composition id).
Response — 200
Field
Type
Description
datarequired
object
200 The scene's gen:html unit maps.401 Missing or invalid API key.403 Plan does not include API access.404 Scene not found or not accessible.
POST/scenes/{sceneId}/genhtml
Edit a gen:html sub-composition's inner units
Apply deterministic, LLM-free edits to inner units of a resolved gen:html sub-composition: delete_unit removes the unit's element subtree; hide_unit hides it in place. The hosted blob is rewritten IMMUTABLY (a new copy is hosted and the scene's resolution swaps to it) — the previous URL is returned so the edit can be reverted. Unit keys come from GET …/genhtml.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
element_idrequired
string
The Composition element id (as returned by GET …/genhtml).
opsrequired
object[]
Response — 200
Field
Type
Description
datarequired
object
200 The edit result.400 Invalid body.404 Scene or composition not found.422 Edit could not be applied (e.g. unit not found in the blob).
GET/scenes/{sceneId}/authoring-context
Everything known about a beat, before you author it
Return the authoring envelope for each of a scene's composition elements — what the pipeline's own composition author is told about the beat it is writing. Read this BEFORE authoring or rewriting a composition: without it, an author is guessing at numbers the system already holds.
Per element: the brief; the director's numbered beats resolved to real seconds (each beat quotes a phrase of the narration, joined here to the word timeline); the canvas and the element's own window on the scene clock; the scene-relative VO word timeline; the musical grid re-based to the element's zero; the theme's resolved colour and type roles, image style and actual ground; the camera itinerary that will move over the composition; measured anchors; the previous and next scene; and the cast, generated imagery and user library available for verbatim reuse.
Timing, theme resolution, text-fit geometry and camera authority belong to the pipeline. Author against the values returned here rather than hard-coding equivalents.
Read-only and free — no credits, no writes, no generation.
Parameters
Name
In
Description
sceneIdrequired
path
element_id
query
Narrow to one composition element. Omit for every gen:html in the scene.
detail
query
Scene packet detail. compact (default) trims the project layer and caps words to the element window; full carries everything.
kind
query
Force a composition kind instead of the inferred one.
Response — 200
Field
Type
Description
datarequired
object
200 The beat's authoring context.401 Missing or invalid API key.403 Plan does not include API access.404 Scene not found, not accessible, or it has no script yet.503 The authoring service is temporarily unreachable (e.g. a worker redeploy). Nothing was spent; retry.
GET/authoring-contract
The vocabulary a composition is authored in
One record per attribute, CSS variable, declared global and callable primitive in the composition authoring contract. Each says WHEN its value appears — authored, injected by the worker at resolve, computed by a kernel at mount, or driven per frame off the paused timeline — and what literal NOT to write in its place.
`role=knob` returns the author-facing vocabulary alone, which is what an author needs. `role=machinery` returns the rest: worker-stamped kernel version markers, the `-done` receipts a kernel writes, and published values such as the measured anchor table. None of those are authored, and writing a receipt by hand suppresses the pass it names.
The response carries its own coverage report, including what it does not cover. Free, read-only, and not scene-scoped.
Parameters
Name
In
Description
role
query
Narrow the records returned.
kernel
query
Restrict to one kernel module, e.g. "tc-text".
Response — 200
Field
Type
Description
datarequired
object
200 The contract records plus a coverage report.401 Missing or invalid API key.403 Plan does not include API access.503 The authoring service is temporarily unreachable. Retry.
POST/compositions/validate
Would the pipeline accept this composition?
Check a composition against the REAL gates before landing it. Free — no hosting, no writes, no credits.
Runs the actual chain at its live configuration: the same normalisation every generated candidate receives, the hard structural gates (exactly one closed <template>, a composition id, a registered timeline, and the seek-law bans on <canvas>, requestAnimationFrame, appends to document.body and getElementsBy*), the configurable gate stack, and the vocabulary checks from GET /authoring-contract. A refusal is reported in the gate's own words, with per-stage results.
The most common refusal is driving animation from requestAnimationFrame or a wall clock: the renderer seeks a paused timeline and screenshots, so it never runs a frame loop and such a composition renders blank in the video while looking correct in a browser.
Reports whether the composition is CONFORMANT, never whether it is any good. To SEE it, pass `check: true`: the response gains `check` with the rendered findings and a `contact_sheet` image of the whole scene over time.
Request body
Field
Type
Description
htmlrequired
string
The composition source to check.
width
number
Canvas width it is authored against. Default 1920.
height
number
Canvas height. Default 1080.
duration
number
Scene duration in seconds. Default 10.
element_id
string
Element id it mounts as. Default "comp".
contract_only
boolean
Run only the vocabulary checks, skipping the gate stack.
check
boolean
Also render it on a CPU renderer and return what the checker measured (text visible at its word and inside the canvas, an empty stage, dead air, exposure, runtime errors) plus contact_sheet: ONE image of the whole scene, a frame every 0.5 s labelled with its time. A few seconds; only run when the gates accept the composition.
brief
string
What the scene is meant to say; sharpens the check.
Response — 200
Field
Type
Description
datarequired
object
200 The verdict: per-stage results, gate flags and contract findings.400 Invalid request body.401 Missing or invalid API key.403 Plan does not include API access.503 The authoring service is temporarily unreachable. Retry.
POST/scenes/{sceneId}/frame-report
What is on screen in this scene?
What is actually on screen at each requested time, as JSON — no screenshots. Free; cached by composition content.
Per visible element (named by data-tc-anchor, data-wire-id, id, or tag.class plus its text): box on the composition canvas, fraction in frame, clipping by an overflow:hidden ancestor (and which), text overflow, glyph px when the frame is shown 360 px wide, contrast against the CSS paint underneath (null over an image), what covers it and which text it overlaps. Plus a sampled visibility timeline, fonts that failed to load, tc-wire conformance when a wire spec is present, and grouped one-line `findings`.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
timesrequired
number[]
Scene-clock seconds to inspect (max 64).
element_id
string
Limit to one composition element.
width
number
Width the glyph-size check scales the frame to. Default 360 (a phone).
step
number
Visibility-timeline sample step in seconds. Default 0.5; 0 disables it.
Response — 200
Field
Type
Description
datarequired
object
200 The report: per-element frames, visibility, findings.400 Invalid request body.401 Missing or invalid API key.403 Plan does not include API access.404 Scene not found.429 Rate limited.503 The verification service is temporarily unreachable or busy. Retry.
POST/compositions/frame-report
What would this composition show?
What is actually on screen at each requested time, as JSON — no screenshots. Free; cached by composition content.
Per visible element (named by data-tc-anchor, data-wire-id, id, or tag.class plus its text): box on the composition canvas, fraction in frame, clipping by an overflow:hidden ancestor (and which), text overflow, glyph px when the frame is shown 360 px wide, contrast against the CSS paint underneath (null over an image), what covers it and which text it overlaps. Plus a sampled visibility timeline, fonts that failed to load, tc-wire conformance when a wire spec is present, and grouped one-line `findings`.
For a draft you hold: times are on its own clock.
Request body
Field
Type
Description
htmlrequired
string
The composition source to inspect.
timesrequired
number[]
Seconds on the composition clock (max 64).
width
number
Width the glyph-size check scales the frame to. Default 360.
step
number
Visibility-timeline sample step in seconds. Default 0.5; 0 disables it.
Response — 200
Field
Type
Description
datarequired
object
200 The report: per-element frames, visibility, findings.400 Invalid request body.401 Missing or invalid API key.403 Plan does not include API access.429 Rate limited.503 The verification service is temporarily unreachable or busy. Retry.
POST/scenes/{sceneId}/preview
Render real frames of this scene
Real pixel frames, exactly as the final MP4 shows them: the same document, on the same GPU renderer, through the same capture pipeline. Free.
Returns `frames` (one PNG URL per time); `determinism` — each check time taken cold vs after seeking elsewhere and back (seek-order), and before vs after the scripts run a second time (double-mount), pass/fail with PSNR, changed-pixel share and a diff image; `kernels` — what the kernels compiled and every moment they could not resolve; the checker's static `findings`; and a one-line `summary`.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
timesrequired
number[]
Scene-clock seconds to render (max 24).
element_id
string
Limit the static findings to one composition element.
determinism
boolean
Run the render-twice determinism check. Default true.
determinism_times
number[]
Times the determinism check uses. Default: up to 3 spread over times.
compare_mp4
boolean
Also diff each frame against the scene's cached MP4 at the same frame (PSNR per time, and whether that MP4 is current).
Response — 200
Field
Type
Description
datarequired
object
200 Frames, determinism verdict, kernel reports, findings.400 Invalid request body.401 Missing or invalid API key.403 Plan does not include API access.404 Scene not found.429 Rate limited.503 The preview renderer is cold, busy or unreachable. Retry.
POST/compositions/preview
Render real frames of a draft composition
Real pixel frames, exactly as the final MP4 shows them: the same document, on the same GPU renderer, through the same capture pipeline. Free.
Returns `frames` (one PNG URL per time); `determinism` — each check time taken cold vs after seeking elsewhere and back (seek-order), and before vs after the scripts run a second time (double-mount), pass/fail with PSNR, changed-pixel share and a diff image; `kernels` — what the kernels compiled and every moment they could not resolve; the checker's static `findings`; and a one-line `summary`.
For a draft you hold: times are on its own clock unless scene_id is given.
Request body
Field
Type
Description
htmlrequired
string
The composition source to preview.
timesrequired
number[]
Seconds to render (max 24).
scene_id
string
Preview the draft inside this scene, replacing its composition element.
element_id
string
Which composition element the draft replaces (scene form).
width
number
Canvas width (bare draft). Default 1920.
height
number
Canvas height (bare draft). Default 1080.
duration
number
Duration in seconds (bare draft). Default 10.
determinism
boolean
Run the render-twice determinism check. Default true.
determinism_times
number[]
Times the determinism check uses.
Response — 200
Field
Type
Description
datarequired
object
200 Frames, determinism verdict, kernel reports, findings.400 Invalid request body.401 Missing or invalid API key.403 Plan does not include API access.429 Rate limited.503 The preview renderer is cold, busy or unreachable. Retry.
PUT/scenes/{sceneId}/composition
Land a composition you authored
Put a composition the caller authored onto a scene's element — the other end of GET /scenes/{sceneId}/authoring-context.
It goes through the SAME path a generated composition takes: the real validation gate stack at its live configuration, then namespacing, per-instance CSS scoping, theme-variable injection, asset-slot expansion and hosting. Authored work is not a second-class route.
Timing, theme resolution, text-fit geometry and camera authority belong to the pipeline: author against the values the authoring context returns rather than hard-coding equivalents.
A refusal answers 422 carrying the gate's own verdict, so `dry_run: true` is free and worth doing first. Expect refusals for a requestAnimationFrame or wall-clock drive (the renderer seeks a paused timeline and never runs one), a <canvas> in the DOM path, an unclosed <template>, no registered timeline, or nodes appended to document.body.
Refuses a scene that is mid-generation rather than racing its write-back (409), and refuses if the scene changed while the composition was in flight (409) — re-read and retry. Free unless the HTML declares generated assets; any cost is reported.
Advisory claims: the response carries `claimed_by` when another actor holds a live claim on the scene (POST /scenes/{sceneId}/claim). The write is never blocked by a claim.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
htmlrequired
string
The composition source — a full HyperFrame sub-comp document.
element_id
string
Which composition element. Omit only when the scene has exactly one; a scene with several is refused rather than guessed at.
dry_run
boolean
Validate and report without hosting or writing anything. Free.
Response — 200
Field
Type
Description
datarequired
object
200 The composition was landed, or validated on a dry run.400 Invalid request, or the scene has no script to mount onto.401 Missing or invalid API key.403 Plan does not include API access.404 Scene not found or not accessible.409 The scene is mid-generation, or it changed while the composition was in flight. Nothing was written.422 Refused by the pipeline gates. The response carries the verdict under `validation`.503 The authoring service is temporarily unreachable. Nothing was written; retry.
POST/scenes/{sceneId}/claim
Say you are working on a scene (advisory claim)
Take, or renew, an ADVISORY time-limited claim on a scene: a lock the editor shows and other writers see as `claimed_by` on their scene writes. It never blocks anyone, and other holders do not stop yours (they come back in `held_by_others`). `ttl_seconds` defaults to 90 (5..900); POST again as the heartbeat. The holder is your declared session (X-Typecut-Session / the MCP connection), else your API key. 503 `unavailable` until the scene_claims migration is applied. MCP: claim_scene.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
label
string
What the lock reads, e.g. "Claude · scene 4 pass".
ttl_seconds
integer
Response — 200
Field
Type
Description
datarequired
object
200 Claimed (or renewed).400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.503 Scene claims are not enabled on this deployment yet.
PATCH/scenes/{sceneId}/claim
Renew a claim you still hold
Extends a live claim of yours. Unlike POST it never creates one: 404 when it lapsed (claim again).
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
ttl_seconds
integer
Response — 200
Field
Type
Description
datarequired
object
200 Renewed.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.503 Scene claims are not enabled on this deployment yet.
DELETE/scenes/{sceneId}/claim
Release your claim on a scene
Idempotent: `released: false` when you held nothing live. MCP: release_scene.
Parameters
Name
In
Description
sceneIdrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 Released (or nothing to release).400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
GET/scenes/{sceneId}/frames
List a scene's harvested frames
The frames of the scene's current composition_version and duration (`frames`), the previous version's frames while under 24 h old (`fallback_frames`), `thumbnail_version` (below composition_version means the published picture is stale), and `missing_t_ms`: the times still worth capturing (2 cut anchors, 6-12 uniform frames, gap fill until no gap exceeds max(duration/8, 1.5 s)). Any member may read.
Parameters
Name
In
Description
sceneIdrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The frames.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/scenes/{sceneId}/frames
Store harvested still frames of a scene
Multipart. Part `meta` (JSON): `{ composition_version, duration_ms, source: "agent"|"editor"|"worker", frames: [{ t_ms, width, height }] }`; then one `frames` file per entry, in order (webp, jpeg or png, sniffed by magic bytes; the declared size must match the image). Needs the editor role on the scene's project. The frames must belong to the scene's CURRENT composition_version and duration (409 `stale_version` / `duration_mismatch` with the current values otherwise). At most 16 frames per call and 16 stored per scene version, each at most 400 KB and 160..960 px wide with t_ms inside the duration (413 over the size caps). A frame at an occupied slot (same t_ms and width, or within 150 ms) is a `duplicate` unless its source outranks the stored one (worker > agent > editor), which replaces it. Stored publicly under media/<account>/frames/<scene>/<version>/. On success the scene's published thumbnail_url and filmstrip_url move forward to this version (never back to an older one). About 200 calls per hour per account.
Parameters
Name
In
Description
sceneIdrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 Per-frame outcome and what the scene's published pointer did.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.409 Conflict with a concurrent change.413 The body or a frame is over the size cap.415 A frame is not a webp, jpeg or png image.429 Rate limit reached. Retry after the seconds in the error.
GET/projects/{id}/claims
List the live scene claims in a project
Every live claim (people in the editor and agents), longest-held first. Lapsed claims are not returned. Empty until the scene_claims migration is applied.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The live claims.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
GET/projects/{id}/history
What happened to a project, and who did it
One described record per change, newest first, from `project_events` (every writer: the editor, API keys and the connector, agent runs, the Director, the pipeline). Before recording started, or while the table is empty or absent, the legacy ledgers (project_edits, changesets, editor_revisions) answer instead: `source`, `recording_since` and `notes` say which. Project-level changes (tempo / conductor, music cues, mix, groove, theme, captions, branding, voice, aspect ratio, ...) are described key by key in `changes[]`. Every member can list; `changes[]` is reviewer and above. An API key acts as its account. MCP: get_project_history.
Parameters
Name
In
Description
idrequired
path
actor_kind
query
actor
query
agent_run_id
query
scene_id
query
chapter_id
query
kind
query
since
query
until
query
group_id
query
limit
query
cursor
query
Response — 200
Field
Type
Description
datarequired
ProjectHistoryPage
200 A page of described events.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.500 Unexpected server error.
GET/projects/{id}/history/{event_id}
One history event in full
The described changes, the structured before -> after rows, the version range, the old values the ledger kept, and pointers to where the rest lives (the editor_revisions row with the previous composition or script; harvested stills before and after). Reviewer and above; a viewer gets 403 and keeps the list. Raw scene source never comes back.
Parameters
Name
In
Description
idrequired
path
event_idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The event.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.500 Unexpected server error.
GET/projects/{id}/activity
Who is working on a project right now
Live scene claims (people and agents, with expiry), everyone who wrote in the last `minutes` (from the history ledger, with the legacy fallback), and the agent runs on this project (status, last tool, scenes touched). Presence (who has the editor open) is a browser-only Realtime channel and is NOT available server-side: the response says so. MCP: get_project_activity.
Parameters
Name
In
Description
idrequired
path
minutes
query
Response — 200
Field
Type
Description
datarequired
object
200 Who is on the project.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.500 Unexpected server error.
PUT/projects/{id}/mix
Set how the music sits under the voice
Ducking mode (`bed` | `off`), the duck dials, sidechain `ducks` from any source, the aux `room`, and film-editor MOMENTS hung off scene edges (swell, out, in, hit). Applied in the editor, the share link and the export alike. `expected_version` makes it a compare-and-set.
Sidechain from any source: REPLACES all rules ([] clears), at most 4. The target bus (music | sfx) dips while the source (vo | sfx) sounds; duck_db -30..0 (default -8 on music, -4 on sfx).
room
object | null
Aux sends into one shared reverb: send.vo|music|sfx 0..1 (default 0 = no room), return_db -24..6 (default -6), decay_ms 100..6000, pre_delay_ms 0..200, damping 500..16000 Hz. A merge; null knobs reset, null clears the room.
moments
object[]
Replace ALL moments ([] clears).
add_moments
object[]
remove_moments
integer[]
clear_scenes
string[]
expected_version
integer
The project's `edit_version` as you last read it (GET /projects/{id} returns it; every project-level write answers with the new one). When sent, the write is a compare-and-set: if anyone changed the project settings since, NOTHING is written and the response is a 409 `conflict` carrying `current_version` and `last_writer`. Omit it to write unconditionally.
Response — 200
Field
Type
Description
datarequired
object
200 The mix after the change, with `edit_version`.400 Invalid request body or parameters.401 Missing or invalid API key.404 Not found, or not accessible with this key.409 expected_version is stale: the project settings changed since you read them. Nothing was written; re-read and reapply.
GET/workspaces/{id}/mix-profile
Get a workspace's default audio mix profile
The default audio profile (how music ducks under voice, voice/music/sfx bus levels) that projects created in this workspace start with. `profile` is null when none is set. A project created with `mix_from_project_id` uses that basis instead.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The profile or null.401 Missing or invalid API key.404 Not found, or not accessible with this key.500 Unexpected server error.
PUT/workspaces/{id}/mix-profile
Set a workspace's default audio mix profile
Send `{ profile: { mix, voMasterVolume?, musicMasterVolume?, sfxMasterVolume? } }` to set it (stored clamped; `mix.moments` is dropped), `{ from_project_id }` to copy a project's current profile, or `{ profile: null }` to clear. Applies only to projects created afterwards; existing projects are never changed.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
profile
object | null
from_project_id
string
Response — 200
Field
Type
Description
datarequired
object
200 The stored profile (or null).400 Invalid request body or parameters.401 Missing or invalid API key.404 Not found, or not accessible with this key.500 Unexpected server error.
PATCH/projects/{id}/music
Set, replace or clear the project's music bed
A catalog `trackId`, a BYO `url`, a `sequence` of beds, or `generate` (commission one). Sending none of them CLEARS the bed. `expected_version` makes the write a compare-and-set (not used by `generate`, which only commissions a track).
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
trackId
string | null
url
string | null
sequence
string[]
generate
object
volume
number
fadeIn
number
fadeOut
number
loop
boolean
startFrom
number
endAt
number
expected_version
integer
The project's `edit_version` as you last read it (GET /projects/{id} returns it; every project-level write answers with the new one). When sent, the write is a compare-and-set: if anyone changed the project settings since, NOTHING is written and the response is a 409 `conflict` carrying `current_version` and `last_writer`. Omit it to write unconditionally.
Response — 200
Field
Type
Description
datarequired
object
200 The bed now set (null when cleared or commissioned), with `edit_version`.400 Invalid request body or parameters.401 Missing or invalid API key.402 Insufficient credits.404 Not found, or not accessible with this key.409 expected_version is stale: the project settings changed since you read them. Nothing was written; re-read and reapply.
PATCH/projects/{id}/voice
Set the project's narration voice
Assigns a voice from GET /projects/{id}/voice. Does not re-voice anything already generated. `expected_version` makes it a compare-and-set.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
voice_idrequired
string
expected_version
integer
The project's `edit_version` as you last read it (GET /projects/{id} returns it; every project-level write answers with the new one). When sent, the write is a compare-and-set: if anyone changed the project settings since, NOTHING is written and the response is a 409 `conflict` carrying `current_version` and `last_writer`. Omit it to write unconditionally.
Response — 200
Field
Type
Description
datarequired
object
200 The voice now set, with `edit_version`.400 Invalid request body or parameters.401 Missing or invalid API key.404 Not found, or not accessible with this key.409 expected_version is stale: the project settings changed since you read them. Nothing was written; re-read and reapply.
PUT/projects/{id}/runtime
Declare the shared modules a project (or chapter) runs on
Module refs "id@version" in load order; `chapter_id` sets one chapter's own list; `modules: null` clears. `expected_version` makes it a compare-and-set.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
modulesrequired
array | null
chapter_id
string
expected_version
integer
The project's `edit_version` as you last read it (GET /projects/{id} returns it; every project-level write answers with the new one). When sent, the write is a compare-and-set: if anyone changed the project settings since, NOTHING is written and the response is a 409 `conflict` carrying `current_version` and `last_writer`. Omit it to write unconditionally.
Response — 200
Field
Type
Description
datarequired
object
200 The runtime now stored, with `edit_version`.400 Invalid request body or parameters.401 Missing or invalid API key.404 Not found, or not accessible with this key.409 expected_version is stale: the project settings changed since you read them. Nothing was written; re-read and reapply.
GET/projects/{id}/reference
The source a project is meant to match
The video analysis recorded as the project's reference, with its state (status, duration, shots, size). `reference: null` when none is set.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The reference and its analysis.400 Invalid request body or parameters.401 Missing or invalid API key.404 Not found, or not accessible with this key.
PUT/projects/{id}/reference
Record the source a project is meant to match
Points the project at a video analysis (`analysis_id`, analyze_video first) and/or an audio analysis (`audio_analysis_id`, analyze_audio first), with an optional `alignment` of replica scenes to spans of the source ([{scene_id, ref_t0, ref_t1}] in seconds; absent = proportional). The body replaces the whole reference; both ids null clears it. `expected_version` makes it a compare-and-set. Pictures of it: GET /projects/{id}/reference/frames.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
analysis_id
string | null
audio_analysis_id
string | null
alignment
array | null
note
string | null
expected_version
integer
The project's `edit_version` as you last read it (GET /projects/{id} returns it; every project-level write answers with the new one). When sent, the write is a compare-and-set: if anyone changed the project settings since, NOTHING is written and the response is a 409 `conflict` carrying `current_version` and `last_writer`. Omit it to write unconditionally.
Response — 200
Field
Type
Description
datarequired
object
200 The reference now stored, with `edit_version`.400 Invalid request body or parameters.401 Missing or invalid API key.404 Not found, or not accessible with this key.409 expected_version is stale: the project settings changed since you read them. Nothing was written; re-read and reapply.
GET/projects/{id}/reference/frames
A picture of the project's reference at any time, width and crop
Same as GET /video-analyses/{analysisId}/frames, resolved through the project's reference (404 when it has none). Times are on the SOURCE clock.
Parameters
Name
In
Description
idrequired
path
trequired
query
width
query
crop
query
Response — 200
Field
Type
Description
datarequired
object
200 The frames.400 Invalid request body or parameters.401 Missing or invalid API key.404 Not found, or not accessible with this key.
GET/projects/{id}/reference/facts
What the project's reference says in a window
The reference's shots (kind, framing, on-screen text, motion, palette, keyframe url), its transcript and any sound events inside a window, as rows. Times are seconds on the SOURCE clock: `from`/`to`, or `scene_id` for the span the reference alignment gives that replica scene. No window = the whole source (shots cap at 200, words at 4000). Free.
Parameters
Name
In
Description
idrequired
path
from
query
to
query
scene_id
query
words
query
Response — 200
Field
Type
Description
datarequired
object
200 The facts.400 Invalid request body or parameters.401 Missing or invalid API key.404 Not found, or not accessible with this key.
GET/projects/{id}/reference/find
Find shots of the project's reference
Shots whose on-screen `text` contains a string, or of a given `kind` / `framing`; or, with `similar_to` (a shot index), the nearest shots by picture (`measure` dino = composition and content, clip = meaning; cosine distance, smaller is closer). Needs the shot embeddings (they are filled in the background). Free.
Parameters
Name
In
Description
idrequired
path
text
query
kind
query
framing
query
similar_to
query
measure
query
scope
query
limit
query
Response — 200
Field
Type
Description
datarequired
object
200 The shots.400 Invalid request body or parameters.401 Missing or invalid API key.404 Not found, or not accessible with this key.
POST/projects/{id}/reference/compare
Compare a scene with the project's reference
Renders a few frames of the scene and takes the reference frames at the aligned times, then returns how far apart they look: a distance vector (DINO = composition and content, CLIP = meaning; cosine distance, smaller is closer), never one score, plus the worst frames with both pictures. The scene must be aligned to the reference. Free.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
scene_idrequired
string
samples
integer
Frames to compare (default 5).
quick
boolean
Draft-quality scene frames (default true).
detail
boolean | array
Also read realism cues (glow, rim, edge, grain, weave, fringe) on one region of the worst-matching moment: true = the centre, or [x0, y0, x1, y1] as fractions of the frame. Use quick:false for grain and weave.
against_image_url
string
Compare the scene with this one picture instead of the reference (no alignment needed).
at
number
With against_image_url: scene time in seconds to take the frame at (default: the scene evenly).
Response — 200
Field
Type
Description
datarequired
object
200 The comparison.400 Invalid request body or parameters.401 Missing or invalid API key.404 Not found, or not accessible with this key.503
GET/members
List members
Account-level, hand-made API keys only: workspace-, project- and budget-scoped keys and connector tokens get 403. A key is never a member; it acts as the account (owner authority). Returns the account's own user first (role owner, isAccountOwner true), then members.
Response — 200
Field
Type
Description
datarequired
object[]
200 Members.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.
POST/members
Invite a member
Account-level, hand-made API keys only: workspace-, project- and budget-scoped keys and connector tokens get 403. A key is never a member; it acts as the account (owner authority). Same as POST /members/invites: creates an email invite (7 days; one open invite per email; role cannot be owner). Nobody becomes a member until the invitee accepts. Typecut sends no email; hand over `accept_url`.
Request body
Field
Type
Description
emailrequired
string
Lowercased server-side.
rolerequired
"maintainer" | "editor" | "reviewer" | "viewer"
Response — 201
Field
Type
Description
datarequired
object
201 Invite created.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.409 An open invite for this email already exists.
PATCH/members/{user_id}
Change a member role
Account-level, hand-made API keys only: workspace-, project- and budget-scoped keys and connector tokens get 403. A key is never a member; it acts as the account (owner authority). Only an owner may grant, change or remove an owner. The account's own user cannot be changed (400).
200 Role changed.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
DELETE/members/{user_id}
Remove a member
Account-level, hand-made API keys only: workspace-, project- and budget-scoped keys and connector tokens get 403. A key is never a member; it acts as the account (owner authority). Only an owner may remove an owner. The account's own user cannot be removed (400).
Parameters
Name
In
Description
user_idrequired
path
204 Member removed.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
GET/members/invites
List open invites
Account-level, hand-made API keys only: workspace-, project- and budget-scoped keys and connector tokens get 403. A key is never a member; it acts as the account (owner authority). Open invites (not accepted, not revoked), expired ones flagged. Never includes tokens.
Response — 200
Field
Type
Description
datarequired
object[]
200 Invites.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.
POST/members/invites
Create an invite
Account-level, hand-made API keys only: workspace-, project- and budget-scoped keys and connector tokens get 403. A key is never a member; it acts as the account (owner authority). Creates an invite and returns the raw token and accept link ONCE. Typecut stores only a sha256 of the token and sends no email. The invitee must be signed in with the invited, verified email.
Request body
Field
Type
Description
emailrequired
string
Lowercased server-side.
rolerequired
"maintainer" | "editor" | "reviewer" | "viewer"
Response — 201
Field
Type
Description
datarequired
object
201 Invite created.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.409 An open invite for this email already exists.
DELETE/members/invites/{id}
Revoke an invite
Account-level, hand-made API keys only: workspace-, project- and budget-scoped keys and connector tokens get 403. A key is never a member; it acts as the account (owner authority). The invite link stops working immediately.
Parameters
Name
In
Description
idrequired
path
204 Invite revoked.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/members/invites/{id}/reissue
Re-issue an invite link
Account-level, hand-made API keys only: workspace-, project- and budget-scoped keys and connector tokens get 403. A key is never a member; it acts as the account (owner authority). Rotates the token of an open invite (the old link stops working) and restarts the 7-day expiry. Returns the new link once.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 New link.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/scenes
Create a scene
Create a scene in a chapter. A prompt-only body makes a draft composition scene (generation pending); a `media_url` body makes a resolved "uploaded" scene with its background already set. Shares scene-crud-service with the Director chat.
Request body
Field
Type
Description
chapter_idrequired
string
block_id
string | null
anchor_text
string | null
title
string | null
prompt
string | null
duration
number | null
media_url
string | null
is_video
boolean
thumbnail_url
string | null
position
integer | null
reference_image_url
string | null
Draft scenes only: a picture the author should look at (a library asset url). Keep `prompt` to intent rather than describing the picture.
narration
string | null
Place the scene at the END of the chapter, in a NEW narration block carrying this text, anchored at its start; the block is voiced on the next resolve and its voiceover drives the scene timing. Mutually exclusive with block_id / anchor_text. Omit both for a scene with no narration: it lands after the chapter's voiced track and keeps the `duration` given.
Response — 201
Field
Type
Description
datarequired
object
201 Scene created.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
GET/scenes/{sceneId}
Get a scene
Read one scene in full, including its composition and `composition_version`. Read a scene before rewriting it — patches address the source by line number.
Parameters
Name
In
Description
sceneIdrequired
path
Response — 200
Field
Type
Description
datarequired
Scene
200 The scene.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
PATCH/scenes/{sceneId}
Update a scene
Update a scene's own fields. To adjust one element inside the composition, prefer PATCH /scenes/{sceneId}/elements — it patches source lines instead of replacing the script. The response carries `claimed_by` when another actor holds a live claim on the scene (the write is not blocked).
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
title
string | null
prompt
string | null
duration
number | null
captions_enabled
boolean | null
caption_style_id
string | null
block_id
string | null
Response — 200
Field
Type
Description
datarequired
object
200 Scene updated.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
DELETE/scenes/{sceneId}
Delete a scene
Soft-delete: the scene becomes a restorable tombstone rather than being removed, so other clients reconcile the removal and the change can be undone.
Parameters
Name
In
Description
sceneIdrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 Scene soft-deleted.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/me/delete
Delete the account
Permanently deletes the account behind this key: projects, workspaces, media, voices, API keys and sessions. Live subscriptions are cancelled immediately and saved cards removed. The credit ledger is kept, anonymised, for accounting. Requires a full-access hand-made key (connector tokens and workspace-scoped keys get 403) and `{ "confirm": "DELETE" }`. Not reversible. The same deletion is in the app at /settings/account/delete.
Request body
Field
Type
Description
confirmrequired
"DELETE"
Response — 200
Field
Type
Description
datarequired
object
200 Account deleted.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.500 Unexpected server error.503 Billing could not be closed; nothing was deleted. Retry later.
GET/scenes/{sceneId}/render
Get single-scene render status
Poll for the rendered clip. The URL lands on the scene once the job completes.
Parameters
Name
In
Description
sceneIdrequired
path
Response — 200
Field
Type
Description
data
object
200 Render status.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/scenes/{sceneId}/render
Render a single scene
Render one scene to a standalone MP4 — no project stitch, no audio bed. SPENDS CREDITS. Shares render-scene-service with the Studio render button. Poll with GET on the same path.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
resolution
string
Response — 202
Field
Type
Description
data
object
202 Render queued.400 Invalid request body or parameters.401 Missing or invalid API key.402 Insufficient credits.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
GET/projects/{id}/storyboard
Get the storyboard
The structural map of a project: chapters, script blocks, and the scenes anchored to each block, with voice-over status and durations. The orientation call — read this before editing so you address real ids. Scene detail is deliberately excluded; use GET /scenes/{id}.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
data
object
200 The storyboard.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
GET/projects/requirements
Inspect creation requirements
Dry-run of the template-driven creation resolver: given a template and a workspace, reports what the workspace presets would prefill, what is still required, and what the template offers as optional — creating nothing. Same resolver POST /projects runs, so an agent can ask exactly the right questions before spending anything.
Parameters
Name
In
Description
templateSlugrequired
query
workspaceId
query
usePresets
query
Response — 200
Field
Type
Description
data
object
200 Resolved requirements.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/projects/{id}/chapters
Create a chapter
Create a chapter. Pair with POST /chapters/{chapterId}/blocks and POST /scenes to build a project's structure headlessly, using the same services the Director chat uses.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
title
string | null
position
integer | null
Response — 201
Field
Type
Description
datarequired
object
201 Chapter created.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/projects/{id}/generate-scenes
Bulk-generate scenes
Start bulk scene generation on a planned draft — the "Generate scenes" button, headless. Valid only from status='draft' with a script. SPENDS CREDITS: the bulk estimate is deducted upfront, with refund-on-failure.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 Bulk generation started.400 Invalid request body or parameters.401 Missing or invalid API key.402 Insufficient credits.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/projects/{id}/share
Create a share link
Mint a public share link. Same gate as the editor Share button: the project must be fully resolved — no unresolved scenes, no chapter missing voice-over — before a link is minted. Pass `{ partial: true }` to mint mid-build (still-resolving scenes play as placeholders); the response then carries `ready` and `pending`.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
partial
boolean
Response — 200
Field
Type
Description
datarequired
object
200 Share link.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.409 Conflict with a concurrent change.
POST/chapters/{chapterId}/blocks
Add a script block
Add a block to the chapter's script. Blocks live in chapters.script_blocks; narration blocks are what voice-over and scene anchors attach to.
Parameters
Name
In
Description
chapterIdrequired
path
Request body
Field
Type
Description
type
"narration" | "direction" | "note" | "heading"
contentrequired
string
position
integer | null
Response — 201
Field
Type
Description
datarequired
object
201 Block added.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
PATCH/chapters/{chapterId}/blocks/{blockId}
Rewrite a script block
Replace a block's text. Timing derived from the OLD text — pauses, silence trims, scene anchors — is cleared, because offsets into replaced text would be meaningless. Re-anchor scenes afterwards if the block carried any.
Parameters
Name
In
Description
chapterIdrequired
path
blockIdrequired
path
Request body
Field
Type
Description
contentrequired
string
Response — 200
Field
Type
Description
datarequired
object
200 Block updated.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/chapters/{chapterId}/blocks/{blockId}/cuts
Strike or restore words in a voiced block
Text-based editing: remove words from a block's voice-over WITHOUT regenerating it. The text stays (shown struck through in the editor); the audio span is skipped in preview, export and captions. Give a `phrase` (first occurrence, case-insensitive) or a character range; a selection that clips a word takes the whole word. Cut edges are placed in the silence between words so no word is clipped. `action:"restore"` with no range restores every cut. Cuts are tied to the current voice take: regenerating the voice drops them. Free.
Parameters
Name
In
Description
chapterIdrequired
path
blockIdrequired
path
Request body
Field
Type
Description
action
"cut" | "restore"
phrase
string
char_from
integer
char_to
integer
Response — 200
Field
Type
Description
datarequired
object
200 Cuts updated.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
GET/projects/{id}/forks
A project's lineage
Where the project was forked from (if anywhere) and the forks and snapshots made from it, newest first. Free.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The lineage.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/projects/{id}/forks
Fork a project, or freeze a snapshot of it
Copy a project into a NEW project that continues on its own, keeping the lineage. Chapters, script, pauses, scenes and their compositions, music, theme, captions, bible and characters come along; voice and media are shared by URL (every voice take is kept). History, comments, costs, renders and shares are not copied. `kind: "snapshot"` makes a frozen, archived copy; restore one by forking it. Counts toward the plan's project limit. Free.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
name
string
kind
"fork" | "snapshot"
workspace_id
string
Response — 201
Field
Type
Description
datarequired
object
201 The new project.400 Invalid request body or parameters.401 Missing or invalid API key.402 Insufficient credits.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/projects/{id}/voice-analysis
Measure the silences in a project's existing voice-over
Voice-overs made recently carry a silence map so cuts and pauses land between words. Older ones do not; this measures them from the stored audio and fills it in. No vendor call, no credits, nothing re-voiced, idempotent. Returns per-chapter counts of blocks analysed, skipped and failed. Free.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 Backfill done.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.503
GET/chapters/{chapterId}/blocks/{blockId}/takes
List the voice takes of a block
Every synthesis of a block's voice is kept as a take (a re-voice appends one instead of replacing the last). Returns each take with its audio url, length and word count, and which one is active (the one that plays). Free.
Parameters
Name
In
Description
chapterIdrequired
path
blockIdrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The block's takes.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
PUT/chapters/{chapterId}/blocks/{blockId}/takes
Make a voice take the active one
Switch which take of a block plays. Nothing is re-voiced and nothing is billed; the block's anchored scenes re-time to the take's length and the chapter timeline follows. Pauses are text positions and carry over; cuts and silence trims were made on a specific take, so `stale_edits` counts the ones that no longer apply. Free.
Parameters
Name
In
Description
chapterIdrequired
path
blockIdrequired
path
Request body
Field
Type
Description
take_idrequired
string
Response — 200
Field
Type
Description
datarequired
object
200 The take is active.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
The block's silence trims, sorted by position: each moves the natural silence after the word ending at `at_audio_ms`. Free.
Parameters
Name
In
Description
chapterIdrequired
path
blockIdrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The block's trims.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
Lengthen (`delta_ms` > 0) or shorten (< 0) the natural silence after the word ending at `at_audio_ms` (audio-file ms); 0 removes the trim; at most 5000 ms either way. This is what dragging a dark gap box on the timeline's voice track writes. Nothing is re-voiced; the block's anchored scenes re-time and the timeline follows. A trim belongs to the voice take it was made on. Free.
Parameters
Name
In
Description
chapterIdrequired
path
blockIdrequired
path
Request body
Field
Type
Description
at_audio_msrequired
number
delta_msrequired
number
Response — 200
Field
Type
Description
datarequired
object
200 The block's trims after the change.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
GET/scenes/{sceneId}/takes
List the saved versions (takes) of a scene
A take is a frozen copy of a scene's composition (script, metadata, captions, thumbnail). Returns each take newest first with its label, kind (`manual`, or `auto` when taken by a switch so nothing is lost), title, length and thumbnail, and which one the scene is right now. Scripts are never returned. Free.
Parameters
Name
In
Description
sceneIdrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The scene's takes.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/scenes/{sceneId}/takes
Save the scene as it is now as a take
Freeze the scene's composition as a take with an optional label and note. Placement (chapter, order, narration anchor) is not part of a take. Free.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
label
string
note
string
Response — 201
Field
Type
Description
datarequired
object
201 The new take.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
PUT/scenes/{sceneId}/takes
Make a take the scene
Write a take back onto the scene. If the scene as it is now matches no take it is frozen first (`saved_take_id`), so nothing is lost. A scene anchored to narration keeps its length (`duration_kept`) so the voice and timeline do not move; a free scene takes the take's length and the chapter timeline is rebuilt. Free.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
take_idrequired
string
Response — 200
Field
Type
Description
datarequired
object
200 The take is the scene now.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
GET/scenes/{sceneId}/takes/compare
Compare two versions of a scene
Two takes (or `current`, the scene as it is now) side by side: title, length, thumbnail, script size, and the composition fields that differ. Scripts are not returned. Free.
Parameters
Name
In
Description
sceneIdrequired
path
arequired
query
A take id, or "current".
b
query
A take id, or "current" (default).
Response — 200
Field
Type
Description
datarequired
object
200 The comparison.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
GET/chapters/{chapterId}/blocks/{blockId}/pauses
Read the pauses of a script block
The block's inline pauses (silence inserted into the voice-over timeline), sorted by position. Free.
Parameters
Name
In
Description
chapterIdrequired
path
blockIdrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The block's pauses.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
PUT/chapters/{chapterId}/blocks/{blockId}/pauses
Set the pauses of a script block
Set a block's pacing: a pause is SILENCE inserted into the voice-over timeline at a point in the text, so the words after it are heard later (the editor's pause chips). Nothing is re-voiced; the block's anchored scenes re-time and the chapter timeline is re-derived. Place each pause with `after_text` (right after that phrase, case-insensitive; `occurrence` = n-th match, default 1) or a raw `char_pos` — exactly one. `duration_ms` 1–20000, at most 64 pauses. `mode:"replace"` (default) makes the list the block's pauses (`[]` clears); `"merge"` keeps existing pauses at other positions. Pauses at the same position collapse (last wins). Pauses are text positions: they survive a voice regeneration, but rewriting the block text clears them. Free.
Parameters
Name
In
Description
chapterIdrequired
path
blockIdrequired
path
Request body
Field
Type
Description
pausesrequired
object[]
mode
"replace" | "merge"
Response — 200
Field
Type
Description
datarequired
object
200 Pauses updated.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/chapters/{chapterId}/reorder-scenes
Reorder a chapter's scenes
Set the full scene order. The body must list EVERY non-deleted scene id in the chapter exactly once — full-order semantics, so a stale caller cannot silently interleave with a concurrent edit.
Parameters
Name
In
Description
chapterIdrequired
path
Request body
Field
Type
Description
scene_idsrequired
string[]
Response — 200
Field
Type
Description
datarequired
object
200 New order applied.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
GET/projects/{id}/theme
Get the project theme
The project's visual identity: theme slug, colour roles, font roles, light/dark.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
ProjectTheme
200 The theme.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
PATCH/projects/{id}/theme
Switch or restyle the theme
Exactly one of `slug` (switch to an existing theme) or `restyle` (fork-on-edit). A restyle never mutates a shared preset: it copies the current theme into a private user theme with the patch applied and repoints the project at the fork. NOTE: themes are baked into each scene at generation time, so already-generated scenes keep their look until regenerated — the response repeats this in `note`.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
slug
string
expected_version
integer
The project's `edit_version` as you last read it (GET /projects/{id} returns it; every project-level write answers with the new one). When sent, the write is a compare-and-set: if anyone changed the project settings since, NOTHING is written and the response is a 409 `conflict` carrying `current_version` and `last_writer`. Omit it to write unconditionally.
restyle
object
Response — 200
Field
Type
Description
datarequired
object
200 Theme switched or forked.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.409 expected_version is stale: the project settings changed since you read them. Nothing was written; re-read and reapply.
GET/projects/{id}/captions
Get caption settings
The project-level caption default. Scene overrides live on the scene.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
ProjectCaptions
200 Caption settings.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
PATCH/projects/{id}/captions
Set caption settings
Set any subset of the caption default. Unlike a theme change this is free and needs no regeneration — captions are a render overlay, so the change lands on the next render. `style` is a fine-tune partial, DEEP-merged over the existing one; pass null to clear it. Preset ids come from GET /catalogs/caption-styles.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
enabled
boolean
styleId
string | null
colorScheme
"default" | "theme"
alignment
string | null
fontSize
number | null
style
object | null
expected_version
integer
The project's `edit_version` as you last read it (GET /projects/{id} returns it; every project-level write answers with the new one). When sent, the write is a compare-and-set: if anyone changed the project settings since, NOTHING is written and the response is a 409 `conflict` carrying `current_version` and `last_writer`. Omit it to write unconditionally.
Response — 200
Field
Type
Description
datarequired
ProjectCaptions
200 Updated caption settings.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.409 expected_version is stale: the project settings changed since you read them. Nothing was written; re-read and reapply.
GET/liked-shares
List liked shares
The staff "liked shares" collection — every share hearted on a /share/<token> page — as a flat list an ingestion client can enumerate without DB or browser access. STAFF ONLY: the key's account needs an admin role; other keys get 403. Shared collection, not per-account.
Response — 200
Field
Type
Description
datarequired
object[]
200 Liked shares, newest first.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.
GET/pricing
Get the pricing catalog
Plans, credit packs, marketing-flagged features, and per-generation estimates, assembled from the DB. PUBLIC — no authentication, CORS-open and CDN-cached, because the marketing site fetches it cross-origin.
Response — 200
200 Pricing catalog.
GET/price-book
Get the price book
Every price Typecut charges: one line per generation type plus the production base, classed pass-through vs tool, in credits and USD at list (1 credit = $0.01). `price_book_version` changes whenever any price does, so a quote or budget can say which book it was made from. `drift` lists any scene price the estimators quote that differs from what is charged (empty when reconciled). PUBLIC — no authentication, CORS-open and CDN-cached.
Response — 200
Field
Type
Description
datarequired
object
200 The price book.
POST/quotes
Quote operations
Price a list of operations before running them: credits_expected and credits_max per line and in total, in credits and USD at list, pinned to a price_book_version. Operations are `{type:"generation", slug, quantity?, units?, provider?}` (a /price-book line; units are characters or seconds per call), `{type:"scenes", media_source, count, quality_tier?}`, or `{type:"project", ...}` with the /projects/estimate inputs in snake_case. When a price depends on something left open (a TTS provider), credits_max is the dearest case. Stores the quote; moves no credits. 422 with a code when an operation cannot be priced.
Request body
Field
Type
Description
operationsrequired
object[]
Response — 201
Field
Type
Description
datarequired
object
201 The quote.400 Invalid request body or parameters.401 Missing or invalid API key.422 Understood, but could not be applied.
GET/quotes/{id}
Get a quote
Read back a quote this account made. 404 (not 403) when not accessible.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The quote.400 Invalid request body or parameters.401 Missing or invalid API key.404 Not found, or not accessible with this key.
POST/holds
Reserve credits
Hold a quote's credits_max (`quote_id`) or an amount (`credits`). A hold moves no credits; it lowers `credits_available` on GET /me (balance minus active holds) until it is captured, released or expires (`ttl_seconds`, default 3600), so parallel work cannot spend the same credits. Idempotent on `idempotency_key` (body) or the `Idempotency-Key` header: a retry returns the same hold with 200 and `replayed: true`. 402 when available credits fall short.
Request body
Field
Type
Description
quote_id
string
credits
integer
idempotency_key
string
ttl_seconds
integer
reason
string
Response — 200
Field
Type
Description
datarequired
object
200 A replay of an earlier hold with the same key.201 The hold.400 Invalid request body or parameters.401 Missing or invalid API key.402 Insufficient credits.404 Not found, or not accessible with this key.409 Conflict with a concurrent change.
GET/holds/{id}
Get a hold
One hold: held, captured, remaining, status. 404 (not 403) when not accessible.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The hold.400 Invalid request body or parameters.401 Missing or invalid API key.404 Not found, or not accessible with this key.
POST/holds/{id}/release
Release a hold
Give back what a hold still reserves. Safe to retry: a closed hold reports `released: 0`.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The hold after release.400 Invalid request body or parameters.401 Missing or invalid API key.404 Not found, or not accessible with this key.
POST/budgets
Create a budget and its key
A hard-capped budget and an api key bound to it, returned ONCE, to hand to an agent you spawn. The key reads anything this account can; it moves money only through holds inside its budget and every budget it nests in, so it cannot overspend. Other mutating calls with a budget key return 403 `budget_key_scope` until they charge through holds. Called with a budget key, the new budget nests in that key's budget; 402 `budget_exceeded` if the cap is more than is left there.
Request body
Field
Type
Description
caprequired
integer
label
string
expires_at
string
parent_id
string
Response — 201
Field
Type
Description
datarequired
object
201 The budget, with its api_key.400 Invalid request body or parameters.401 Missing or invalid API key.402 Insufficient credits.404 Not found, or not accessible with this key.
GET/budgets/{id}
Get a budget
Cap, spent, held and remaining. A budget key sees only its own budget and those nested in it. 404 (not 403) otherwise.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The budget.400 Invalid request body or parameters.401 Missing or invalid API key.404 Not found, or not accessible with this key.
POST/budgets/{id}/revoke
Revoke a budget
Revokes the budget and every budget nested in it, revokes their keys and releases their active holds. Idempotent.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The budget after revocation.400 Invalid request body or parameters.401 Missing or invalid API key.404 Not found, or not accessible with this key.
GET/projects/{id}/exports/subtitles
Export subtitles
The project's spoken words as a subtitle sidecar (SubRip or WebVTT), timed from the voiceover and chunked by the same caption plan the burned-in captions use. Needs no render and costs no credits. Returns 422 until the voiceover has been generated.
Parameters
Name
In
Description
idrequired
path
format
query
download
query
Response — 200
Field
Type
Description
filenamerequired
string
contentTyperequired
string
contentrequired
string
The file itself, as text.
200 The exported file.401 Missing or invalid API key.404 Project not found.422 Nothing to export yet (no voiceover).
POST/projects/{id}/exports/package
Export an editable project for an NLE
Starts a zip an editor opens in Premiere Pro, DaVinci Resolve or Final Cut Pro. Version 1 (default): FCP7 XML or FCPXML for one `flavour`, from the clips the project already rendered. Version 2 (behind the `nle_package` flag; 403 while it is off): OTIO for Premiere 25.6+ and Resolve plus FCPXML for Final Cut in one zip, clean scene renders (ProRes 4444 alpha layers in a transparent project), voiceover / music / SFX stems, SRT, a manifest, and chapter, beat and word markers. Costs no credits.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
version
number | number
Package version (default 1).
flavour
"premiere" | "finalcut" | "resolve"
Version 1 target app (default premiere). Ignored by version 2, which serves all three.
Response — 202
Field
Type
Description
generation_idrequired
string
Poll GET /generations/{id}; `output_url` is the zip when `completed`.
unrenderedrequired
object[]
Version 1 only: scenes with no rendered clip, missing from the timeline. Always empty for version 2.
202 Export queued.401 Missing or invalid API key.403 Version 2 is not enabled for this account.404 Project not found.422 Nothing to package yet.503 The export service is unavailable.
POST/issues
Report an issue
File a report about something that went wrong, from wherever it went wrong. The report itself is one sentence; everything needed to diagnose it is attached automatically — the project, the failures, the pipeline trace, and for a chat or editor issue the Director transcript pinned at the moment it was filed.
Returns 503 while issue intake is not yet enabled for this deployment.
Request body
Field
Type
Description
surfacerequired
"chat" | "editor" | "render" | "export"
What part of the product broke. Not a device: an issue reported from the phone is still `chat` or `editor`, with `app.shell` set to `capacitor`.
body
string
What went wrong, in the reporter's own words. One sentence is enough.
title
string
Override the generated title. Normally omitted — the body's first line is used.
project_id
string | null
The project the issue happened in. Required if `body` is empty.
level
"basic" | "full"
`full` also attaches the Director transcript (by reference plus a watermark), the pipeline-trace link and the reproduce handle. You only ever receive what your own credentials can read.
source
"product" | "admin" | "agent"
focus
object
Where the reporter was standing when it broke.
client_errors
object[]
app
object
replay_url
string | null
Session-replay URL, when one exists.
Response — 201
Field
Type
Description
datarequired
object
201 The ticket that was filed.400 Invalid body, or a report with neither words nor a project.401 Missing or invalid API key.503 Issue intake is not enabled yet.
GET/projects/{id}/comments
List review comments
The review comments on a project, in the order they were left. Default: OPEN threads with their replies (the work queue). Each is anchored to the project, a scene, a time range, an element id or a composition node path.
Parameters
Name
In
Description
idrequired
path
status
query
scene_id
query
kind
query
awaiting
query
limit
query
Response — 200
Field
Type
Description
datarequired
object
200 Comments.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/projects/{id}/comments
Leave a review comment
Comment on a project, a scene (scene_id), a moment or range (t_start/t_end), an element (element_id) or a composition node (composition_path), or point at several places with anchors. parent_id replies to a thread. kind is note (default) | question | blocker; to is anyone (default) | agent | director | run:<id>. A comment from a person steers the project's live agent runs. Free.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
bodyrequired
string
scene_id
string
t_start
number
t_end
number
element_id
string
composition_path
string
anchors
object[]
parent_id
string
kind
"note" | "question" | "blocker"
to
string
Response — 201
Field
Type
Description
datarequired
Comment
201 The comment.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/projects/{id}/comments/pass
Send a review pass
Send several small notes at once. Every note is validated first (one bad note refuses the pass), the notes share a pass_id, and the project's live agent runs are steered once with the whole pass. Each note takes the fields of POST /comments (except parent_id). Free.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
notesrequired
object[]
Response — 201
Field
Type
Description
datarequired
object
201 The pass.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/projects/{id}/comments/{commentId}/resolve
Resolve a review comment
Mark a comment resolved and link the change that answered it: changeset_id (apply_changes) and/or revision_id (the edit-history ledger). Links must belong to this project. reopen: true reopens. Free.
Parameters
Name
In
Description
idrequired
path
commentIdrequired
path
Request body
Field
Type
Description
changeset_id
string
revision_id
string
note
string
reopen
boolean
Response — 200
Field
Type
Description
datarequired
Comment
200 The updated comment.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
GET/projects/{id}/revisions
Edit history (restore points)
The edit-history ledger, newest first. Each row is the PRIOR state of a scene or chapter captured just before an update or delete, whoever made it. Metadata only.
Parameters
Name
In
Description
idrequired
path
scene_id
query
limit
query
Response — 200
Field
Type
Description
datarequired
object
200 Revisions.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.
POST/scenes/{sceneId}/undo
Undo a scene edit
Roll a scene back through the edit-history ledger: steps (1-10) rewinds that many edits, revision_id restores one exact restore point of this scene. Covers every kind of scene edit. Free and non-destructive: the rollback is itself recorded.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
steps
integer
revision_id
string
Response — 200
Field
Type
Description
datarequired
object
200 The restore point that was applied.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.422 Understood, but could not be applied.
POST/scenes/{sceneId}/verdict
Accept or reject a scene
Record an explicit accept or reject of a scene as a feedback signal, keyed to the scene's latest generation. Changes nothing about the scene. Free.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
verdictrequired
"accept" | "reject"
reason
string
comment_id
string
Response — 201
Field
Type
Description
datarequired
object
201 The recorded verdict.400 Invalid request body or parameters.401 Missing or invalid API key.403 Plan does not include API access, or the action is not permitted.404 Not found, or not accessible with this key.500 Unexpected server error.
GET/projects/{id}/map
The project map
Chapters, blocks and scenes with film windows, briefs, states, and the words heard in each scene on the scene clock as [word, start_sec, end_sec]; the tempo summary; what is left to do. `?words=false` drops the words. Free.
Parameters
Name
In
Description
idrequired
path
words
query
Response — 200
Field
Type
Description
datarequired
object
200 The map.401 Missing or invalid API key.404 Project not found.
GET/projects/{id}/beat-grid
The beat grid
The project tempo as plain JSON on the film clock: bars [bar, sec], beats [sec, bar, beat], tempo sections, the music beds with their feature files, and the feature channel names. `grid` is null for a project with no tempo. Free.
Parameters
Name
In
Description
idrequired
path
from
query
to
query
Response — 200
Field
Type
Description
datarequired
object
200 The grid.401 Missing or invalid API key.404 Project not found.
GET/projects/{id}/timeline
The film timeline
The whole film on ONE clock (film seconds, all chapters end to end): the chapter/scene tree by node path (ch:<id>/sc:<id>/el:<id>) with each scene's window, the tempo map with its sections, and every timed track — voice, music beds (in/out, loop, conformed bpm), chapter score cues, SFX (seconds and bar:beat), transitions per seam, grooves, captions. A summary by default; `detail=1` (or `from`/`to`) expands a range with bars, beats, words and elements; `chapter_id` narrows to one chapter. Free.
Parameters
Name
In
Description
idrequired
path
chapter_id
query
detail
query
from
query
to
query
words
query
Response — 200
Field
Type
Description
datarequired
object
200 The timeline.401 Missing or invalid API key.404 Project or chapter not found.
POST/projects/{id}/check-sync
Do picture and sound hit together?
Every scene's hit-like moves (cuts, instant sets/jumps, sharp-eased scale/opacity/position/rotation/shape moves, read off the mounted composition's timeline and timed where their speed peaks) matched on the FILM clock against the sound's onsets: the music beds' analysis, SFX / seam / marker cues, chapter score cues, and `onsets` you pass (e.g. a score trigger map). Returns `unsounded_motion`, `unmotivated_sound` (accent sounds only) and `weak_sync` with film seconds and bar:beat, plus a summary with median/p90 offsets. Free.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
scene_ids
string[]
Only these scenes (and the sound under them). Default every placed scene (max 40).
tolerance_ms
number
How far apart still counts as together. Default 40.
onsets
object[]
Extra onsets on the film clock.
detail
boolean
Also list every matched pair.
Response — 200
Field
Type
Description
datarequired
object
200 The sync report.400 Invalid request body, or too many scenes.401 Missing or invalid API key.404 Project or scene not found.503 The picture reader is temporarily unreachable. Retry.
POST/projects/{id}/hyperframes/export
Export the project as a HyperFrames folder
The project as a plain HyperFrames folder that upstream `npx hyperframes render .` plays: root index.html with every scene a sub-composition at its film window (the get_timeline tree), compositions/ (native scenes: the authored document; structured scenes: compiled output), captions/ (the captions layer), audio tracks, typecut.json. The zip is hosted and its url returned; `format=zip` returns the zip itself. Free.
Parameters
Name
In
Description
idrequired
path
format
query
scenes
query
Comma-separated scene ids (at most 20): just those documents, inline and stamped as the folder holds them.
Response — 200
Field
Type
Description
datarequired
object
200 The export: url, files, scene counts, warnings (or the zip bytes; or, with scenes, the documents inline).401 Missing or invalid API key.404 Project not found.
GET/projects/{id}/hyperframes/watch
Wait for the project to move (synced-folder long-poll)
Each live scene's composition_version and the project's structure_version, with a cursor fingerprinting them. Pass the last cursor and `wait` (seconds, at most 25) to be held until an editor save, edit_elements or a changeset moves something; `changed: false` when the wait ran out. Re-export the scenes whose version moved (`hyperframes/export?scenes=`). What `typecut dev` uses to pull a human's edits back to disk. Free.
Parameters
Name
In
Description
idrequired
path
cursor
query
wait
query
Response — 200
Field
Type
Description
datarequired
object
200 cursor, changed, structure_version, scenes [{scene_id, chapter_id, composition_version}].401 Missing or invalid API key.404 Project not found.
POST/scenes/{sceneId}/web-block
Export a scene as an embeddable web block
Publish a finished, resolved DOM+GSAP scene as a block any web page can embed and drive. You declare the contract in `manifest` (typed inputs, named timeline states, events, text/colour slots); a validation gate refuses a scene that is not portable or a manifest that does not match it, with a fix for every error. Returns a hosted embed_url and the two-line snippet (a data-typecut-block div plus the loader script). Re-exporting a scene keeps its embed_url and bumps the version. `dry_run` runs the gate only. Free.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
manifestrequired
object
{v:1, initial, inputs, states, events, slots}
name
string
dry_run
boolean
with_scene_audio
boolean
Add the scene's own voice and music as manifest.audio tracks (when the manifest has none).
Response — 200
Field
Type
Description
datarequired
object
200 block {token, version, embed_url, loader_url, snippet, duration_sec, warnings} (null on dry_run), valid, errors, warnings.400 Invalid body, scene not exportable, or the gate refused it (errors listed).401 Missing or invalid API key.404 Scene not found.
POST/chapters/{chapterId}/web-block
Export a chapter as one embeddable sequence
Publish a whole chapter as ONE embeddable block: its scenes play back to back on one clock, mounted a window at a time (WebGL, Pixi and project modules included), with the chapter voice and the project music carried across scene boundaries. Every scene must be HTML you authored. Returns a hosted embed_url (opens a play page in a browser) and the two-line snippet; the page gets block.play/pause/seek(sec). Re-exporting keeps the embed_url. `dry_run` runs the gate only. Free.
Add the chapter's voice and the project's music as audio tracks (default true; ignored when the manifest has audio).
Response — 200
Field
Type
Description
datarequired
object
200 block {kind, scene_count, token, version, embed_url, loader_url, snippet, duration_sec, warnings} (null on dry_run), valid, errors, warnings.400 Invalid body, a scene is not HTML you authored, or the gate refused it.401 Missing or invalid API key.404 Chapter not found.
POST/projects/{id}/hyperframes/import
Import a HyperFrames folder
An edited HyperFrames folder back into the project, as JSON `{files: [{path, content | src_url}], dry_run, force}` (src_url: a create_upload public_url, for documents too large to send inline) or the zip as the body (`application/zip`, `?dry_run=1&force=1`). Only documents that differ from the current ones land, through put_scene_composition; a root sub-composition with no scene becomes a new scene (its `data-chapter`, else the last chapter). A document exported from an older version of its scene is refused unless `force`. Landing is free; a composition that expands asset slots may spend.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
filesrequired
object[]
dry_run
boolean
force
boolean
Response — 200
Field
Type
Description
datarequired
object
200 Per file: unchanged, updated, created or refused, with the reason.400 Bad files or zip.401 Missing or invalid API key.404 Project not found.
POST/imports/html
Import a single-file HTML video, split into scenes
A video made elsewhere as ONE HTML file (a master GSAP timeline, every scene inline, often an <audio>) becomes a chapter of scenes: in a new project (`workspace_id`) or appended to one (`project_id`). Inline HyperFrames sub-compositions become scenes with their own documents; the rest is cut by time (GSAP labels, else section visibility windows, else full-frame clears, else shot changes on a low-res render; `cuts` overrides) into slices of ONE hosted copy of the file, each pixel-identical to its window. Root audio becomes the project music, or the voice (`audio: "voice"`: a speech clip per scene on the recording's transcribe_media transcript). Free. Non-seekable code is flagged; `verify` render-checks each scene and `video_url` turns a failing one into a clip of the file's own render. `dry_run` analyses and proposes, creating nothing. Staff and authoring-pilot accounts land; dry_run is open to all.
Request body
Field
Type
Description
html
string
src_url
string
base_url
string
workspace_id
string
name
string
project_id
string
chapter_id
string
chapter_title
string
cuts
number[]
duration_sec
number
min_scene_sec
number
max_scene_sec
number
audio
"music" | "voice" | "none"
shot_detection
"auto" | "off" | "force"
subcomps
boolean
video_url
string
verify
boolean
captions
boolean
dry_run
boolean
Response — 200
Field
Type
Description
datarequired
object
200 The analysis, the cuts and where they came from, every scene (kind, window, landed, flags), the audio, warnings.400 No file, no length, or a bad option.401 Missing or invalid API key.403 This account may not land authored HTML yet.404 Project, workspace or chapter not found.
POST/scenes/{sceneId}/extract-html
Lift an imported HTML slice into a standalone scene
import_html tier 3. An LLM pass writes the slice's window of the original file as a standalone composition on local time. It lands only if every compared frame is at or above `min_psnr` (default 36 dB, the HyperFrames conformance bar) against the slice, both rendered in the scene on the production renderer; otherwise the slice stays. `candidate_url` holds the attempt either way. Asynchronous: the first call starts it (202, status running); the same call again returns progress, then the result (200); `retry` starts a new attempt. `dry_run` diffs without landing. Staff and authoring-pilot accounts.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
dry_run
boolean
min_psnr
number
model
string
retry
boolean
Response — 200
Field
Type
Description
datarequired
object
200 The run: status (running | done | failed), and when done its result — landed, passed, psnr {min, mean, frames}, candidate_url, the slice window, reason.202 Started: status running. Call again to read it.400 Not an import_html slice.401 Missing or invalid API key.403 This account may not land authored HTML yet.404 Scene not found.503 The model or the renderer could not be reached.
POST/voices/speak/batch
Speak up to 20 lines
POST /voices/speak for each line (cache, per-character charge, word timings), reported per line in input order. Top-level voice_id / clone_id / language are defaults a line may override. Stops attempting lines after one runs out of credits.
Request body
Field
Type
Description
linesrequired
object[]
voice_id
string
clone_id
string
language
string
workspace_id
string
Response — 200
Field
Type
Description
datarequired
object
200 Per-line results.400 Invalid request.401 Missing or invalid API key.
POST/scenes/contexts
Authoring context for up to 10 scenes
One merged context per scene: GET /scenes/{id}/authoring-context plus the rulers, cues and heard words of GET /scenes/{id}/context. Per-scene results in input order. Free.
Request body
Field
Type
Description
scene_idsrequired
string[]
detail
"compact" | "full"
Response — 200
Field
Type
Description
datarequired
object
200 Per-scene contexts.400 Invalid request.401 Missing or invalid API key.
POST/scenes/compositions
Land up to 10 authored compositions
PUT /scenes/{id}/composition for each scene — the same gates, mount, hosting and write — with every verdict inline. `dry_run` validates without writing (free, parallel); real writes run one at a time. Authored HTML is staff-only for now (403 authoring_restricted), except on a dry run.
Request body
Field
Type
Description
scenesrequired
object[]
dry_run
boolean
Response — 200
Field
Type
Description
datarequired
object
200 Per-scene verdicts.400 Invalid request.401 Missing or invalid API key.403 Authored HTML is not enabled for this account.
POST/scenes/previews
Frame facts for up to 10 scenes
POST /scenes/{id}/frame-report for each scene at the given scene-clock seconds (per scene, or a shared `times`). Free.
Request body
Field
Type
Description
scenesrequired
object[]
times
number[]
width
integer
Response — 200
Field
Type
Description
datarequired
object
200 Per-scene frame reports.400 Invalid request.401 Missing or invalid API key.
POST/status
Poll many jobs
Generations (status, done, output_url, error) and projects (resolution rollup, latest render) in one call, ≤ 50 ids. `all_done` is true when nothing listed is still moving. Free.
Request body
Field
Type
Description
generation_ids
string[]
project_ids
string[]
Response — 200
Field
Type
Description
datarequired
object
200 Statuses.400 Invalid request.401 Missing or invalid API key.
GET/projects/{id}/bible
Get the project bible
The project's source of look and cast: look (palette and type roles, rules), cast by id (identity refs, renderers, voice), sets, motifs, story, props. When nobody has written one it is derived read-only from the theme, settings.visualSystem, the characters and the visual bible (`derived: true`, `version: 0`). Free.
Parameters
Name
In
Description
idrequired
path
parts
query
Response — 200
Field
Type
Description
datarequired
object
200 The bible.401 Missing or invalid API key.404 Project not found or not accessible.
PUT/projects/{id}/bible
Write the project bible
Write the bible whole, compare-and-set: base_version is the version you read (0 for a derived bible, which this first write stores). The version bumps by one. A stale base_version is a 409 carrying current_version. Free.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
biblerequired
Bible
base_versionrequired
integer
Response — 200
Field
Type
Description
datarequired
object
200 Stored; the new version.400 Invalid bible (the message names the field).401 Missing or invalid API key.404 Project not found or not accessible.409 Stale base_version; re-read and retry.
GET/projects/{id}/fs
List the project as files
The project as a tree of plain files with ids, sizes and states. The tree: project.json · chapters/<NN>-<slug>/chapter.json · chapters/<NN>-<slug>/<NNN>-<slug>/{scene.json, narration.md, composition.html}. Any directory segment may be the entity id, or its number alone. Free.
Parameters
Name
In
Description
idrequired
path
path
query
Response — 200
Field
Type
Description
datarequired
object
200 The tree.400 Invalid body or path.401 Missing or invalid API key.404 Project not found or not accessible.
GET/projects/{id}/repo-tree
The project as the files of a git repository
The project as a deterministic tree of text files (project.json, audio/*.json, bible.json, assets.lock.json, chapters/NN-slug/…) that a git client can commit: every source field once, generated things (word timings, thumbnails, renders) left out and listed in `report.omitted`. Assets are pinned in assets.lock.json by the hash of their URL (bytes and type from a HEAD on our own storage; bodies are not downloaded). Read-only. Free.
Parameters
Name
In
Description
idrequired
path
Response — 200
Field
Type
Description
datarequired
object
200 The files, the asset index and what was not written.401 Missing or invalid API key.404 Project not found or not accessible.
POST/projects/restore-tree
Restore a repository tree as a new project
The inverse of GET /projects/{id}/repo-tree: the files of a repository become a NEW project. The tree is validated as a whole first (allowlisted paths, at most 2000 files and 40 MB, schemas, every scene, block text file and $asset reference resolving); any problem answers 400 `invalid_tree` with every error and writes nothing. `dry_run` returns the plan and the report without writing. Otherwise a project is created and these steps run through the existing writers, in this order: tempo, theme, voice, captions, branding, runtime, chapters, block text, block pauses. Every id is minted by the server; `applied.id_map` maps the repo ids to the new ones. Not restored in this version (listed in `deferred`, with the reason): scenes, compositions, structured scene source, seams, cues, music, score, bible, groove and mix. `dropped` lists what has no writer or depends on generated audio (a block's voice take is reported `voice_not_restored: regenerate`). A step that fails leaves `status: partial` with the project in place; nothing is rolled back. Never charges credits and never generates media. The request body is bounded by the platform (about 4.5 MB on serverless) before the parser's own caps.
Request body
Field
Type
Description
filesrequired
object
Repo-relative path to file text, as GET /projects/{id}/repo-tree returns it.
workspace_id
string | null
Defaults to the key's own workspace, if it has one.
dry_run
boolean
Response — 200
Field
Type
Description
datarequired
object
200 Dry run: the plan and the report; nothing was written.201 The project was created; `status` says whether every step applied.400 Invalid body, or the tree was rejected (`code: invalid_tree`, with `errors`).401 Missing or invalid API key.402 Plan limit reached.403 The key cannot create projects in that workspace.404 No such workspace for this key.
POST/projects/{id}/fs/read
Read project files
Read up to 20 project files. composition.html is the resolved composition source, fetched from its hosted blob. A path that fails carries its own `error`. Free.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
path
string
paths
string[]
Response — 200
Field
Type
Description
datarequired
object
200 The files.400 Invalid body or path.401 Missing or invalid API key.404 Project not found or not accessible.
POST/projects/{id}/fs/write
Write a project file
Write one file, routed to the service that already owns it: structure through apply_changes (recorded, undoable), composition.html through PUT /scenes/{id}/composition's gates, the scene source after a validity check, and the scene, block, tempo, theme, voice and caption services for the rest. A .json write merges over the current file. A new scene directory (…/<NNN>-<slug>/scene.json or narration.md) creates the scene at that position. `dry_run` returns the diff and verdict without writing. 200 carries the verdict either way.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
pathrequired
string
content
string | object
.json: an object (or its text), merged. Others: the whole text. Send this or `edit`.
edit
object
music/ files only: patch the file without re-sending it. `old` must occur once (or send replace_all). The answer carries `music` {path, bytes, sha, lines} and never the file.
dry_run
boolean
Response — 200
Field
Type
Description
datarequired
object
200 The verdict.400 Invalid body or path.401 Missing or invalid API key.404 Project not found or not accessible.
PUT/projects/{id}/score
Play, plan, edit or read the status of the project's sound program
The project's score: a JSON `score`, a Python sound program (`code`), or the program kept as project files (`source: "music"`: music/main.py, music/voices.py, music/cues/*.py, music/mix.json, written with the fs endpoints). `edit {path, old, new}` patches a music/ file without re-sending it (answers path, bytes, sha, lines; no code echo). `plan: true` (with `source`) returns the Plan: a key, its hash and url (stored by hash; gzip JSON of events with t, q, bar, beat, midi, vel, len and numeric params, curves published with publish() sampled at 120 Hz, the beat axis it used, the chord track) and a summary; free, no audio. `status: <key>` reads a render or plan by key (rendering | planning | failed | done). `async: true` answers with the key at once. A render lists `triggers` (default 20, max 400) and a count per track, never the program. `dry_run`, `range`, `place` and `apply` as before.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
score
object
code
string
source
string
edit
object
plan
boolean
status
string
A render key (32 hex) or a plan key (pl_ + 32 hex).
async
boolean
triggers
integer
reference
string
dry_run
boolean
range
object
place
object
apply
boolean
Response — 200
Field
Type
Description
datarequired
object
200 The score view: render, plan, edited or status, whichever the call asked for.400 Invalid body or path.401 Missing or invalid API key.404 Project not found or not accessible.
POST/projects/{id}/fs/mv
Move a scene or chapter
Move a scene directory to chapters/<chapter>/<NNN> (within or across chapters; across, it is re-created with a new id) or a chapter directory to chapters/<NN>. One apply_changes batch. Free.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
fromrequired
string
torequired
string
dry_run
boolean
Response — 200
Field
Type
Description
datarequired
object
200 The verdict.400 Invalid body or path.401 Missing or invalid API key.404 Project not found or not accessible.
POST/projects/{id}/fs/rm
Delete a scene or chapter
Delete a scene or chapter directory as one apply_changes batch (undoable). Free.
Parameters
Name
In
Description
idrequired
path
Request body
Field
Type
Description
pathrequired
string
dry_run
boolean
Response — 200
Field
Type
Description
datarequired
object
200 The verdict.400 Invalid body or path.401 Missing or invalid API key.404 Project not found or not accessible.
POST/scenes/{sceneId}/split
Split a scene in two
`at` is seconds on the scene clock, or "word:<word>" — the word the second scene starts on. A scene anchored in narration keeps its voice: the second scene is anchored at that word in the same block and each takes its own window (seconds snap to the nearest word). Lengths sum to the original. The composition is cloned as is onto the second scene — re-author either one afterwards. Free.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
atrequired
number | string
dry_run
boolean
Response — 200
Field
Type
Description
datarequired
object
200 Dry run.201 Split; the changeset and the new scene id.400 Invalid body, or the verb cannot apply (the message says why).401 Missing or invalid API key.404 Scene or chapter not found or not accessible.
POST/scenes/merge
Merge two adjacent scenes
The earlier scene keeps its composition and takes the summed length; the later is removed. Across two adjacent blocks the second block's text is joined onto the first (a voiced block then needs its voice again). Free.
Request body
Field
Type
Description
scene_idsrequired
string[]
dry_run
boolean
Response — 200
Field
Type
Description
datarequired
object
200 Dry run.201 Merged.400 Invalid body, or the verb cannot apply (the message says why).401 Missing or invalid API key.404 Scene or chapter not found or not accessible.
POST/scenes/copy
Copy scenes into another project, by reference
Copies scene_ids (or every scene of chapter_id) from from_project into to_project. The composition is shared by its immutable url (pinned modules and linked kernels included), so nothing is pasted and the call stays small. Carries metadata, entry/exit state, length, title and the scene's own sound (include_sound false drops it); narration and voice stay behind. Needs read on the source and write on the target. Lands in to_chapter_id or a new chapter. Free.
Request body
Field
Type
Description
from_projectrequired
string
scene_ids
string[]
chapter_id
string
to_projectrequired
string
to_chapter_id
string
position
integer
include_sound
boolean
dry_run
boolean
Response — 200
Field
Type
Description
datarequired
object
200 Dry run.201 Copied: the new scene ids.400 Invalid body, or the verb cannot apply (the message says why).401 Missing or invalid API key.404 Scene or chapter not found or not accessible.
POST/scenes/{sceneId}/move
Move a scene to another chapter
To a 1-based position in another chapter of the same project (the same chapter reorders it). The composition and length travel; the narration stays in the source chapter. In a narrated chapter the scene is placed with a non-spoken direction line holding its title. Free.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
chapter_idrequired
string
position
integer
dry_run
boolean
Response — 200
Field
Type
Description
datarequired
object
200 Dry run.201 Moved.400 Invalid body, or the verb cannot apply (the message says why).401 Missing or invalid API key.404 Scene or chapter not found or not accessible.
PUT/scenes/{sceneId}/frame
Set a scene's background and/or canvas
background: {type:"solid", color} | {media_url, fit: cover|contain, is_video?} | {type:"stage"} (drop it; the theme stage shows). canvas: "WxH" — this scene's own frame. Written into the scene and recompiled; pass expected_version (composition_version) for a compare-and-set. Free.
Parameters
Name
In
Description
sceneIdrequired
path
Request body
Field
Type
Description
background
object
canvas
string
expected_version
integer
Response — 200
Field
Type
Description
datarequired
object
200 Written.400 Invalid body, or the verb cannot apply (the message says why).401 Missing or invalid API key.404 Scene or chapter not found or not accessible.409 The scene changed since expected_version.422 The edited scene did not compile.
GET/scenes/{sceneId}/composition/source
Read the composition a scene mounts
The HTML text of the hosted composition the scene mounts (resolved:"…"), with its url and size. Free.
Parameters
Name
In
Description
sceneIdrequired
path
element_id
query
Response — 200
Field
Type
Description
datarequired
object
200 The composition.400 Invalid body, or the verb cannot apply (the message says why).401 Missing or invalid API key.404 Scene or chapter not found or not accessible.
POST/chains
Run a chain of tool calls
Several tool calls in ONE request, run server-side: each step's result feeds later steps by $ref (generations are awaited, so "$img.url" is the finished picture), `each` fans a step out over an array result, and spend is bounded by `credit_cap` (required when any step spends: every spending step is priced with dry_run first and refused if the total would pass the cap). Stops at the first failed step. `recipe` + `inputs` runs a saved recipe. The chain outlives the request: this waits up to `wait_sec` (default 50) and answers 200 with the finished chain or 202 with the chain so far.
Request body
Field
Type
Description
steps
object[]
Ordered steps. Give steps OR recipe.
recipe
string
A saved recipe name (GET /recipes).
inputs
object
Values for $inputs.<key>; with a recipe, the content its steps need (prompt, project_id; "s2": {…} for one step).
credit_cap
integer
wait_sec
number
surface
"house" | "studio"
Which MCP profile's version of an op name the steps mean when both define it. Default house.
Response — 200
Field
Type
Description
datarequired
object
200 The chain finished (completed or failed).202 Still running; follow with GET /chains/{id}.400 Invalid chain (unknown op, bad ref, spend with no credit_cap…).401 Missing or invalid API key.404 Recipe not found.
GET/chains/{id}
Read a chain
Per-step status, compact results and credits (quoted, charged from the ledger, cap); failed_step and error when it stopped. `wait` (≤ 55 s) holds until it finishes. Reading a stalled chain restarts it. Free.
Parameters
Name
In
Description
idrequired
path
wait
query
Response — 200
Field
Type
Description
datarequired
object
200 The chain.401 Missing or invalid API key.404 No chain with that id on this account.
Prefer a conversation?
The same capabilities are coming to agents over MCP.