Developers

Typecut API reference

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

NameInDescription
cursorquery
limitquery

Response — 200

FieldTypeDescription
datarequiredProject[]
next_cursorrequiredstring | 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
FieldTypeDescription
moderequired"templates"Creation mode "templates" runs the full plan → scaffold → generate pipeline from a template + a topic (prompt) or your own script.
templateSlugrequiredstringTemplate to generate from (its scene vocabulary + pacing). Required.
workspaceIdstring | nullWorkspace to place the project in. Required when the account has multiple workspaces (else a 422 lists the candidates); omitted for workspace-scoped keys.
promptstringA topic/brief — the model writes the script. Provide this OR `script`.
sourcesobject[]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.
scriptstringYour 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.
namestringProject name. Defaults to the script `# H1`, else the topic, else the template.
usePresetsbooleanFill unset fields from the workspace presets (voice, captions, etc.). Default true.
bulkGeneratebooleanRun generation (TTS + scenes) after scaffolding. Default true. False = plan only.
ttsPresetIdstring | nullVoice (tts_presets id). Required unless the workspace has a default voice.
durationSecondsintegerTarget 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.
musicTrackIdstring | nullOptional background music (music_tracks id; see GET /v1/catalogs/music).
audioobjectAudio 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`.
logoobjectChannel 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
FieldTypeDescription
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.
namestringProject name. Defaults to "Untitled Project".
workspaceIdstring | nullWorkspace 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".
ttsPresetIdstring | nullOptional default TTS preset for the project.

Response — 201

FieldTypeDescription
datarequiredobject

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

NameInDescription
cursorquery
limitquery
project_idquery

Response — 200

FieldTypeDescription
datarequiredMediaAsset[]
next_cursorrequiredstring | 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

FieldTypeDescription
urlrequiredstringAbsolute http(s) URL of an already-hosted asset.
content_typerequiredstringMIME type, e.g. image/png.
category"upload" | "archival"
file_namestring
project_idstring | null
source_urlstring | null
widthinteger | null
heightinteger | null

Response — 201

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
urlrequiredstringFull-resolution external asset url (not a thumbnail).
project_idstring | null
file_namestring | null
providerstring | nullWhere it came from, recorded as provenance.
source_urlstring | null
widthinteger | null
heightinteger | null
duration_secnumber | null

Response — 201

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
file_namerequiredstringThe 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).
bytesintegerImages and video: the file size in bytes. Required for video; above 100 MB the upload goes in 64 MB parts.
project_idstring | nullImages and video: file the asset under this project. A project (CLI) key files into its own project.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
tokenrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
tokenrequiredpath

Response — 200

Variant 1 — CompletedMediaUploadResponse
FieldTypeDescription
datarequiredobject
Variant 2 — ReceivedUploadResponse
FieldTypeDescription
datarequiredobject

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

NameInDescription
tokenrequiredpath
nrequiredpath

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
ttl_daysinteger
labelstring

Response — 201

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
keyIdrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
from_secnumber
to_secnumber
bounceboolean
mask_dbnumber
outlier_dbnumber

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
urlrequiredstringA public video link (X, TikTok, Reddit, Instagram, a direct file) or a storage URL of a file already uploaded. Not YouTube.
project_idstring | null

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
source_idquery
from_secquery
to_secquery
limitquery
include_textquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
decisionquery
kindquery
originquery
limitquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
itemsrequiredobject[]

Response — 201

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
changesrequiredobject[]

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
mediaIdrequiredpath

Request body

FieldTypeDescription
project_idstring | nullWhich project the transcript grounds. Defaults to the asset's own project.
role"subject" | "supporting" | "style"

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
media_idstring | nullA library audio/video asset to clone from. Exactly one of media_id / upload_url.
upload_urlstring | nullAn http(s) url to import into the library first, then clone from.
namerequiredstringWhat the voice is called in pickers.
languagestring | nullLanguage of the recording ('en', 'fr-fr'). Omit to use the language detected from the audio. A clone speaks with the accent of its reference.
consentrequiredobjectREQUIRED, 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

FieldTypeDescription
datarequiredobject

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

NameInDescription
cloneIdrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
take_media_idrequiredstringThe recorded take: a library audio or video asset (up to 60 s). No voice clone is needed.
voice_idstring | nullA voice from GET /voices. Exactly one of voice_id / clone_id.
clone_idstring | nullOne of your voice clones (POST /voices/clones).
modestring | null'tags' (default): the take's delivery written as expression tags. 'convert' is not available yet.
line_hintstring | nullWhat you are after, in a few words. Passed to the tag writer.
project_idstring | null
workspace_idstring | null
dry_runboolean | nullQuote only: spends nothing, returns the credits.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
performanceIdrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
urlstring | nullA 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_idstring | nullA library video (media_assets id).
max_shotsinteger | nullKeep at most this many shots; the strongest cuts win. Default 120.
sheet_colsinteger | nullColumns per contact sheet (default 4). Each sheet holds up to 16 shots.
thresholdnumber | nullHard-cut sensitivity (ffmpeg scene score, default 0.3). Dark-scene cuts and dips to black are detected regardless.
transcribeboolean | nullTranscribe the audio (default true).
summarizeboolean | nullOne cheap vision-model pass over the contact sheets (default false).
soundboolean | nullAnalyse the sound: speech/music/singing when, sound effects, the music reading when there is music (default true).
classifyboolean | nullClassify every shot: kind, framing, on-screen text, graphics, voice-over (default true).
blueprintboolean | nullAlso 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'.
forceboolean | nullRun again even when this source was already analysed with these options.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
analysisIdrequiredpath
wordsquery
t0query
t1query
detailquery
fpsquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
analysisIdrequiredpath
countquery
min_secquery
max_secquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
analysisIdrequiredpath
trequiredquery
widthquery
cropquery

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
media_asset_idstring | nullAn uploaded audio or video asset. Exactly one of media_asset_id / track_id.
track_idstring | nullA music track (library or the account's own).
forceboolean | nullRun again even if this source was already analysed (charged again).

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
analysisIdrequiredpath
eventsquery
t0query
t1query

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
voicesrequiredobject[]Up to 4 voices; each has exactly one of voice_id / clone_id.
textstring | nullThe line to speak (≤400 chars). Omit for a short natural sample in `language` (or each voice's own language).
languagestring | nullLanguage to speak, e.g. 'fr' or 'en-gb'.

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
voice_idstring | nulltts_preset id (from list voices).
clone_idstring | nullVoice clone id.
textrequiredstringThe line to speak.
languagestring | null
workspace_idstring | nullWorkspace to file the asset under.

Response — 201

FieldTypeDescription
datarequiredSpokenLine

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

FieldTypeDescription
project_idrequiredstring
promptstringWhat to make, or what to change about the reference(s). Give this or prompts.
promptsstring[]A batch: one image per prompt in one job (≤10), priced per image. Use instead of prompt.
reference_image_urlsstring[]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.
aspect"1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3"
modelstringA 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.
seedintegerProvider seed for a repeatable take; batch item i uses seed + i.
experimentalbooleanSTAFF 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".
dry_runbooleanQuote only: returns {dry_run, credits, images, available}; charges nothing.
waitbooleanHold the request open up to ~50s for the result (default true). false returns 202 immediately.

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
project_idrequiredstring
promptrequiredstringThe motion and scene.
asset_idstringFirst frame: a library image. Or image_url. Neither = text-to-video.
image_urlstring
durationnumberSeconds (default 5; rounded up to whole seconds).
aspect"16:9" | "9:16" | "1:1"Text-to-video only.
modelstringAn allow-listed video model id (default prunaai:p-video@0).
seedinteger
audiobooleanGenerate a soundtrack (models that make one).
dry_runboolean
waitboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
project_idrequiredstring
face_asset_idstringThe face: a library image or video. Or face_url.
face_urlstringThe face: a public https image or video url.
audio_asset_idstringThe voice: a library audio asset (a speak result). Or audio_url.
audio_urlstringThe voice: a public https audio url; then audio_seconds is required.
audio_secondsnumberLength of the voice, up to 60 s. Required with audio_url; prices the clip.
modelstringA talking-head model id from the allow-list (replicate:…).
seedinteger
dry_runboolean
waitboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
project_idrequiredstring
asset_idstringA library image. Pass this OR url.
urlstring
factornumber | numberDefault 2.
modelstringOne upscaler id, e.g. runware:501@1 (GET /catalogs/models?modality=upscale). Omit for the chain.
dry_runboolean
waitboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
project_idrequiredstring
moderequired"panels" | "items" | "sprite"
spriteobjectmode `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.
panelsobject
itemsobject[]
background"transparent" | "white"
modelstringA generate_image model (GET /catalogs/models?tool=generate_image).
reference_image_urlsstring[]
seedinteger
upscaleobject
dry_runboolean
waitboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
project_idrequiredstring
asset_idstringA library image. Pass this OR url.
urlstringA public https image url. Pass this OR asset_id.
partsstring | object[]Named parts ("tail" or {name, hint}). Required for segment and plate; cutout defaults to the main subject; depth returns each part z.
bandsintegerdepth: parallax bands (default 3).
dilatenumberplate: grow the hole by this fraction of the long side (default 0.016).
cropbooleancutout: trim to the part box (default full frame).
dry_runbooleanQuote only; charges nothing.
waitbooleanHold up to ~50s for the result (default true).

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
project_idrequiredstring
asset_idstringA library image. Pass this OR url.
urlstringA public https image url. Pass this OR asset_id.
partsstring | object[]Named parts ("tail" or {name, hint}). Required for segment and plate; cutout defaults to the main subject; depth returns each part z.
bandsintegerdepth: parallax bands (default 3).
dilatenumberplate: grow the hole by this fraction of the long side (default 0.016).
cropbooleancutout: trim to the part box (default full frame).
dry_runbooleanQuote only; charges nothing.
waitbooleanHold up to ~50s for the result (default true).

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
project_idrequiredstring
asset_idstringA library image. Pass this OR url.
urlstringA public https image url. Pass this OR asset_id.
partsstring | object[]Named parts ("tail" or {name, hint}). Required for segment and plate; cutout defaults to the main subject; depth returns each part z.
bandsintegerdepth: parallax bands (default 3).
dilatenumberplate: grow the hole by this fraction of the long side (default 0.016).
cropbooleancutout: trim to the part box (default full frame).
dry_runbooleanQuote only; charges nothing.
waitbooleanHold up to ~50s for the result (default true).

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
project_idrequiredstring
asset_idstringA library image. Pass this OR url.
urlstringA public https image url. Pass this OR asset_id.
partsstring | object[]Named parts ("tail" or {name, hint}). Required for segment and plate; cutout defaults to the main subject; depth returns each part z.
bandsintegerdepth: parallax bands (default 3).
dilatenumberplate: grow the hole by this fraction of the long side (default 0.016).
cropbooleancutout: trim to the part box (default full frame).
dry_runbooleanQuote only; charges nothing.
waitbooleanHold up to ~50s for the result (default true).

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
waitquery

