{"openapi":"3.1.0","info":{"title":"Typecut API","version":"v1","description":"HTTP API for managing workspaces, projects, scenes, and renders programmatically — same surface area the editor UI uses."},"servers":[{"url":"https://app.typecut.ai/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"tc_<env>_<random>","description":"Typecut API key. Generate one in app.typecut.ai → Settings → API keys. Send as `Authorization: Bearer tc_live_…`."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"Coarse error category, e.g. \"authentication_error\"."},"code":{"type":"string","description":"Stable machine-readable error code."},"message":{"type":"string","description":"Human-readable explanation."}},"required":["type","code","message"]}},"required":["error"],"description":"Standard error envelope. All non-2xx responses use this shape."},"Project":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"workspace_id":{"type":["string","null"],"format":"uuid"},"status":{"type":"string","enum":["draft","planning","generating","review","ready","rendering","rendered","failed"],"description":"Project lifecycle. \"draft\" covers both blank quick-create projects and plan-done-no-bulk projects (the FE distinguishes by chapter count)."},"type":{"type":["string","null"]},"target_duration_seconds":{"type":["integer","null"]},"scene_density":{"type":["string","null"]},"thumbnail_url":{"type":["string","null"]},"aspect_ratio":{"type":["string","null"]},"duration":{"type":"integer"},"total_scenes":{"type":"integer"},"rendered_scenes":{"type":"integer"},"failed_scenes":{"type":"integer"},"render_url":{"type":["string","null"]},"settings":{"type":"object","additionalProperties":{},"description":"Wizard generation spec (only returned by the detail endpoint)."},"global_style":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","name","description","workspace_id","status","type","target_duration_seconds","scene_density","thumbnail_url","aspect_ratio","duration","total_scenes","rendered_scenes","failed_scenes","render_url","created_at","updated_at"],"description":"A video project. Belongs to one workspace (or workspace_id null). Status drives the FE lifecycle."},"ProjectListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Project"}},"next_cursor":{"type":["string","null"]}},"required":["data","next_cursor"]},"ProjectDetailResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Project"}},"required":["data"]},"Workspace":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"topic":{"type":["string","null"]},"language":{"type":"string"},"profile_image_url":{"type":["string","null"]},"default_aspect_ratio":{"type":["string","null"]},"default_tts_preset_id":{"type":["string","null"],"format":"uuid"},"default_visual_style_id":{"type":["string","null"],"format":"uuid"},"default_generation_tier":{"type":["string","null"]},"default_transitions":{"type":["array","null"],"items":{"type":"string"}},"default_transition_duration":{"type":["integer","null"]},"default_caption_style_id":{"type":["string","null"]},"default_caption_position":{"type":["string","null"],"enum":["top","middle","bottom"]},"default_caption_font_size":{"type":["integer","null"]},"editorial_guidelines":{"type":["object","null"],"additionalProperties":{}},"content_context":{"type":["string","null"]},"branding":{"type":["object","null"],"additionalProperties":{}},"credit_balance":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","name","description","topic","language","profile_image_url","default_aspect_ratio","default_tts_preset_id","default_visual_style_id","default_generation_tier","default_transitions","default_transition_duration","default_caption_style_id","default_caption_position","default_caption_font_size","editorial_guidelines","content_context","branding","credit_balance","created_at","updated_at"],"description":"A workspace — per-show preset container. Holds defaults (voice, visual style, transitions, captions), brand assets, and editorial context. Projects belong to workspaces. (The underlying table is `channels`.)"},"WorkspaceListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Workspace"}},"next_cursor":{"type":["string","null"]}},"required":["data","next_cursor"]},"WorkspaceDetailResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Workspace"}},"required":["data"]},"CreateBlankProjectRequest":{"type":"object","properties":{"mode":{"type":"string","enum":["blank"],"description":"Creation mode. \"blank\" creates an empty project with one chapter, no LLM planning. Additional modes (e.g. \"prompt\") will be added in a future release."},"name":{"type":"string","minLength":1,"maxLength":200,"description":"Project name. Defaults to \"Untitled Project\"."},"workspaceId":{"type":["string","null"],"format":"uuid","description":"Workspace to place the project in. Required for workspace-scoped API keys (may be omitted — the scoped workspace is used)."},"aspectRatio":{"type":"string","enum":["16:9","9:16","4:3","1:1"],"description":"Project aspect ratio. Defaults to \"16:9\"."},"ttsPresetId":{"type":["string","null"],"format":"uuid","description":"Optional default TTS preset for the project."}},"required":["mode"],"description":"Body for POST /v1/projects with mode=\"blank\" — an empty project, no LLM planning."},"CreateTemplateProjectRequest":{"type":"object","properties":{"mode":{"type":"string","enum":["templates"],"description":"Creation mode \"templates\" runs the full plan → scaffold → generate pipeline from a template + a topic (prompt) or your own script."},"templateSlug":{"type":"string","minLength":1,"maxLength":200,"description":"Template to generate from (its scene vocabulary + pacing). Required.","example":"narrative"},"workspaceId":{"type":["string","null"],"format":"uuid","description":"Workspace to place the project in. Required when the account has multiple workspaces (else a 422 lists the candidates); omitted for workspace-scoped keys."},"prompt":{"type":"string","minLength":1,"maxLength":20000,"description":"A topic/brief — the model writes the script. Provide this OR `script`."},"script":{"type":"string","minLength":1,"maxLength":100000,"description":"Your own finished narration, used as the source. Provide this OR `prompt`. May be a markdown document (see `scriptFormat`): `# Title`, `## Chapters`, paragraphs become beats. Text is used verbatim (see `scriptFidelity`)."},"scriptFidelity":{"type":"string","enum":["verbatim","adapt"],"description":"How faithfully to keep your `script`. 'verbatim' (segment only, never reword) or 'adapt' (default — the model may restructure/rephrase)."},"scriptFormat":{"type":"string","enum":["markdown","plaintext","auto"],"description":"How to parse `script` into chapters/blocks. 'auto' (default) detects markdown headings (`#`/`##`); 'markdown' forces it; 'plaintext' treats it as flat prose. With markdown headings, `##` define chapters and `#` becomes the project title."},"name":{"type":"string","minLength":1,"maxLength":200,"description":"Project name. Defaults to the script `# H1`, else the topic, else the template."},"usePresets":{"type":"boolean","description":"Fill unset fields from the workspace presets (voice, captions, etc.). Default true."},"bulkGenerate":{"type":"boolean","description":"Run generation (TTS + scenes) after scaffolding. Default true. False = plan only."},"ttsPresetId":{"type":["string","null"],"format":"uuid","description":"Voice (tts_presets id). Required unless the workspace has a default voice."},"durationSeconds":{"type":"integer","minimum":5,"maximum":7200,"description":"Target length (prompt mode). Ignored in verbatim script mode (length is implied)."},"aspectRatio":{"type":"string","enum":["16:9","9:16","4:3","1:1"],"description":"Aspect ratio. Defaults to the workspace preset, else 16:9."},"musicTrackId":{"type":["string","null"],"format":"uuid","description":"Optional background music (music_tracks id; see GET /v1/catalogs/music)."},"audio":{"type":"object","properties":{"vo":{"type":"number","minimum":0,"maximum":2},"music":{"type":"number","minimum":0,"maximum":2},"sfx":{"type":"number","minimum":0,"maximum":2}},"description":"Audio mix masters (0–2), mirroring the editor VO/Music/SFX faders. Each is applied multiplicatively in the render mix. Omit any to keep the editor-neutral default (vo 1, music 0.3, sfx 0.7). Read back via GET /v1/projects/{id} → `audio`."},"logo":{"type":"object","properties":{"enabled":{"type":"boolean"},"url":{"type":"string","format":"uri"},"position":{"type":"string","enum":["top-left","top-right","bottom-left","bottom-right"]},"scale":{"type":"number","minimum":0.02,"maximum":0.5},"opacity":{"type":"number","minimum":0,"maximum":1}},"description":"Channel logo / watermark — a static image pinned to a corner of every scene. `scale` is logo width as a fraction of video width (default 0.12). Omit to inherit the workspace branding preset. Read back via GET /v1/projects/{id} → `logo`."}},"required":["mode","templateSlug"],"description":"Body for POST /v1/projects with mode=\"templates\" — runs the same pipeline as the create wizard. Provide a `prompt` OR a `script`. Call GET /v1/projects/requirements first to discover required inputs and (for a script) preview its parsed structure."},"CreateProjectResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["ready","planning","generating"]},"generationId":{"type":"string","format":"uuid","description":"The generation job tracking this creation (templates mode)."},"editor_url":{"type":"string","format":"uri","description":"Absolute link to the project editor — send the user here.","example":"https://app.typecut.ai/editor/52e088d6-345f-47f5-86af-31b694175f8e"},"credit_cost":{"type":"number","description":"Credits charged upfront for this creation (templates mode)."},"credits_remaining":{"type":"number","description":"Account credit balance after the charge."}},"required":["id","status"]}},"required":["data"],"description":"Project created (status=\"generating\" plans asynchronously — poll GET /v1/projects/{id} for status + duration). Duration is TTS-derived so it is not known at this response."},"DiscoverySearchRequest":{"type":"object","properties":{"query":{"type":"string","minLength":1,"maxLength":200,"description":"Subject to search — a topic, person, place, event, or object."},"kind":{"type":"string","enum":["image","video"],"description":"Media kind. Defaults to \"image\" (entity-specific video is sparse)."},"limit":{"type":"integer","minimum":1,"maximum":48,"description":"Max results to return. Defaults to 24."}},"required":["query"],"description":"Body for POST /v1/discovery/search. Entity-grounded open/archival asset search (Wikimedia Commons, NASA, ...)."},"DiscoverySearchResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"entity":{"type":["object","null"],"properties":{"qid":{"type":"string"},"label":{"type":"string"},"commonsCategory":{"type":["string","null"]}},"required":["qid","label","commonsCategory"],"description":"The resolved Wikidata entity, or null if none matched."},"coverage":{"type":"integer","description":"Count of license-cleared results."},"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"source":{"type":"string","description":"Origin source, e.g. \"wikimedia\", \"nasa\"."},"assetId":{"type":"string"},"type":{"type":"string","enum":["image","video"]},"thumbnailUrl":{"type":"string"},"url":{"type":"string"},"width":{"type":"integer"},"height":{"type":"integer"},"durationSec":{"type":"number"},"attribution":{"type":"string"},"title":{"type":["string","null"]},"author":{"type":["string","null"]},"sourceUrl":{"type":["string","null"]},"license":{"type":"object","properties":{"tier":{"type":"string"},"name":{"type":"string"},"url":{"type":["string","null"]}},"required":["tier","name","url"]}},"required":["id","source","assetId","type","thumbnailUrl","url","width","height","attribution","title","author","sourceUrl","license"]}}},"required":["entity","coverage","results"]}},"required":["data"]},"CreateCompositionRequest":{"type":"object","properties":{"njs":{"type":"string","minLength":1,"maxLength":200000,"description":"A raw NJS scene script. Transpiled server-side; declared gen elements (gen:image / gen:video / gen:stock-image / gen:stock-video / gen:html) resolve asynchronously after the scene is created."},"workspace_id":{"type":["string","null"],"format":"uuid","description":"Workspace (channel) to create the scene in. Required for account-level API keys; workspace-scoped keys may omit it (the scoped workspace is used)."},"settings":{"type":"object","properties":{"fps":{"type":"integer","minimum":1,"maximum":120,"description":"Frames per second. Overrides the NJS `config: fps`."},"aspect_ratio":{"type":"string","enum":["16:9","9:16","4:3","1:1"],"description":"Aspect ratio. Overrides the NJS `config:` canvas."},"duration":{"type":"number","minimum":0.1,"maximum":600,"description":"Scene duration in seconds. Overrides the NJS `config: duration`."},"width":{"type":"integer","minimum":16,"maximum":8192},"height":{"type":"integer","minimum":16,"maximum":8192}},"description":"Optional render settings; each field overrides the matching value in the NJS `config:` block."}},"required":["njs"],"description":"Body for POST /v1/compositions. Submit raw NJS directly — no prompt, no LLM. For agents that already produce NJS."},"CreateCompositionResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The created scene id."},"generation_id":{"type":"string","format":"uuid","description":"Tracking id for the generation."},"share_url":{"type":"string","format":"uri","description":"Public, login-free viewer URL — renders the scene live as gen elements resolve."},"share_token":{"type":"string","description":"The bare share token. Poll GET https://app.typecut.ai/api/share/{token} for status + metadata."},"has_gen_elements":{"type":"boolean","description":"True if the NJS declared gen elements that are resolving asynchronously."},"diagnostics":{"type":"array","items":{"type":"object","properties":{"level":{"type":"string"},"code":{"type":"string"},"message":{"type":"string"},"line":{"type":"number"},"suggestion":{"type":"string"}},"required":["level","message"]},"description":"Transpiler diagnostics (warnings / info), if any."}},"required":["id","generation_id","share_url","share_token","has_gen_elements"]}},"required":["data"]},"AttachMediaRequest":{"type":"object","properties":{"media_url":{"type":"string","format":"uri","description":"Public URL of the image or video to use as the scene background."},"is_video":{"type":"boolean","description":"Set true when media_url points to a video."},"thumbnail_url":{"type":["string","null"],"format":"uri","description":"Optional pre-generated thumbnail written to the scene chip."}},"required":["media_url"]},"AttachMediaResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"scene_id":{"type":"string","format":"uuid"},"background":{"type":"object","properties":{"type":{"type":"string","enum":["image","video"]},"url":{"type":"string","format":"uri"},"fit":{"type":"string","enum":["cover"]},"overflowBehavior":{"type":"string","enum":["loop"]},"muted":{"type":"boolean"},"volume":{"type":"number"}},"required":["type","url","fit"]},"thumbnail_url":{"type":["string","null"]}},"required":["scene_id","background","thumbnail_url"]}},"required":["data"]}},"parameters":{}},"paths":{"/projects":{"get":{"summary":"List projects","description":"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).","operationId":"listProjects","tags":["Projects"],"parameters":[{"schema":{"type":"string","description":"Opaque cursor from a previous response's next_cursor."},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Max items per page."},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"A page of projects.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectListResponse"}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create a project","description":"Create a new project. Mode-discriminated by `mode`:\n- `templates` — runs the full plan → scaffold → generate pipeline from a template + a topic (`prompt`) or your own `script` (markdown-structured supported). The main path.\n- `blank` — an empty project with one chapter, no planning; edit it via later API calls.\n\nFor `templates`, call GET /v1/projects/requirements first to learn the required inputs (and preview a script's parsed structure).","operationId":"createProject","tags":["Projects"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/CreateTemplateProjectRequest"},{"$ref":"#/components/schemas/CreateBlankProjectRequest"}]}}}},"responses":{"201":{"description":"Project created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectResponse"}}}},"400":{"description":"Invalid request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Plan limit reached (free tier project cap, lapsed subscription, etc).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key cannot create projects in the requested workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/projects/{id}":{"get":{"summary":"Get a project","description":"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.","operationId":"getProject","tags":["Projects"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Project found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectDetailResponse"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Project not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/projects/{id}/trace":{"get":{"summary":"Get a project generation trace","description":"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.","operationId":"getProjectTrace","tags":["Projects"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"Comma-separated detail flags: 'raw' and/or 'prompts'. Omit for the compact digest.","example":"raw,prompts"},"required":false,"name":"include","in":"query"}],"responses":{"200":{"description":"The project trace digest.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}}}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Project not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/workspaces":{"get":{"summary":"List workspaces","description":"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. Cursor-paginated.","operationId":"listWorkspaces","tags":["Workspaces"],"parameters":[{"schema":{"type":"string","description":"Opaque cursor from a previous response's next_cursor."},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Max items per page."},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"A page of workspaces.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceListResponse"}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/workspaces/{id}":{"get":{"summary":"Get a workspace","description":"Fetch one workspace by id. Returns 404 (not 403) when the workspace exists but is not accessible by the API key.","operationId":"getWorkspace","tags":["Workspaces"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Workspace found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceDetailResponse"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Workspace not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/discovery/search":{"post":{"summary":"Search open & archival assets","description":"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.","operationId":"searchDiscovery","tags":["Discovery"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscoverySearchRequest"}}}},"responses":{"200":{"description":"Ranked, license-cleared candidates.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscoverySearchResponse"}}}},"400":{"description":"Invalid request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Upstream discovery engine error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/compositions":{"post":{"summary":"Create a scene from raw NJS","description":"Submit a raw NJS scene script directly — no prompt, no LLM. The script is transpiled server-side; any declared gen elements (gen:image / gen:video / gen:stock-image / gen:stock-video / gen:html) resolve asynchronously. Returns the scene id and a public `share_url` that renders the scene live as those elements resolve. Intended for agents that already produce NJS (see the NJS reference at njs.typecut.ai). The public viewer and its polling endpoint (`GET /api/share/{token}`) need no authentication.","operationId":"createComposition","tags":["Compositions"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCompositionRequest"}}}},"responses":{"201":{"description":"Scene created; gen elements (if any) are resolving asynchronously.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCompositionResponse"}}}},"400":{"description":"Invalid request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Plan limit reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key cannot create scenes in the requested workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"NJS failed to transpile — diagnostics are in the error `detail`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/projects/{id}/render":{"post":{"summary":"Render a project to video","description":"Trigger a full project video render. Validates readiness first: every chapter must have TTS audio 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.","operationId":"renderProject","tags":["Renders"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"renderMode":{"type":"string","enum":["canvas","hyperframe"],"description":"Render backend. Defaults to hyperframe."}}}}}},"responses":{"202":{"description":"Render queued.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"project_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["rendering"]},"generation_id":{"type":"string","format":"uuid"},"credits_remaining":{"type":"integer"},"editor_url":{"type":"string","format":"uri"}},"required":["project_id","status","generation_id","credits_remaining","editor_url"]}},"required":["data"]}}}},"402":{"description":"Insufficient credits.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Project not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Project is already rendering.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Project not ready to render — `blockers` lists the chapters missing audio and scenes missing visuals.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Render queue unavailable (credits were refunded).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"summary":"Get render status","description":"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.","operationId":"getRenderStatus","tags":["Renders"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Current render status.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"project_id":{"type":"string","format":"uuid"},"project_status":{"type":"string"},"render_url":{"type":["string","null"],"description":"Final MP4 URL once the render completes."},"editor_url":{"type":"string","format":"uri"},"job":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending","rendering_scenes","stitching","uploading","completed","failed"]},"total_scenes":{"type":"integer"},"rendered_scenes":{"type":"integer"},"cached_scenes":{"type":"integer"},"failed_scenes":{"type":"integer"},"stitch_progress":{"type":["number","null"]},"output_url":{"type":["string","null"]},"output_duration":{"type":["number","null"]},"error_message":{"type":["string","null"]},"started_at":{"type":["string","null"]},"completed_at":{"type":["string","null"]},"created_at":{"type":["string","null"]}},"required":["id","status","total_scenes","rendered_scenes","cached_scenes","failed_scenes","stitch_progress","output_url","output_duration","error_message","started_at","completed_at","created_at"],"description":"Latest render job, or null if the project has never rendered."}},"required":["project_id","project_status","render_url","editor_url","job"]}},"required":["data"]}}}},"404":{"description":"Project not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/projects/{id}/resolution":{"get":{"summary":"Get resolution status","description":"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).","operationId":"getProjectResolution","tags":["Resolution"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Resolution status + cost quote.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"project_id":{"type":"string","format":"uuid"},"editor_url":{"type":"string","format":"uri"},"ready":{"type":"boolean","description":"Nothing left to resolve — every scene resolved and every chapter has voice-over."},"scenes":{"type":"object","properties":{"total":{"type":"integer"},"resolved":{"type":"integer"},"generating":{"type":"integer"},"unresolved":{"type":"integer","description":"Needs generation and can be generated."},"unresolvable":{"type":"integer","description":"Nothing to generate from (no prompt / media source)."}},"required":["total","resolved","generating","unresolved","unresolvable"]},"voiceover":{"type":"object","properties":{"chapters_total":{"type":"integer"},"chapters_missing_audio":{"type":"integer"}},"required":["chapters_total","chapters_missing_audio"]},"cost_estimate":{"type":"object","properties":{"scenes":{"type":"integer"},"voiceover":{"type":"integer"},"total":{"type":"integer"}},"required":["scenes","voiceover","total"],"description":"Estimated credits to resolve the unresolved work. The real charge happens at generation time."},"actionable":{"type":"integer","description":"Unresolved scenes + chapters missing audio."}},"required":["project_id","editor_url","ready","scenes","voiceover","cost_estimate","actionable"]}},"required":["data"]}}}},"404":{"description":"Project not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/projects/{id}/resolve":{"post":{"summary":"Resolve (generate) unresolved work","description":"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).","operationId":"resolveProject","tags":["Resolution"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"string","enum":["project","chapter","block"],"default":"project","description":"What to resolve. `chapter` requires `chapterId`; `block` requires `blockId`."},"chapterId":{"type":"string","format":"uuid"},"blockId":{"type":"string"},"dryRun":{"type":"boolean","description":"When true, returns the credit quote without firing any generation."}}}}}},"responses":{"200":{"description":"Dry-run quote — nothing was fired.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"project_id":{"type":"string","format":"uuid"},"editor_url":{"type":"string","format":"uri"},"dry_run":{"type":"boolean"},"triggered":{"type":"object","properties":{"scenes":{"type":"integer","description":"Scene composition generations fired."},"voiceover_chapters":{"type":"integer","description":"Chapters for which voice-over generation was fired."}},"required":["scenes","voiceover_chapters"]},"skipped_unresolvable":{"type":"integer","description":"Scenes skipped — nothing to generate them from."},"cost_estimate":{"type":"object","properties":{"scenes":{"type":"integer"},"voiceover":{"type":"integer"},"total":{"type":"integer"}},"required":["scenes","voiceover","total"],"description":"Estimated credits to resolve the unresolved work. The real charge happens at generation time."}},"required":["project_id","editor_url","dry_run","triggered","skipped_unresolvable","cost_estimate"]}},"required":["data"]}}}},"202":{"description":"Resolution started — generations were queued.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"project_id":{"type":"string","format":"uuid"},"editor_url":{"type":"string","format":"uri"},"dry_run":{"type":"boolean"},"triggered":{"type":"object","properties":{"scenes":{"type":"integer","description":"Scene composition generations fired."},"voiceover_chapters":{"type":"integer","description":"Chapters for which voice-over generation was fired."}},"required":["scenes","voiceover_chapters"]},"skipped_unresolvable":{"type":"integer","description":"Scenes skipped — nothing to generate them from."},"cost_estimate":{"type":"object","properties":{"scenes":{"type":"integer"},"voiceover":{"type":"integer"},"total":{"type":"integer"}},"required":["scenes","voiceover","total"],"description":"Estimated credits to resolve the unresolved work. The real charge happens at generation time."}},"required":["project_id","editor_url","dry_run","triggered","skipped_unresolvable","cost_estimate"]}},"required":["data"]}}}},"400":{"description":"Invalid scope (e.g. chapter scope without chapterId).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Subscription inactive (past_due / unpaid).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Project, chapter, or block not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/catalogs/{catalog}":{"get":{"summary":"List a value catalog","description":"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), `music` (published music tracks + license info). Pass the returned slug/id values to the matching creation or editing fields.","operationId":"listCatalog","tags":["Catalogs"],"parameters":[{"schema":{"type":"string","enum":["transitions","caption-styles","voices","music"]},"required":true,"name":"catalog","in":"path"}],"responses":{"200":{"description":"Catalog rows.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Catalog rows; the shape depends on the catalog (see description)."}},"required":["data"]}}}},"404":{"description":"Unknown catalog.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/scenes/{sceneId}/attach-media":{"post":{"summary":"Attach media to a scene","description":"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.","operationId":"attachSceneMedia","tags":["Scenes"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"sceneId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachMediaRequest"}}}},"responses":{"200":{"description":"Media attached; scene background updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachMediaResponse"}}}},"400":{"description":"Invalid request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Plan does not include API access.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Scene not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{}}