Response — 200

FieldTypeDescription
datarequiredGeneration

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredCancelGeneration

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredProjectA 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

NameInDescription
idrequiredpath
includequery

Response — 200

FieldTypeDescription
dataobject

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

NameInDescription
cursorquery
limitquery

Response — 200

FieldTypeDescription
datarequiredWorkspace[]
next_cursorrequiredstring | 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

FieldTypeDescription
namerequiredstring
descriptionstring | null
languagestring
aspectRatio"16:9" | "9:16" | "4:3" | "1:1"
contentContextstring | null

Response — 201

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredWorkspaceA 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

FieldTypeDescription
queryrequiredstringSubject to search — a topic, person, place, event, or object.
kind"image" | "video"Media kind. Defaults to "image" (entity-specific video is sparse).
limitintegerMax results to return. Defaults to 24.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
renderMode"canvas" | "hyperframe"Render backend. Defaults to hyperframe.

Response — 202

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
countintegerHow many options, each a different concept. Default 1.
promptstringDirection for the art director.
reference_urlstringA 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

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
urlrequiredstringA candidate url, or a file in your library.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
scope"project" | "chapter" | "block"What to resolve. `chapter` requires `chapterId`; `block` requires `blockId`.
chapterIdstring
blockIdstring
dryRunbooleanWhen true, returns the credit quote without firing any generation.
includeStalebooleanAlso regenerate stale voice / scenes (inputs changed since generation).
voiceOnlybooleanVoiceover and word timings only — no scene is composed, quoted or billed. For callers that author their own HTML.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
catalogrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject[]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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
media_urlrequiredstringPublic URL of the image or video to use as the scene background.
is_videobooleanSet true when media_url points to a video.
thumbnail_urlstring | nullOptional pre-generated thumbnail written to the scene chip.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
slotquery

Response — 200

FieldTypeDescription
datarequiredobject[]

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

FieldTypeDescription
slot"bug" | "caption"Default "bug".
slugstringbug only — a caption formula mints its id from the title.
titlerequiredstring
hintstring
specrequiredobject
tiers"raster" | "svg" | "lockup" | "template"[]
status"held" | "active" | "disabled"

Response — 201

FieldTypeDescription
datarequiredobject

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

NameInDescription
slugrequiredpath

Request body

FieldTypeDescription
slot"bug" | "caption"Default "bug".
slugstringbug only — a caption formula mints its id from the title.
titlerequiredstring
hintstring
specrequiredobject
tiers"raster" | "svg" | "lockup" | "template"[]
status"held" | "active" | "disabled"

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
slugrequiredpath

Request body

FieldTypeDescription
statusrequired"held" | "active" | "disabled"

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
cuesrequiredobject[]The complete cue list for this scene. [] clears them.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
enabledbooleanTurn the mark on or off without touching the image.
urlstring | nullLogo image URL. null clears it, which also takes the mark off. Supplying one for an unbranded project turns the mark on.
position"top-left" | "top-right" | "bottom-left" | "bottom-right"
scalenumberMark width as a fraction of frame width.
opacitynumberA motion formula treats this as its ceiling rather than overriding it.
formulastringMotion formula: none, corner, pop, breather, tick, ghost.
expected_versionintegerThe 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

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
transitionstringTransition slug from GET /catalogs/transitions, or "none" for a hard cut. Omit to change only the sound.
duration_msintegerSeam length. Omit to take the transition's own registry default.
lengthstringSeam 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"
soundstringA 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"
leadnumber | stringHow long a build runs before the cut: seconds, or musical ('1 bar'). Default 1 bar on a tempo, else 1.6 s.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
renderquery
scene_idquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
add_presetsstring[]Copy presets into the library: glass-acid, grain-garage, pluck-bells, stutter-house.
upsertobject[]
removestring[]Library ids to delete; the default and scene picks that use them are cleared.
default_idstring | nullThe groove every scene without an override plays; a preset id is copied in on first use.
scenesobject[]
gain_dbnumber
enabledboolean
expected_versionintegerThe 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

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
bpmnumber
meterinteger | objectA bare number means n/4.
keyScalestring | null
originSecnumber
sectionsobject[]Tempo/meter/key changes from a point onward. Replaces the list when sent; [] clears it.
source"user" | "track" | "film" | "default"
expected_versionintegerThe 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

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
conductorrequiredobjectThe conductor document (see GET).
dry_runboolean
ripple"ripple" | "hold"
expected_versionintegerThe 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

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
opsrequiredConductorOp[]
dry_runbooleanValidate 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_versionintegerThe 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

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
kindrequired"motion-design" | "image" | "text"
promptrequiredstringCreative prompt for motion-design/image, or the literal copy for text.
at_secondsnumber
areaobject
reference_scene_idstringmotion-design only: author the element to MATCH that resolved scene's design.
reference_image_urlstringmotion-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

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
patchesrequiredobject[]
expect_linesobject[]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_versionintegerOptimistic 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

FieldTypeDescription
successrequiredtrue
datarequiredobject

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

FieldTypeDescription
namerequiredstringDisplay name for the theme.
colorsrequiredobjectPalette keyed by semantic role. bg, ink and accent are required; the rest optional.
fontsobjectType 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

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath
element_idqueryDescribe only the composition with this element id (the id returned in `element_id`, derived from the blob's composition id).

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
element_idrequiredstringThe Composition element id (as returned by GET …/genhtml).
opsrequiredobject[]

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath
element_idqueryNarrow to one composition element. Omit for every gen:html in the scene.
detailqueryScene packet detail. compact (default) trims the project layer and caps words to the element window; full carries everything.
kindqueryForce a composition kind instead of the inferred one.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
rolequeryNarrow the records returned.
kernelqueryRestrict to one kernel module, e.g. "tc-text".

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
htmlrequiredstringThe composition source to check.
widthnumberCanvas width it is authored against. Default 1920.
heightnumberCanvas height. Default 1080.
durationnumberScene duration in seconds. Default 10.
element_idstringElement id it mounts as. Default "comp".
contract_onlybooleanRun only the vocabulary checks, skipping the gate stack.
checkbooleanAlso 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.
briefstringWhat the scene is meant to say; sharpens the check.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
timesrequirednumber[]Scene-clock seconds to inspect (max 64).
element_idstringLimit to one composition element.
widthnumberWidth the glyph-size check scales the frame to. Default 360 (a phone).
stepnumberVisibility-timeline sample step in seconds. Default 0.5; 0 disables it.

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
htmlrequiredstringThe composition source to inspect.
timesrequirednumber[]Seconds on the composition clock (max 64).
widthnumberWidth the glyph-size check scales the frame to. Default 360.
stepnumberVisibility-timeline sample step in seconds. Default 0.5; 0 disables it.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
timesrequirednumber[]Scene-clock seconds to render (max 24).
element_idstringLimit the static findings to one composition element.
determinismbooleanRun the render-twice determinism check. Default true.
determinism_timesnumber[]Times the determinism check uses. Default: up to 3 spread over times.
compare_mp4booleanAlso diff each frame against the scene's cached MP4 at the same frame (PSNR per time, and whether that MP4 is current).

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
htmlrequiredstringThe composition source to preview.
timesrequirednumber[]Seconds to render (max 24).
scene_idstringPreview the draft inside this scene, replacing its composition element.
element_idstringWhich composition element the draft replaces (scene form).
widthnumberCanvas width (bare draft). Default 1920.
heightnumberCanvas height (bare draft). Default 1080.
durationnumberDuration in seconds (bare draft). Default 10.
determinismbooleanRun the render-twice determinism check. Default true.
determinism_timesnumber[]Times the determinism check uses.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
htmlrequiredstringThe composition source — a full HyperFrame sub-comp document.
element_idstringWhich composition element. Omit only when the scene has exactly one; a scene with several is refused rather than guessed at.
dry_runbooleanValidate and report without hosting or writing anything. Free.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
labelstringWhat the lock reads, e.g. "Claude · scene 4 pass".
ttl_secondsinteger

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
ttl_secondsinteger

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
actor_kindquery
actorquery
agent_run_idquery
scene_idquery
chapter_idquery
kindquery
sincequery
untilquery
group_idquery
limitquery
cursorquery

Response — 200

FieldTypeDescription
datarequiredProjectHistoryPage

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

NameInDescription
idrequiredpath
event_idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
minutesquery

Response — 200

FieldTypeDescription
datarequiredobject

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.

Parameters

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
mode"bed" | "off"
duckobjectduck_db, attack_sec, release_sec, bridge_sec, carve_db; null = default.
ducksobject[]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).
roomobject | nullAux 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.
momentsobject[]Replace ALL moments ([] clears).
add_momentsobject[]
remove_momentsinteger[]
clear_scenesstring[]
expected_versionintegerThe 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

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
profileobject | null
from_project_idstring

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
trackIdstring | null
urlstring | null
sequencestring[]
generateobject
volumenumber
fadeInnumber
fadeOutnumber
loopboolean
startFromnumber
endAtnumber
expected_versionintegerThe 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

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
voice_idrequiredstring
expected_versionintegerThe 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

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
modulesrequiredarray | null
chapter_idstring
expected_versionintegerThe 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

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
analysis_idstring | null
audio_analysis_idstring | null
alignmentarray | null
notestring | null
expected_versionintegerThe 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

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
trequiredquery
widthquery
cropquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
fromquery
toquery
scene_idquery
wordsquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
textquery
kindquery
framingquery
similar_toquery
measurequery
scopequery
limitquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
scene_idrequiredstring
samplesintegerFrames to compare (default 5).
quickbooleanDraft-quality scene frames (default true).
detailboolean | arrayAlso 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_urlstringCompare the scene with this one picture instead of the reference (no alignment needed).
atnumberWith against_image_url: scene time in seconds to take the frame at (default: the scene evenly).

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
datarequiredobject[]

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

FieldTypeDescription
emailrequiredstringLowercased server-side.
rolerequired"maintainer" | "editor" | "reviewer" | "viewer"

Response — 201

FieldTypeDescription
datarequiredobject

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).

Parameters

NameInDescription
user_idrequiredpath

Request body

FieldTypeDescription
rolerequired"owner" | "maintainer" | "editor" | "reviewer" | "viewer"

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
user_idrequiredpath

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

FieldTypeDescription
datarequiredobject[]

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

FieldTypeDescription
emailrequiredstringLowercased server-side.
rolerequired"maintainer" | "editor" | "reviewer" | "viewer"

Response — 201

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
chapter_idrequiredstring
block_idstring | null
anchor_textstring | null
titlestring | null
promptstring | null
durationnumber | null
media_urlstring | null
is_videoboolean
thumbnail_urlstring | null
positioninteger | null
reference_image_urlstring | nullDraft scenes only: a picture the author should look at (a library asset url). Keep `prompt` to intent rather than describing the picture.
narrationstring | nullPlace 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

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Response — 200

FieldTypeDescription
datarequiredScene

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
titlestring | null
promptstring | null
durationnumber | null
captions_enabledboolean | null
caption_style_idstring | null
block_idstring | null

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
confirmrequired"DELETE"

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Response — 200

FieldTypeDescription
dataobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
resolutionstring

Response — 202

FieldTypeDescription
dataobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
dataobject

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

NameInDescription
templateSlugrequiredquery
workspaceIdquery
usePresetsquery

Response — 200

FieldTypeDescription
dataobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
titlestring | null
positioninteger | null

Response — 201

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
partialboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
chapterIdrequiredpath

Request body

FieldTypeDescription
type"narration" | "direction" | "note" | "heading"
contentrequiredstring
positioninteger | null

Response — 201

FieldTypeDescription
datarequiredobject

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

NameInDescription
chapterIdrequiredpath
blockIdrequiredpath

Request body

FieldTypeDescription
contentrequiredstring

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
chapterIdrequiredpath
blockIdrequiredpath

Request body

FieldTypeDescription
action"cut" | "restore"
phrasestring
char_frominteger
char_tointeger

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
namestring
kind"fork" | "snapshot"
workspace_idstring

Response — 201

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
chapterIdrequiredpath
blockIdrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
chapterIdrequiredpath
blockIdrequiredpath

Request body

FieldTypeDescription
take_idrequiredstring

Response — 200

FieldTypeDescription
datarequiredobject

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.

GET/chapters/{chapterId}/blocks/{blockId}/silences

Read the silence trims of a script block

The block's silence trims, sorted by position: each moves the natural silence after the word ending at `at_audio_ms`. Free.

Parameters

NameInDescription
chapterIdrequiredpath
blockIdrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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.

PUT/chapters/{chapterId}/blocks/{blockId}/silences

Tighten or stretch one natural silence

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

NameInDescription
chapterIdrequiredpath
blockIdrequiredpath

Request body

FieldTypeDescription
at_audio_msrequirednumber
delta_msrequirednumber

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
labelstring
notestring

Response — 201

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
take_idrequiredstring

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath
arequiredqueryA take id, or "current".
bqueryA take id, or "current" (default).

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
chapterIdrequiredpath
blockIdrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
chapterIdrequiredpath
blockIdrequiredpath

Request body

FieldTypeDescription
pausesrequiredobject[]
mode"replace" | "merge"

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
chapterIdrequiredpath

Request body

FieldTypeDescription
scene_idsrequiredstring[]

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredProjectTheme

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
slugstring
expected_versionintegerThe 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.
restyleobject

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredProjectCaptions

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
enabledboolean
styleIdstring | null
colorScheme"default" | "theme"
alignmentstring | null
fontSizenumber | null
styleobject | null
expected_versionintegerThe 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

FieldTypeDescription
datarequiredProjectCaptions

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

FieldTypeDescription
datarequiredobject[]

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

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
operationsrequiredobject[]

Response — 201

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
quote_idstring
creditsinteger
idempotency_keystring
ttl_secondsinteger
reasonstring

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
caprequiredinteger
labelstring
expires_atstring
parent_idstring

Response — 201

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
formatquery
downloadquery

Response — 200

FieldTypeDescription
filenamerequiredstring
contentTyperequiredstring
contentrequiredstringThe 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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
versionnumber | numberPackage version (default 1).
flavour"premiere" | "finalcut" | "resolve"Version 1 target app (default premiere). Ignored by version 2, which serves all three.

Response — 202

FieldTypeDescription
generation_idrequiredstringPoll GET /generations/{id}; `output_url` is the zip when `completed`.
unrenderedrequiredobject[]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

FieldTypeDescription
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`.
bodystringWhat went wrong, in the reporter's own words. One sentence is enough.
titlestringOverride the generated title. Normally omitted — the body's first line is used.
project_idstring | nullThe 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"
focusobjectWhere the reporter was standing when it broke.
client_errorsobject[]
appobject
replay_urlstring | nullSession-replay URL, when one exists.

Response — 201

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
statusquery
scene_idquery
kindquery
awaitingquery
limitquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
bodyrequiredstring
scene_idstring
t_startnumber
t_endnumber
element_idstring
composition_pathstring
anchorsobject[]
parent_idstring
kind"note" | "question" | "blocker"
tostring

Response — 201

FieldTypeDescription
datarequiredComment

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
notesrequiredobject[]

Response — 201

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
commentIdrequiredpath

Request body

FieldTypeDescription
changeset_idstring
revision_idstring
notestring
reopenboolean

Response — 200

FieldTypeDescription
datarequiredComment

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

NameInDescription
idrequiredpath
scene_idquery
limitquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
stepsinteger
revision_idstring

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
verdictrequired"accept" | "reject"
reasonstring
comment_idstring

Response — 201

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
wordsquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
fromquery
toquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
chapter_idquery
detailquery
fromquery
toquery
wordsquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
scene_idsstring[]Only these scenes (and the sound under them). Default every placed scene (max 40).
tolerance_msnumberHow far apart still counts as together. Default 40.
onsetsobject[]Extra onsets on the film clock.
detailbooleanAlso list every matched pair.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
formatquery
scenesqueryComma-separated scene ids (at most 20): just those documents, inline and stamped as the folder holds them.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
cursorquery
waitquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
manifestrequiredobject{v:1, initial, inputs, states, events, slots}
namestring
dry_runboolean
with_scene_audiobooleanAdd the scene's own voice and music as manifest.audio tracks (when the manifest has none).

Response — 200

FieldTypeDescription
datarequiredobject

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.

Parameters

NameInDescription
chapterIdrequiredpath

Request body

FieldTypeDescription
manifestobject{v:1, sequence:{loop, autoplay, controls}, audio:{tracks}}
namestring
dry_runboolean
with_audiobooleanAdd the chapter's voice and the project's music as audio tracks (default true; ignored when the manifest has audio).

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
filesrequiredobject[]
dry_runboolean
forceboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
htmlstring
src_urlstring
base_urlstring
workspace_idstring
namestring
project_idstring
chapter_idstring
chapter_titlestring
cutsnumber[]
duration_secnumber
min_scene_secnumber
max_scene_secnumber
audio"music" | "voice" | "none"
shot_detection"auto" | "off" | "force"
subcompsboolean
video_urlstring
verifyboolean
captionsboolean
dry_runboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
dry_runboolean
min_psnrnumber
modelstring
retryboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
linesrequiredobject[]
voice_idstring
clone_idstring
languagestring
workspace_idstring

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
scene_idsrequiredstring[]
detail"compact" | "full"

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
scenesrequiredobject[]
dry_runboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
scenesrequiredobject[]
timesnumber[]
widthinteger

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
generation_idsstring[]
project_idsstring[]

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
partsquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
biblerequiredBible
base_versionrequiredinteger

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
pathquery

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
filesrequiredobjectRepo-relative path to file text, as GET /projects/{id}/repo-tree returns it.
workspace_idstring | nullDefaults to the key's own workspace, if it has one.
dry_runboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
pathstring
pathsstring[]

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
pathrequiredstring
contentstring | object.json: an object (or its text), merged. Others: the whole text. Send this or `edit`.
editobjectmusic/ 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_runboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
scoreobject
codestring
sourcestring
editobject
planboolean
statusstringA render key (32 hex) or a plan key (pl_ + 32 hex).
asyncboolean
triggersinteger
referencestring
dry_runboolean
rangeobject
placeobject
applyboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
fromrequiredstring
torequiredstring
dry_runboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath

Request body

FieldTypeDescription
pathrequiredstring
dry_runboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
atrequirednumber | string
dry_runboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
scene_idsrequiredstring[]
dry_runboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
from_projectrequiredstring
scene_idsstring[]
chapter_idstring
to_projectrequiredstring
to_chapter_idstring
positioninteger
include_soundboolean
dry_runboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
chapter_idrequiredstring
positioninteger
dry_runboolean

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath

Request body

FieldTypeDescription
backgroundobject
canvasstring
expected_versioninteger

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
sceneIdrequiredpath
element_idquery

Response — 200

FieldTypeDescription
datarequiredobject

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

FieldTypeDescription
stepsobject[]Ordered steps. Give steps OR recipe.
recipestringA saved recipe name (GET /recipes).
inputsobjectValues for $inputs.<key>; with a recipe, the content its steps need (prompt, project_id; "s2": {…} for one step).
credit_capinteger
wait_secnumber
surface"house" | "studio"Which MCP profile's version of an op name the steps mean when both define it. Default house.

Response — 200

FieldTypeDescription
datarequiredobject

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

NameInDescription
idrequiredpath
waitquery

Response — 200

FieldTypeDescription
datarequiredobject

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.

Typecut MCP