# API Reference

This reference is generated from the live OpenAPI builder, schema registry, capabilities manifest, examples manifest, and UI/API/CLI/MCP surface registry. Do not hand-edit endpoint lists; change the contract code and regenerate.

## Machine-readable surfaces

| Surface | Path |
|---|---|
| Capabilities | `/api/v1/capabilities` |
| OpenAPI JSON | `/api/v1/openapi.json` |
| Examples | `/api/v1/examples` |
| Agent index | `/agent-index.json` |
| Four-surface contract | `/docs/surface-contract.json` |
| CLI command map | `/docs/cli-commands.json` |
| MCP tool map | `/docs/mcp-tools.json` |

## Surface coverage

- Operations: 341
- OpenAPI operations: 336
- Complete UI/API/CLI/MCP operations: 340 / 341

| Surface | Bound operations |
|---|---|
| UI | 341 / 341 |
| API | 340 / 341 |
| CLI | 341 / 341 |
| MCP | 340 / 341 |

## Development flow warnings

### Complete the Series Bible before screenplay pages

- Stage: `before_screenplay_generation`
- Blocking: no
- Surfaces: `ui`, `api`, `cli`, `mcp`
- Guidance: Before planning or generating a screenplay-backed table_read artifact, read, save, or generate the project Series Bible and confirm the format, audience, season arc, episode map, character roster, canon/world rules, visual style, audio style, and open questions are usable. The planner and screenplay adapter auto-load the saved Series Bible as project canon; if the user proceeds anyway, carry the missing areas into the StoryPlan review instead of silently ignoring them.

### Finish the project stage a refusal names — do not retry it

- Stage: `before_planning_or_generation`
- Blocking: yes
- Surfaces: `ui`, `api`, `cli`, `mcp`
- Guidance: Planning, drafting episodes, casting, keeping or rendering mood board frames, writing the episode map, and generating project media are refused until the project reaches the stage each one needs. The refusal is a 409 `project_precondition_failed` whose `error.details.stage` names the stage the project has not finished (premise, premise_coverage, planning, full_coverage) with `missingFields`, `canPlan` and `canStartEpisode`. It is state, not a transient fault: nothing was created or charged, and the identical call refuses the same way until the stage is done — a recurring caller must stop and finish the stage (or tell the writer) rather than retry on a schedule.

### A table read performs the project cast — complete it first

- Stage: `before_planning_or_generation`
- Blocking: yes
- Surfaces: `ui`, `api`, `cli`, `mcp`
- Guidance: A table_read job never casts. From a plan it starts only when the project cast canon, plus the request's own voiceMap, covers every canon character: a voice for everyone and, unless the project is audio-only (Series Bible productionMedium "audio"), a headshot, body shot and character sheet; a narrated read also needs a narrator voice. A job that adopts a finalized screenplay (scriptUploadId) needs that screenplay's episode cast complete. The refusal is a 409 `cast_precondition_failed` before anything is created or charged, with `details.reason` and `details.missing`. Once the job has written its screenplay, a speaker the cast does not cover fails the job (failureCode `cast_precondition_failed`, a contract failure) before any audio, and its credits are released.

### Complete the Series Bible Mood board before rendering

- Stage: `before_storyboard_rendering`
- Blocking: yes
- Surfaces: `ui`, `api`, `cli`, `mcp`
- Guidance: Image and video rendering are locked until the Series Bible has explicit production medium, visual style, character design, visual truths, production design, color palette, lighting rules, camera language, and location truths. Read document.readiness.moodBoard on the shared Series Bible response and resolve every missing field before rendering.

### Run one coverage round after the screenplay exists

- Stage: `after_screenplay_exists`
- Blocking: no
- Surfaces: `ui`, `api`, `cli`, `mcp`
- Guidance: Once a screenplay-backed table_read artifact exists, read or poll the first coverage report before treating the script as ready for finalization, pitch packaging, or later installment generation. Table-read jobs queue the initial coverage pass when pages are created; request a new report only if none exists or the user wants another pass. Use the report, especially top fixes, to drive one revision pass. This is guidance, not an enforcement gate.

## Account

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `DELETE /api/v1/account/deletion` | `cancelAccountDeletion` | account:write | no | response:AccountDeletionStatus |
| `GET /api/v1/account/deletion` | `getAccountDeletion` | account:read | no | response:AccountDeletionStatus |
| `POST /api/v1/account/deletion` | `requestAccountDeletion` | account:write | yes | request:AccountDeletionRequest<br>response:AccountDeletionStatus |
| `GET /api/v1/account/export` | `getAccountExport` | account:read | no | response:AccountExportStatus |
| `POST /api/v1/account/export` | `requestAccountExport` | account:read | yes | response:AccountExportStatus |

## Artifacts

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `GET /api/v1/artifacts/{artifactId}` | `getStoryArtifact` | artifact:read | no | response:StoryArtifactEnvelope |
| `PATCH /api/v1/artifacts/{artifactId}` | `renameStoryArtifact` | story:write | no | request:ArtifactRenameRequest<br>response:RenamedArtifact |
| `POST /api/v1/artifacts/{artifactId}/avatar-video` | `animateArtifactAvatar` | artifact:publish | yes | request:TableReadAvatarVideoRequest<br>response:TableReadAvatarVideoResponse |
| `POST /api/v1/artifacts/{artifactId}/avatar` | `recastArtifactAvatar` | artifact:publish | yes | request:AvatarRecastRequest |
| `GET /api/v1/artifacts/{artifactId}/cast` | `getArtifactCast` | artifact:read | no | response:CastEnvelope |
| `POST /api/v1/artifacts/{artifactId}/cast` | `updateArtifactCast` | artifact:publish | yes | request:CastBatchRequest |
| `GET /api/v1/artifacts/{artifactId}/coverage` | `getArtifactCoverage` | artifact:read | no | response:CoverageReportEnvelope |
| `POST /api/v1/artifacts/{artifactId}/coverage` | `generateArtifactCoverage` | story:write | yes | response:CoverageDispatchAck |
| `GET /api/v1/artifacts/{artifactId}/coverage/blacklist` | `getBlackListAnalysis` | artifact:read | no | response:BlackListAnalysisResponse |
| `GET /api/v1/artifacts/{artifactId}/coverage/versions` | `listCoverageVersions` | artifact:read | no | response:CoverageVersionsResponse |
| `POST /api/v1/artifacts/{artifactId}/figure` | `recastArtifactFigure` | artifact:publish | yes | request:FigureRecastRequest |
| `POST /api/v1/artifacts/{artifactId}/finalize` | `finalizeArtifact` | artifact:publish | yes |  |
| `POST /api/v1/artifacts/{artifactId}/finalize/retry` | `retryArtifactFinalize` | artifact:publish | yes | response:ArtifactFinalizeAck |
| `GET /api/v1/artifacts/{artifactId}/location-plates` | `getArtifactLocationPlates` | artifact:read | no | response:LocationPlatesEnvelope |
| `POST /api/v1/artifacts/{artifactId}/location-plates/{plateKey}/remake` | `remakeArtifactLocationPlate` | artifact:publish | yes | request:LocationPlateRemakeRequest<br>response:LocationPlateRemakeEnvelope |
| `GET /api/v1/artifacts/{artifactId}/music` | `getArtifactMusic` | artifact:read | no | response:MusicStatusEnvelope |
| `POST /api/v1/artifacts/{artifactId}/music` | `generateArtifactMusic` | artifact:publish | yes | request:MusicGenerateRequest<br>response:MusicGenerateResponse |
| `GET /api/v1/artifacts/{artifactId}/noun-imagery` | `getArtifactNounImagery` | artifact:read | no | response:TableReadNounImageryResponse |
| `POST /api/v1/artifacts/{artifactId}/noun-imagery` | `generateArtifactNounImagery` | artifact:publish | yes | request:TableReadNounImageryRequest<br>response:TableReadNounImageryQueued |
| `POST /api/v1/artifacts/{artifactId}/performance` | `updateArtifactPerformance` | artifact:publish | yes | request:PerformanceDirectiveRequest<br>response:PerformanceDirectiveResponse |
| `GET /api/v1/artifacts/{artifactId}/publish-state` | `getArtifactPublishState` | artifact:read | no | response:ArtifactPublishState |
| `POST /api/v1/artifacts/{artifactId}/publish-state` | `setArtifactPublishState` | artifact:publish | no | request:ArtifactPublishStateRequest<br>response:ArtifactPublishStateResult |
| `POST /api/v1/artifacts/{artifactId}/refine` | `refineArtifact` | artifact:publish | yes | request:RefineArtifactRequest<br>response:RefineArtifactEnvelope |
| `GET /api/v1/artifacts/{artifactId}/render-video` | `getArtifactRenderStatus` | artifact:read | no | response:ArtifactRenderStatus |
| `POST /api/v1/artifacts/{artifactId}/render-video` | `renderArtifactVideo` | artifact:publish | yes | response:ArtifactRenderAck |
| `GET /api/v1/artifacts/{artifactId}/review-items` | `listLineReviews` | artifact:read | no | response:LineReviewList |
| `POST /api/v1/artifacts/{artifactId}/review-items` | `flagLineForReview` | artifact:publish | no | request:LineReviewFlag<br>response:LineReviewFlagResult |
| `DELETE /api/v1/artifacts/{artifactId}/review-items/{entryIndex}` | `unflagLineForReview` | artifact:publish | no |  |
| `POST /api/v1/artifacts/{artifactId}/revoice-line` | `revoiceArtifactLine` | artifact:publish | yes | request:RevoiceLineRequest<br>response:RevoiceLineEnvelope |
| `POST /api/v1/artifacts/{artifactId}/revoice` | `revoiceArtifactCharacter` | artifact:publish | yes | request:RevoiceRequest<br>response:RevoiceEnvelope |
| `GET /api/v1/artifacts/{artifactId}/script` | `getArtifactScript` | artifact:read | no | response:ArtifactScriptContentEnvelope |
| `POST /api/v1/artifacts/{artifactId}/script` | `replaceArtifactScript` | artifact:publish | yes | request:ArtifactScriptReplacementRequest<br>response:StoryJobEnvelope |
| `GET /api/v1/artifacts/{artifactId}/sfx` | `listArtifactSfx` | artifact:read | no | response:SfxListResponse |
| `POST /api/v1/artifacts/{artifactId}/sfx` | `manageArtifactSfx` | artifact:publish | yes | request:SfxManageRequest<br>response:SfxMutationResponse |
| `GET /api/v1/artifacts/{artifactId}/soundscapes` | `listArtifactSoundscapes` | artifact:read | no | response:TableReadSoundscapesResponse |
| `POST /api/v1/artifacts/{artifactId}/soundscapes` | `manageArtifactSoundscapes` | artifact:publish | yes | request:TableReadSoundscapeRequest<br>response:TableReadSoundscapeResponse |
| `POST /api/v1/artifacts/{artifactId}/theater-backdrop-prompts` | `writeArtifactTheaterBackdropPrompts` | artifact:publish | yes | request:TableReadBackdropPromptsRequest<br>response:TableReadBackdropPromptsResponse |
| `POST /api/v1/artifacts/{artifactId}/theater-canvas` | `applyTheaterCanvas` | artifact:publish | yes | request:TheaterCanvasRequest<br>response:TheaterMotionResult |
| `POST /api/v1/artifacts/{artifactId}/theater-fx-cues` | `generateArtifactTheaterFxCues` | artifact:publish | yes | request:TableReadFxCuesRequest<br>response:TableReadFxCuesResponse |
| `POST /api/v1/artifacts/{artifactId}/theater-motion-clip` | `generateTheaterMotionClip` | artifact:publish | yes | request:TheaterMotionClipRequest<br>response:TheaterMotionResult |
| `POST /api/v1/artifacts/{artifactId}/theater-motion` | `directTheaterMotionBeat` | artifact:publish | yes | request:TheaterMotionBeatRequest<br>response:TheaterMotionResult |
| `POST /api/v1/artifacts/{artifactId}/theater-scene` | `updateArtifactTheaterScene` | artifact:publish | yes | request:TableReadTheaterSceneRequest<br>response:TableReadTheaterSceneResponse |
| `GET /api/v1/artifacts/{artifactId}/theater` | `getArtifactTheater` | artifact:read | no | response:TableReadTheaterResponse |
| `POST /api/v1/artifacts/{artifactId}/theater` | `updateArtifactTheater` | artifact:publish | yes | request:TableReadTheaterUpdateRequest<br>response:TableReadTheaterUpdateResponse |
| `POST /api/v1/artifacts/{artifactId}/update-recording` | `updateArtifactRecording` | artifact:publish | yes | request:UpdateRecordingRequest<br>response:UpdateRecordingEnvelope |
| `POST /api/v1/artifacts/{artifactId}/voice-modification` | `modifyArtifactVoice` | artifact:publish | yes | request:VoiceModificationRequest<br>response:VoiceModificationResponse |
| `POST /api/v1/artifacts/{artifactId}/voice` | `recastArtifactVoice` | artifact:publish | yes |  |
| `POST /api/v1/artifacts/{artifactId}/voice/restore` | `restoreArtifactVoice` | artifact:publish | yes | request:VoiceRestoreRequest<br>response:VoiceRestoreResponse |
| `GET /api/v1/story-jobs/{jobId}/artifacts` | `listStoryJobArtifacts` | artifact:read | no | response:StoryArtifactListResponse |

## Brand

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `POST /api/v1/brand-coverage` | `analyzeBrandCoverage` | story:write | yes | request:BrandCoverageRequest<br>response:BrandCoverageAck |
| `GET /api/v1/brand-coverage/{reportId}` | `getBrandCoverage` | story:read | no | response:BrandCoverageReport |
| `POST /api/v1/brand-narrative` | `generateBrandNarrative` | story:write | yes | request:BrandNarrativeRequest<br>response:BrandNarrativeResponse |

## Credits

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `GET /api/v1/credits` | `getCredits` | credits:read | no | response:CreditsSummary |
| `GET /api/v1/generation-health` | `getGenerationHealth` | story:read | no | response:GenerationHealthEnvelope |

## Discovery

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `GET /api/v1/agent-guidance` | `getAgentGuidance` | story:read | no | response:AgentGuidance |
| `GET /api/v1/capabilities` | `getCapabilities` | story:read | no | response:Capabilities |
| `DELETE /api/v1/device/credential` | `revokeDeviceCredential` |  | no |  |
| `POST /api/v1/device/credential` | `exchangeDeviceCode` |  | no |  |
| `GET /api/v1/examples` | `getApiExamples` |  | no |  |

## Jobs

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `GET /api/v1/background-activity` | `getBackgroundActivity` | story:read | no |  |
| `GET /api/v1/screenplay-drafts/{scriptUploadId}` | `getScreenplayDraft` | story:read | no | response:ScreenplayDraftStatus |
| `POST /api/v1/screenplay-drafts/{scriptUploadId}` | `startScreenplayDraft` | story:write | yes | response:ScreenplayDraftStatus |
| `GET /api/v1/story-jobs` | `listStoryJobs` | story:read | no | response:StoryJobList |
| `POST /api/v1/story-jobs` | `createStoryJob` | story:write | yes | request:StoryJobCreate<br>response:StoryJobEnvelope |
| `GET /api/v1/story-jobs/{jobId}` | `getStoryJob` | story:read | no | response:StoryJobEnvelope |
| `POST /api/v1/story-jobs/{jobId}/cancel` | `cancelStoryJob` | story:write | yes | response:StoryJobEnvelope |
| `POST /api/v1/story-jobs/{jobId}/resume` | `resumeStoryJob` | story:write | yes | response:StoryJobResumeResponse |

## Library

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `GET /api/v1/library/audio` | `listMyLibraryAudio` | story:read | no | response:MyLibraryAudio |
| `POST /api/v1/library/audio` | `importMyLibraryAudio` | story:write | yes | request:MyLibraryAudioImportRequest<br>response:MyLibraryAudioImportResponse |
| `PATCH /api/v1/library/audio/{assetId}` | `renameMyLibraryAudio` | story:write | no | request:MyLibraryAudioRenameRequest<br>response:MyLibraryAudioRenameResponse |
| `GET /api/v1/library/images` | `listMyLibraryImages` | story:read | no | response:MyLibraryImages |

## Pitch Deck

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `DELETE /api/v1/pitch-decks/{jobId}` | `deletePitchDeck` | story:write | no |  |
| `PATCH /api/v1/pitch-decks/{jobId}` | `renamePitchDeck` | story:write | no | request:ArtifactRenameRequest<br>response:RenamedVideo |
| `GET /api/v1/pitch-decks/{jobId}/bell-lane` | `getPitchDeckBellLane` | story:read | no |  |
| `POST /api/v1/pitch-decks/{jobId}/bell-lane` | `strikePitchDeckBellOnTheBeat` | story:write | yes |  |
| `POST /api/v1/pitch-decks/{jobId}/bell-lane/remove` | `removePitchDeckBellLane` | story:write | yes |  |
| `GET /api/v1/pitch-decks/{jobId}/camera` | `getPitchDeckCamera` | story:read | no | response:CameraView |
| `POST /api/v1/pitch-decks/{jobId}/camera` | `setPitchDeckCamera` | story:write | yes | request:CameraSetRequest<br>response:CameraSetAck |
| `POST /api/v1/pitch-decks/{jobId}/cancel-render` | `cancelPitchDeckRender` | artifact:publish | yes | response:RenderCancelResult |
| `GET /api/v1/pitch-decks/{jobId}/chapters` | `getPitchDeckChapters` | story:read | no | response:PitchDeckChapters |
| `POST /api/v1/pitch-decks/{jobId}/chapters/{chapterIndex}/abandon-take` | `abandonPitchDeckTake` | artifact:publish | yes | request:TakeAbandonRequest<br>response:TakeAbandonResult |
| `POST /api/v1/pitch-decks/{jobId}/chapters/{chapterIndex}/drop-line` | `dropPitchDeckChapterLine` | story:write | yes | request:PitchDeckDropLineRequest<br>response:PitchDeckDropLineAnswer |
| `POST /api/v1/pitch-decks/{jobId}/chapters/{chapterIndex}/repair-take` | `repairPitchDeckTake` | artifact:publish | yes | request:TakeRepairRequest<br>response:TakeRepairAnswer |
| `POST /api/v1/pitch-decks/{jobId}/chapters/{chapterIndex}/select-take` | `selectPitchDeckTake` | story:write | yes | request:PitchDeckSelectTakeRequest<br>response:PitchDeckSelectTakeAck |
| `POST /api/v1/pitch-decks/{jobId}/chapters/{chapterIndex}/stage-voice-over` | `stageVoiceOverPitchDeckTake` | artifact:publish | yes | request:StageVoiceOverRequest<br>response:StageVoiceOverAnswer |
| `POST /api/v1/pitch-decks/{jobId}/chapters/{chapterIndex}/take-repair-override` | `overridePitchDeckTakeRepair` | story:write | yes | request:TakeRepairOverrideRequest<br>response:TakeRepairOverrideAck |
| `POST /api/v1/pitch-decks/{jobId}/chapters/{chapterIndex}/trim-take` | `trimPitchDeckTake` | story:write | yes | request:TakeTrimRequest<br>response:TakeTrimQueued |
| `POST /api/v1/pitch-decks/{jobId}/chapters/insert` | `insertPitchDeckChapter` | story:write | yes | request:PitchDeckInsertChapterRequest<br>response:PitchDeckInsertChapterAck |
| `POST /api/v1/pitch-decks/{jobId}/chapters/refine` | `refinePitchDeckChapter` | story:write | yes | request:PitchDeckChapterRefineRequest<br>response:PitchDeckChapterRefineAck |
| `POST /api/v1/pitch-decks/{jobId}/chapters/remove` | `removePitchDeckChapter` | story:write | yes | request:PitchDeckRemoveChapterRequest<br>response:PitchDeckRemoveChapterAck |
| `POST /api/v1/pitch-decks/{jobId}/chapters/render-clip` | `renderPitchDeckChapterClip` | artifact:publish | yes | request:PitchDeckRenderClipRequest<br>response:PitchDeckRenderAnswer |
| `POST /api/v1/pitch-decks/{jobId}/chapters/reorder` | `reorderPitchDeckChapters` | story:write | yes | request:PitchDeckReorderChaptersRequest<br>response:PitchDeckReorderChaptersAck |
| `POST /api/v1/pitch-decks/{jobId}/closing-sequence` | `updatePitchDeckClosingSequence` | story:write | yes | request:PitchDeckClosingSequenceUpdateRequest<br>response:PitchDeckClosingSequenceUpdateAck |
| `POST /api/v1/pitch-decks/{jobId}/duplicate` | `duplicatePitchDeck` | story:write | yes | request:PitchDeckDuplicateRequest<br>response:PitchDeckDuplicate |
| `POST /api/v1/pitch-decks/{jobId}/finish` | `finishPitchDeck` | artifact:publish | yes | request:PitchDeckFinishRequest<br>response:PitchDeckFinishAnswer |
| `GET /api/v1/pitch-decks/{jobId}/intent` | `getPitchDeckIntent` | story:read | no | response:VideoIntent |
| `POST /api/v1/pitch-decks/{jobId}/intent` | `updatePitchDeckIntent` | story:write | yes | request:VideoIntentEditRequest<br>response:VideoIntentEditAck |
| `POST /api/v1/pitch-decks/{jobId}/interludes` | `updatePitchDeckInterludes` | story:write | yes | request:PitchDeckInterludesUpdateRequest<br>response:PitchDeckInterludesUpdateAck |
| `GET /api/v1/pitch-decks/{jobId}/look` | `getPitchDeckLook` | story:read | no | response:FinishView |
| `POST /api/v1/pitch-decks/{jobId}/look` | `setPitchDeckLook` | story:write | yes | request:FinishSetRequest<br>response:FinishSetAck |
| `POST /api/v1/pitch-decks/{jobId}/more-fixes` | `allowMorePitchDeckFixes` | artifact:publish | yes | request:MoreFixesRequest<br>response:MoreFixesAnswer |
| `GET /api/v1/pitch-decks/{jobId}/motion-blur` | `getPitchDeckMotionBlur` | story:read | no | response:MotionBlurView |
| `POST /api/v1/pitch-decks/{jobId}/motion-blur` | `setPitchDeckMotionBlur` | story:write | yes | request:MotionBlurSetRequest<br>response:MotionBlurSetAck |
| `GET /api/v1/pitch-decks/{jobId}/narrator` | `getPitchDeckNarrator` | story:read | no |  |
| `POST /api/v1/pitch-decks/{jobId}/narrator` | `setPitchDeckNarrator` | story:write | yes |  |
| `POST /api/v1/pitch-decks/{jobId}/pdf` | `requestPitchDeckPdf` | story:write | yes | response:PitchDeckPdf |
| `POST /api/v1/pitch-decks/{jobId}/plan/approve` | `approvePitchDeckPlan` | story:write | yes | request:PitchDeckPlanApproveRequest<br>response:PitchDeckPlanApproveAnswer |
| `POST /api/v1/pitch-decks/{jobId}/publish` | `publishPitchDeck` | story:write | yes | response:PitchDeckPublish |
| `GET /api/v1/pitch-decks/{jobId}/quiet-track` | `getPitchDeckQuietTrack` | story:read | no | response:QuietTrackView |
| `POST /api/v1/pitch-decks/{jobId}/quiet-track` | `setPitchDeckQuietTrack` | story:write | yes | request:QuietTrackSetRequest<br>response:QuietTrackSetAck |
| `POST /api/v1/pitch-decks/{jobId}/reassemble` | `reassemblePitchDeck` | story:write | yes | response:RenderReassembleAck |
| `GET /api/v1/pitch-decks/{jobId}/resolution` | `getPitchDeckResolution` | story:read | no |  |
| `POST /api/v1/pitch-decks/{jobId}/resolution` | `setPitchDeckResolution` | story:write | yes |  |
| `GET /api/v1/pitch-decks/{jobId}/score-to-picture` | `getPitchDeckScoreToPicture` | story:read | no |  |
| `POST /api/v1/pitch-decks/{jobId}/score-to-picture` | `scorePitchDeckToPicture` | artifact:publish | yes |  |
| `POST /api/v1/pitch-decks/{jobId}/score-to-picture/revert` | `revertPitchDeckScoreToPicture` | story:write | yes |  |
| `GET /api/v1/pitch-decks/{jobId}/storyboard/audio-timeline` | `getPitchDeckAudioTimeline` | story:read | no |  |
| `POST /api/v1/pitch-decks/{jobId}/storyboard/audio-timeline` | `editPitchDeckAudioTimeline` | story:write | yes |  |
| `GET /api/v1/pitch-decks/{jobId}/storyboard/audio` | `getPitchDeckSceneAudio` | story:read | no |  |
| `GET /api/v1/pitch-decks/{jobId}/storyboard/coverage` | `getPitchDeckStoryboardCoverage` | story:read | no |  |
| `POST /api/v1/pitch-decks/{jobId}/storyboard/coverage` | `generatePitchDeckStoryboardCoverage` | story:write | yes |  |
| `POST /api/v1/pitch-decks/{jobId}/storyboard/direct-text-motion` | `directPitchDeckTextMotion` | story:write | yes | request:PitchDeckDirectTextMotionRequest<br>response:PitchDeckDirectTextMotionAck |
| `POST /api/v1/pitch-decks/{jobId}/storyboard/music` | `editPitchDeckSceneMusic` | story:write | yes |  |
| `POST /api/v1/pitch-decks/{jobId}/storyboard/regenerate-text` | `regeneratePitchDeckText` | artifact:publish | yes | request:PitchDeckChapterIndexRequest<br>response:PitchDeckTextRenderAnswer |
| `POST /api/v1/pitch-decks/{jobId}/storyboard/sfx` | `editPitchDeckSceneSfx` | story:write | yes |  |
| `GET /api/v1/pitch-decks/{jobId}/storyboard/sound-map` | `getPitchDeckSoundMap` | story:read | no |  |
| `POST /api/v1/pitch-decks/{jobId}/title-sequence` | `updatePitchDeckTitleSequence` | story:write | yes | request:PitchDeckTitleSequenceUpdateRequest<br>response:PitchDeckTitleSequenceUpdateAck |
| `POST /api/v1/pitch-decks/{jobId}/title-sequence/motion-closes` | `renderPitchDeckTitleMotionCloses` | artifact:publish | yes | request:PitchDeckTitleMotionClosesRequest<br>response:PitchDeckTitleMotionClosesAnswer |
| `POST /api/v1/pitch-decks/{jobId}/unpublish` | `unpublishPitchDeck` | story:write | yes | response:PitchDeckUnpublish |
| `GET /api/v1/pitch-decks/{jobId}/versions` | `listPitchDeckVersions` | story:read | no | response:PitchDeckVersions |
| `GET /api/v1/pitch-decks/{jobId}/versions/{number}` | `getPitchDeckVersion` | story:read | no | response:PitchDeckVersionRead |
| `POST /api/v1/pitch-decks/{jobId}/versions/{number}/zip` | `requestPitchDeckVersionZip` | story:write | yes | response:VideoVersionZipAck |
| `GET /api/v1/pitch-decks/{jobId}/video-coverage` | `getPitchDeckVideoCoverage` | story:read | no | response:VideoCoverageEnvelope |
| `POST /api/v1/pitch-decks/{jobId}/video-coverage` | `generatePitchDeckVideoCoverage` | story:write | yes | request:VideoCoverageGenerateRequest<br>response:VideoCoverageEnvelope |
| `POST /api/v1/pitch-decks/{jobId}/video-export` | `exportPitchDeckVideo` | story:write | yes | response:PitchDeckExportAck |
| `GET /api/v1/pitch-decks/{jobId}/video-model` | `getPitchDeckVideoModel` | story:read | no | response:PitchDeckVideoModelView |
| `POST /api/v1/pitch-decks/{jobId}/video-model` | `setPitchDeckVideoModel` | story:write | yes | request:PitchDeckVideoModelSetRequest<br>response:PitchDeckVideoModelSetAck |
| `POST /api/v1/pitch-decks/{jobId}/visual-grammar` | `updatePitchDeckVisualGrammar` | story:write | yes | request:PitchDeckVisualGrammarUpdateRequest<br>response:PitchDeckVisualGrammarUpdateAck |

## Plans

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `GET /api/v1/story-plans/{planId}` | `getStoryPlan` | story:read | no | response:StoryPlanEnvelopeResponse |
| `PATCH /api/v1/story-plans/{planId}` | `renameStoryPlan` | story:write | no | request:ArtifactRenameRequest<br>response:StoryPlanEnvelopeResponse |
| `POST /api/v1/story-plans/{planId}/approve` | `approveStoryPlan` | story:write | yes | request:StoryPlanApprovalRequest<br>response:StoryPlanEnvelopeResponse |
| `POST /api/v1/story-plans/{planId}/reject` | `rejectStoryPlan` | story:write | yes | response:StoryPlanEnvelopeResponse |
| `POST /api/v1/story-plans/{planId}/resume` | `resumeStoryPlan` | story:write | yes | response:StoryPlanEnvelopeResponse |
| `GET /api/v1/story-projects/{projectId}/story-plans` | `listStoryPlans` | story:read | no | response:StoryPlanList |
| `POST /api/v1/story-projects/{projectId}/story-plans` | `createStoryPlan` | story:write | yes | request:StoryPlanCreate<br>response:StoryPlanEnvelopeResponse |

## Projects

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `GET /api/v1/story-projects` | `listStoryProjects` | story:read | no | response:StoryProjectList |
| `POST /api/v1/story-projects` | `createStoryProject` | story:write | yes | request:StoryProjectCreate<br>response:StoryProjectEnvelope |
| `DELETE /api/v1/story-projects/{projectId}` | `deleteStoryProject` | story:write | no | response:ProjectDeletionResult |
| `GET /api/v1/story-projects/{projectId}` | `getStoryProject` | story:read | no | response:StoryProjectDetailEnvelope |
| `PATCH /api/v1/story-projects/{projectId}` | `updateStoryProject` | story:write | no | request:StoryProjectPatch<br>response:StoryProjectEnvelope |
| `GET /api/v1/story-projects/{projectId}/cast-canon` | `getProjectCastCanon` | story:read | no | response:CastCanonEnvelope |
| `PATCH /api/v1/story-projects/{projectId}/cast-canon` | `saveProjectCastCanon` | story:write | no | request:CastCanonPatch<br>response:CastCanonEnvelope |
| `GET /api/v1/story-projects/{projectId}/cast-look` | `getProjectCastLook` | story:read | no | response:CastLookRead |
| `PATCH /api/v1/story-projects/{projectId}/cast-look` | `setProjectCastLook` | story:write | no | request:CastLookPatch<br>response:CastLookSaved |
| `POST /api/v1/story-projects/{projectId}/cast-portraits/choose` | `chooseProjectCastPortrait` | story:write | yes | request:CastPortraitChooseRequest<br>response:CastPortraitChooseResponse |
| `POST /api/v1/story-projects/{projectId}/cast-portraits/design` | `designProjectCastPortraits` | artifact:publish | yes | request:CastPortraitDesignRequest<br>response:CastPortraitDesignEnvelope |
| `GET /api/v1/story-projects/{projectId}/cast-portraits/designs` | `listProjectCastPortraitDesigns` | story:read | no | response:CastPortraitDesignListResponse |
| `GET /api/v1/story-projects/{projectId}/cast-portraits/designs/{designId}/coverage` | `getProjectCastPortraitCoverage` | story:read | no | response:CastingPortraitCoverageResponse |
| `POST /api/v1/story-projects/{projectId}/cast-portraits/designs/{designId}/coverage` | `generateProjectCastPortraitCoverage` | story:write | yes | request:CastingCoverageGenerateRequest<br>response:CastingPortraitCoverageResponse |
| `PATCH /api/v1/story-projects/{projectId}/cast-voice` | `setProjectCastVoice` | story:write | no | request:CastVoiceSetRequest<br>response:CastVoiceSetResponse |
| `POST /api/v1/story-projects/{projectId}/cast-voice/choose` | `chooseProjectCastVoice` | story:write | yes | request:CastVoiceChooseRequest<br>response:CastVoiceChooseResponse |
| `POST /api/v1/story-projects/{projectId}/cast-voice/design` | `designProjectCastVoice` | artifact:publish | yes | request:CastVoiceDesignRequest<br>response:CastVoiceDesignEnvelope |
| `GET /api/v1/story-projects/{projectId}/cast-voice/designs` | `listProjectCastVoiceDesigns` | story:read | no | response:CastVoiceDesignListResponse |
| `GET /api/v1/story-projects/{projectId}/cast-voice/designs/{designId}/coverage` | `getProjectCastVoiceCoverage` | story:read | no | response:CastingCoverageResponse |
| `POST /api/v1/story-projects/{projectId}/cast-voice/designs/{designId}/coverage` | `generateProjectCastVoiceCoverage` | story:write | yes | request:CastingCoverageGenerateRequest<br>response:CastingCoverageResponse |
| `GET /api/v1/story-projects/{projectId}/cast` | `getProjectCast` | story:read | no | response:ProjectCastReadEnvelope |
| `PATCH /api/v1/story-projects/{projectId}/cast` | `updateEpisodeCharacterDirection` | story:write | no | request:EpisodeCharacterDirectionRequest<br>response:EpisodeCharacterDirectionResult |
| `POST /api/v1/story-projects/{projectId}/cast` | `generateProjectCast` | artifact:publish | yes | request:GenerateProjectCastRequest<br>response:ProjectCastEnvelope |
| `GET /api/v1/story-projects/{projectId}/episodes` | `listProjectEpisodes` | story:read | no | response:ProjectEpisodesResponse |
| `POST /api/v1/story-projects/{projectId}/episodes` | `startPlannedEpisode` | story:write | yes | request:StartPlannedEpisodeRequest<br>response:StartPlannedEpisodeResponse |
| `GET /api/v1/story-projects/{projectId}/mood-board` | `getProjectMoodBoard` | story:read | no | response:MoodBoardEnvelope |
| `POST /api/v1/story-projects/{projectId}/mood-board/frames` | `keepProjectMoodBoardFrame` | story:write | yes | request:MoodBoardFrameKeepRequest<br>response:MoodBoardEnvelope |
| `DELETE /api/v1/story-projects/{projectId}/mood-board/frames/{frameId}` | `dropProjectMoodBoardFrame` | story:write | no | response:MoodBoardEnvelope |
| `POST /api/v1/story-projects/{projectId}/mood-board/render-frames` | `renderProjectMoodBoardFrames` | artifact:publish | yes | request:MoodBoardRenderRequest<br>response:MoodBoardRenderEnvelope |
| `GET /api/v1/story-projects/{projectId}/outline-map` | `getProjectOutlineMap` | story:read | no | response:OutlineMapEnvelope |
| `PATCH /api/v1/story-projects/{projectId}/outline-map` | `saveProjectEpisodeOutline` | story:write | no | request:SaveEpisodeOutlineRequest<br>response:OutlineMapEnvelope |
| `GET /api/v1/story-projects/{projectId}/outline-map/coverage` | `getProjectOutlineCoverage` | story:read | no | response:OutlineCoverageEnvelope |
| `POST /api/v1/story-projects/{projectId}/outline-map/coverage` | `generateProjectOutlineCoverage` | story:write | yes | request:OutlineCoverageGenerateRequest<br>response:OutlineCoverageEnvelope |
| `POST /api/v1/story-projects/{projectId}/restore` | `restoreStoryProject` | story:write | yes | response:ProjectRestoreResult |
| `POST /api/v1/story-projects/{projectId}/seasons` | `createProjectSeason` | story:write | yes | request:ProjectSeasonCreateRequest<br>response:ProjectSeasonCreateResponse |
| `GET /api/v1/story-projects/{projectId}/series-bible` | `getProjectSeriesBible` | story:read | no | response:SeriesBibleEnvelope |
| `PATCH /api/v1/story-projects/{projectId}/series-bible` | `saveProjectSeriesBible` | story:write | no | request:SeriesBiblePatch<br>response:SeriesBibleSaveResult |
| `POST /api/v1/story-projects/{projectId}/series-bible` | `generateProjectSeriesBible` | story:write | yes | request:SeriesBibleGenerateRequest<br>response:SeriesBibleGenerationStarted |
| `GET /api/v1/story-projects/{projectId}/series-bible/characters` | `getSeriesBibleCharacter` | story:read | no | response:SeriesBibleCharacterResult |
| `PATCH /api/v1/story-projects/{projectId}/series-bible/characters` | `updateSeriesBibleCharacter` | story:write | no | request:SeriesBibleCharacterUpdateRequest<br>response:SeriesBibleCharacterResult |
| `GET /api/v1/story-projects/{projectId}/series-bible/coverage` | `getProjectSeriesBibleCoverage` | story:read | no | response:SeriesBibleCoverageEnvelope |
| `POST /api/v1/story-projects/{projectId}/series-bible/coverage` | `generateProjectSeriesBibleCoverage` | story:write | yes | request:SeriesBibleCoverageGenerateRequest<br>response:SeriesBibleCoverageEnvelope |
| `GET /api/v1/story-projects/{projectId}/sounds` | `listProjectSounds` | story:read | no |  |
| `GET /api/v1/story-projects/deleted` | `listDeletedStoryProjects` | story:read | no | response:DeletedStoryProjectList |
| `POST /api/v1/story-projects/from-script` | `createStoryProjectFromScript` | story:write | yes | request:StoryProjectFromScript<br>response:StoryProjectFromScriptEnvelope |

## Publishing

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `GET /api/v1/publishing-feeds/{seriesSlug}.xml` | `getPublishingRssFeed` |  | no |  |
| `GET /api/v1/publishing-provider-configs` | `listPublishingProviderConfigs` | publishing:connect | no | response:PublishingProviderConfigList |
| `POST /api/v1/publishing-provider-configs` | `upsertPublishingProviderConfig` | publishing:connect | yes | request:PublishingProviderConfigUpsertRequest<br>response:PublishingProviderConfigEnvelope |
| `GET /api/v1/publishing-releases/{releaseId}` | `getPublishingRelease` | publishing:read | no | response:PublishingReleaseEnvelope |
| `PATCH /api/v1/publishing-releases/{releaseId}` | `updatePublishingRelease` | publishing:write | no | request:PublishingReleaseUpdateRequest<br>response:PublishingReleaseEnvelope |
| `POST /api/v1/publishing-releases/{releaseId}/cancel` | `cancelPublishingRelease` | publishing:publish | yes | response:PublishingReleaseEnvelope |
| `POST /api/v1/publishing-releases/{releaseId}/description/generate` | `generatePublishingReleaseDescription` | publishing:write | yes | response:PublishingReleaseEnvelope |
| `POST /api/v1/publishing-releases/{releaseId}/publish` | `publishPublishingRelease` | publishing:publish | yes | response:PublishingReleaseEnvelope |
| `POST /api/v1/publishing-releases/{releaseId}/refresh-media` | `refreshPublishedReleaseMedia` | publishing:publish | yes | request:PublishingReleaseRefreshMediaRequest<br>response:PublishingReleaseEnvelope |
| `POST /api/v1/publishing-releases/{releaseId}/schedule` | `schedulePublishingRelease` | publishing:publish | yes | response:PublishingReleaseEnvelope |
| `GET /api/v1/publishing-releases/{releaseId}/targets` | `listPublishingReleaseTargets` | publishing:read | no | response:PublishingTargetList |
| `POST /api/v1/publishing-releases/{releaseId}/unpublish` | `unpublishPublishingRelease` | publishing:publish | yes | response:PublishingReleaseEnvelope |
| `GET /api/v1/publishing-series/{seriesId}` | `getPublishingSeries` | publishing:read | no | response:PublishingSeriesEnvelope |
| `PATCH /api/v1/publishing-series/{seriesId}` | `updatePublishingSeries` | publishing:write | no | request:PublishingSeriesUpdateRequest<br>response:PublishingSeriesEnvelope |
| `GET /api/v1/publishing-series/{seriesId}/analytics` | `listPublishingAnalyticsSnapshots` | publishing:analytics | no | response:PublishingAnalyticsList |
| `GET /api/v1/publishing-series/{seriesId}/destinations` | `listPublishingDestinations` | publishing:read | no | response:PublishingDestinationList |
| `POST /api/v1/publishing-series/{seriesId}/destinations` | `upsertPublishingDestination` | publishing:connect | yes | request:PublishingDestinationUpsertRequest<br>response:PublishingDestinationEnvelope |
| `GET /api/v1/publishing-series/{seriesId}/episodes` | `listShowEpisodes` | publishing:read | no | response:ShowEpisodeList |
| `POST /api/v1/publishing-series/{seriesId}/episodes` | `produceShowEpisode` | publishing:write | yes | response:ShowEpisodeProduce |
| `POST /api/v1/publishing-series/{seriesId}/generate-cover-art` | `generatePublishingSeriesCoverArt` | publishing:write | no | request:PublishingCoverArtGenerateRequest<br>response:PublishingCoverArtGenerateResponse |
| `GET /api/v1/publishing-series/{seriesId}/quote` | `getShowQuote` | publishing:read | no | response:ShowQuote |
| `GET /api/v1/publishing-series/{seriesId}/releases` | `listPublishingReleases` | publishing:read | no | response:PublishingReleaseList |
| `POST /api/v1/publishing-series/{seriesId}/releases` | `createPublishingRelease` | publishing:write | yes | request:PublishingReleaseCreateRequest<br>response:PublishingReleaseEnvelope |
| `POST /api/v1/publishing-series/{seriesId}/source-preview` | `previewShowSources` | publishing:read | no | response:ShowSourcePreview |
| `GET /api/v1/show-source-templates` | `listShowSourceTemplates` | publishing:read | no | response:ShowSourceCatalog |
| `GET /api/v1/story-projects/{projectId}/publishing-series` | `listPublishingSeries` | publishing:read | no | response:PublishingSeriesList |
| `POST /api/v1/story-projects/{projectId}/publishing-series` | `createPublishingSeries` | publishing:write | yes | request:PublishingSeriesCreateRequest<br>response:PublishingSeriesEnvelope |

## Scripts

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `POST /api/v1/scripts` | `uploadScript` | story:write | yes | request:ScriptUploadRequest<br>response:ScriptUploadResponse |
| `DELETE /api/v1/scripts/{scriptUploadId}` | `deleteScript` | story:write | no |  |
| `GET /api/v1/scripts/{scriptUploadId}` | `getScript` | story:read | no | response:WriterScriptEnvelope |
| `PATCH /api/v1/scripts/{scriptUploadId}` | `updateScript` | story:write | no | request:ScriptMetadataPatch<br>response:WriterScriptEnvelope |
| `PATCH /api/v1/scripts/{scriptUploadId}/content` | `writeScriptContent` | story:write | no | request:ScriptContentWriteRequest<br>response:ScriptContentWriteResponse |
| `GET /api/v1/scripts/{scriptUploadId}/export` | `exportScript` | story:read | no | response:ScriptExportResponse |
| `POST /api/v1/scripts/{scriptUploadId}/finish` | `finishDraft` | story:write | yes | request:FinishDraftRequest<br>response:FinishDraftResponse |
| `GET /api/v1/scripts/{scriptUploadId}/lines/{entryIndex}` | `getScriptLineTuning` | story:read | no | response:ScriptLineTuning |
| `PATCH /api/v1/scripts/{scriptUploadId}/lines/{entryIndex}` | `updateScriptLine` | story:write | no | request:ScriptLineEdit<br>response:ScriptLineTuning |
| `GET /api/v1/scripts/{scriptUploadId}/overrides` | `getScriptOverrides` | story:read | no | response:ScriptOverrides |
| `PATCH /api/v1/scripts/{scriptUploadId}/overrides` | `updateScriptOverrides` | story:write | no | request:ScriptOverridesPatch<br>response:ScriptOverrides |
| `GET /api/v1/scripts/{scriptUploadId}/share` | `getScriptShare` | story:read | no | response:ScriptShareState |
| `POST /api/v1/scripts/{scriptUploadId}/share` | `createScriptShare` | artifact:publish | no | request:CreateScriptShareRequest<br>response:CreateScriptShareResult |
| `POST /api/v1/scripts/{scriptUploadId}/table-read/characters/remove` | `removeScriptTableReadCharacter` | story:write | no | request:RemoveCharacterRequest<br>response:RemoveCharacterResult |
| `POST /api/v1/scripts/{scriptUploadId}/table-read/prepare` | `prepareScriptTableRead` | artifact:publish | yes | request:PrepareTableReadRequest<br>response:TableReadPrepareEnvelope |
| `POST /api/v1/scripts/{scriptUploadId}/table-read/retry-analysis` | `retryScriptTableReadAnalysis` | story:write | no | response:RetryCreativeAnalysisResult |
| `POST /api/v1/scripts/{scriptUploadId}/versions` | `uploadScriptVersion` | story:write | yes | request:ScriptVersionUploadRequest |
| `DELETE /api/v1/scripts/{scriptUploadId}/versions/{versionId}` | `deleteScriptVersion` | story:write | no | response:ScriptVersionDeleteResponse |

## Seasons

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `GET /api/v1/season-runs/{runId}` | `getStorySeasonRun` | story:read | no | response:SeasonRunEnvelope |
| `POST /api/v1/season-runs/{runId}/advance` | `advanceStorySeasonRun` | story:write, artifact:publish, publishing:write | yes | request:SeasonRunAdvanceRequest<br>response:SeasonRunEnvelope |
| `POST /api/v1/season-runs/{runId}/approve` | `approveStorySeasonMap` | story:write | yes | request:SeasonRunApprovalRequest<br>response:SeasonRunEnvelope |
| `PATCH /api/v1/season-runs/{runId}/episodes/{episodeId}` | `updateStorySeasonEpisode` | story:write | no | request:SeasonRunEpisodeUpdateRequest<br>response:SeasonRunEnvelope |
| `GET /api/v1/story-projects/{projectId}/season-runs` | `listStorySeasonRuns` | story:read | no | response:SeasonRunList |
| `POST /api/v1/story-projects/{projectId}/season-runs` | `createStorySeasonRun` | story:write, publishing:write | yes | request:SeasonRunCreateRequest<br>response:SeasonRunEnvelope |

## Sources

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `GET /api/v1/story-projects/{projectId}/captures` | `listAppCaptures` | source:read | no | response:AppCaptureList |
| `POST /api/v1/story-projects/{projectId}/captures` | `createAppCapture` | source:write | yes | request:AppCaptureCreate<br>response:AppCaptureCreateResult |
| `DELETE /api/v1/story-projects/{projectId}/captures/{captureId}` | `deleteAppCapture` | source:write | no | response:AppCaptureDeleted |
| `GET /api/v1/story-projects/{projectId}/captures/{captureId}` | `getAppCapture` | source:read | no | response:AppCaptureEnvelope |
| `POST /api/v1/story-projects/{projectId}/captures/{captureId}/finalize` | `finalizeAppCapture` | source:write | no | response:AppCaptureEnvelope |
| `GET /api/v1/story-projects/{projectId}/sources` | `listStorySources` | source:read | no | response:StorySourceList |
| `POST /api/v1/story-projects/{projectId}/sources` | `createStorySource` | source:write | yes | request:StorySourceCreate<br>response:StorySourceCreateResult |
| `DELETE /api/v1/story-projects/{projectId}/sources/{sourceId}` | `deleteStorySource` | source:write | no | response:DeletionAck |
| `GET /api/v1/story-projects/{projectId}/sources/{sourceId}` | `getStorySource` | source:read | no | response:StorySourceEnvelope |

## Table Read

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `GET /api/v1/table-read/performance-booth` | `getPerformanceBoothStatus` | story:read | no |  |
| `POST /api/v1/table-read/performance-booth` | `startPerformanceBoothSession` | story:write | yes |  |

## Trailer

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `DELETE /api/v1/trailers/{jobId}` | `deleteTrailer` | story:write | no |  |
| `PATCH /api/v1/trailers/{jobId}` | `renameTrailer` | story:write | no | request:ArtifactRenameRequest<br>response:RenamedVideo |
| `GET /api/v1/trailers/{jobId}/aspect` | `getTrailerAspect` | story:read | no |  |
| `POST /api/v1/trailers/{jobId}/aspect` | `setTrailerAspect` | story:write | yes |  |
| `POST /api/v1/trailers/{jobId}/audio-policy` | `setTrailerAudioPolicy` | story:write | yes |  |
| `GET /api/v1/trailers/{jobId}/beats` | `getTrailerBeats` | story:read | no | response:TrailerBeats |
| `POST /api/v1/trailers/{jobId}/beats/{beatIndex}/abandon-take` | `abandonTrailerTake` | artifact:publish | yes | request:TakeAbandonRequest<br>response:TakeAbandonResult |
| `POST /api/v1/trailers/{jobId}/beats/{beatIndex}/drop-line` | `dropTrailerBeatLine` | story:write | yes | request:TrailerDropLineRequest<br>response:TrailerDropLineAnswer |
| `POST /api/v1/trailers/{jobId}/beats/{beatIndex}/repair-take` | `repairTrailerTake` | artifact:publish | yes | request:TakeRepairRequest<br>response:TakeRepairAnswer |
| `POST /api/v1/trailers/{jobId}/beats/{beatIndex}/select-take` | `selectTrailerTake` | story:write | yes | request:TrailerSelectTakeRequest<br>response:TrailerSelectTakeAck |
| `POST /api/v1/trailers/{jobId}/beats/{beatIndex}/stage-voice-over` | `stageVoiceOverTrailerTake` | artifact:publish | yes | request:StageVoiceOverRequest<br>response:StageVoiceOverAnswer |
| `POST /api/v1/trailers/{jobId}/beats/{beatIndex}/take-repair-override` | `overrideTrailerTakeRepair` | story:write | yes | request:TakeRepairOverrideRequest<br>response:TakeRepairOverrideAck |
| `POST /api/v1/trailers/{jobId}/beats/{beatIndex}/trim-take` | `trimTrailerTake` | story:write | yes | request:TakeTrimRequest<br>response:TakeTrimQueued |
| `POST /api/v1/trailers/{jobId}/beats/refine` | `refineTrailerBeat` | story:write | yes | request:TrailerBeatRefineRequest<br>response:TrailerBeatPreview |
| `POST /api/v1/trailers/{jobId}/beats/render-clip` | `renderTrailerBeatClip` | artifact:publish | yes | request:TrailerRenderClipRequest<br>response:TrailerRenderAnswer |
| `GET /api/v1/trailers/{jobId}/camera` | `getTrailerCamera` | story:read | no | response:CameraView |
| `POST /api/v1/trailers/{jobId}/camera` | `setTrailerCamera` | story:write | yes | request:CameraSetRequest<br>response:CameraSetAck |
| `POST /api/v1/trailers/{jobId}/cancel-render` | `cancelTrailerRender` | artifact:publish | yes | response:RenderCancelResult |
| `POST /api/v1/trailers/{jobId}/closing-sequence` | `updateTrailerClosingSequence` | story:write | yes | request:TrailerClosingSequenceUpdateRequest<br>response:TrailerClosingSequenceUpdateAck |
| `POST /api/v1/trailers/{jobId}/duplicate` | `duplicateTrailer` | story:write | yes | request:TrailerDuplicateRequest<br>response:TrailerDuplicate |
| `POST /api/v1/trailers/{jobId}/finish` | `finishTrailer` | artifact:publish | yes | request:TrailerFinishRequest<br>response:TrailerFinishAnswer |
| `GET /api/v1/trailers/{jobId}/intent` | `getTrailerIntent` | story:read | no | response:VideoIntent |
| `POST /api/v1/trailers/{jobId}/intent` | `updateTrailerIntent` | story:write | yes | request:VideoIntentEditRequest<br>response:VideoIntentEditAck |
| `POST /api/v1/trailers/{jobId}/interludes` | `updateTrailerInterludes` | story:write | yes | request:TrailerInterludesUpdateRequest<br>response:TrailerInterludesUpdateAck |
| `GET /api/v1/trailers/{jobId}/look` | `getTrailerLook` | story:read | no | response:FinishView |
| `POST /api/v1/trailers/{jobId}/look` | `setTrailerLook` | story:write | yes | request:FinishSetRequest<br>response:FinishSetAck |
| `POST /api/v1/trailers/{jobId}/more-fixes` | `allowMoreTrailerFixes` | artifact:publish | yes | request:MoreFixesRequest<br>response:MoreFixesAnswer |
| `GET /api/v1/trailers/{jobId}/motion-blur` | `getTrailerMotionBlur` | story:read | no | response:MotionBlurView |
| `POST /api/v1/trailers/{jobId}/motion-blur` | `setTrailerMotionBlur` | story:write | yes | request:MotionBlurSetRequest<br>response:MotionBlurSetAck |
| `GET /api/v1/trailers/{jobId}/narrator` | `getTrailerNarrator` | story:read | no |  |
| `POST /api/v1/trailers/{jobId}/narrator` | `setTrailerNarrator` | story:write | yes |  |
| `POST /api/v1/trailers/{jobId}/plan/approve` | `approveTrailerPlan` | story:write | yes | request:TrailerPlanApproveRequest<br>response:TrailerPlanApproveAnswer |
| `GET /api/v1/trailers/{jobId}/quiet-track` | `getTrailerQuietTrack` | story:read | no | response:QuietTrackView |
| `POST /api/v1/trailers/{jobId}/quiet-track` | `setTrailerQuietTrack` | story:write | yes | request:QuietTrackSetRequest<br>response:QuietTrackSetAck |
| `POST /api/v1/trailers/{jobId}/reassemble` | `reassembleTrailer` | story:write | yes | response:RenderReassembleAck |
| `GET /api/v1/trailers/{jobId}/render-states` | `getTrailerRenderStates` | story:read | no | response:TrailerBeatRenderStates |
| `GET /api/v1/trailers/{jobId}/resolution` | `getTrailerResolution` | story:read | no |  |
| `POST /api/v1/trailers/{jobId}/resolution` | `setTrailerResolution` | story:write | yes |  |
| `GET /api/v1/trailers/{jobId}/score-to-picture` | `getTrailerScoreToPicture` | story:read | no |  |
| `POST /api/v1/trailers/{jobId}/score-to-picture` | `scoreTrailerToPicture` | artifact:publish | yes |  |
| `POST /api/v1/trailers/{jobId}/score-to-picture/revert` | `revertTrailerScoreToPicture` | story:write | yes |  |
| `GET /api/v1/trailers/{jobId}/storyboard/audio-timeline` | `getTrailerAudioTimeline` | story:read | no |  |
| `POST /api/v1/trailers/{jobId}/storyboard/audio-timeline` | `editTrailerAudioTimeline` | story:write | yes |  |
| `GET /api/v1/trailers/{jobId}/storyboard/audio` | `getTrailerSceneAudio` | story:read | no |  |
| `GET /api/v1/trailers/{jobId}/storyboard/coverage` | `getTrailerStoryboardCoverage` | story:read | no |  |
| `POST /api/v1/trailers/{jobId}/storyboard/coverage` | `generateTrailerStoryboardCoverage` | story:write | yes |  |
| `POST /api/v1/trailers/{jobId}/storyboard/direct-text-motion` | `directTrailerTextMotion` | story:write | yes | request:TrailerDirectTextMotionRequest<br>response:TrailerDirectTextMotionAck |
| `POST /api/v1/trailers/{jobId}/storyboard/insert-beat` | `insertTrailerBeat` | story:write | yes | request:TrailerInsertBeatRequest<br>response:TrailerBeatOrderAck |
| `POST /api/v1/trailers/{jobId}/storyboard/move-beat` | `moveTrailerBeat` | story:write | yes | request:TrailerMoveBeatRequest<br>response:TrailerBeatOrderAck |
| `POST /api/v1/trailers/{jobId}/storyboard/music` | `editTrailerSceneMusic` | story:write | yes |  |
| `POST /api/v1/trailers/{jobId}/storyboard/regenerate-text` | `regenerateTrailerText` | artifact:publish | yes | request:TrailerBeatIndexRequest<br>response:TrailerTextRenderAnswer |
| `POST /api/v1/trailers/{jobId}/storyboard/remove-beat` | `removeTrailerBeat` | story:write | yes | request:TrailerRemoveBeatRequest<br>response:TrailerBeatOrderAck |
| `POST /api/v1/trailers/{jobId}/storyboard/restore-take` | `restoreTrailerTake` | story:write | yes | request:TrailerRestoreTakeRequest<br>response:TrailerRestoreTakeAck |
| `POST /api/v1/trailers/{jobId}/storyboard/sfx` | `editTrailerSceneSfx` | story:write | yes |  |
| `GET /api/v1/trailers/{jobId}/storyboard/sound-map` | `getTrailerSoundMap` | story:read | no |  |
| `GET /api/v1/trailers/{jobId}/storyboard/takes` | `getTrailerBeatTakes` | story:read | no | response:TrailerBeatTakes |
| `GET /api/v1/trailers/{jobId}/versions` | `listTrailerVersions` | story:read | no | response:TrailerVersions |
| `GET /api/v1/trailers/{jobId}/versions/{number}` | `getTrailerVersion` | story:read | no | response:TrailerVersionRead |
| `POST /api/v1/trailers/{jobId}/versions/{number}/zip` | `requestTrailerVersionZip` | story:write | yes | response:VideoVersionZipAck |
| `GET /api/v1/trailers/{jobId}/video-coverage` | `getTrailerVideoCoverage` | story:read | no | response:VideoCoverageEnvelope |
| `POST /api/v1/trailers/{jobId}/video-coverage` | `generateTrailerVideoCoverage` | story:write | yes | request:VideoCoverageGenerateRequest<br>response:VideoCoverageEnvelope |
| `POST /api/v1/trailers/{jobId}/video-export` | `exportTrailerVideo` | story:write | yes | response:TrailerExportAck |

## Voices

| Endpoint | Operation ID | Scope | Idempotent | Schemas |
|---|---|---|---|---|
| `GET /api/v1/likeness-consents` | `listLikenessConsents` | story:read | no | response:LikenessConsentList |
| `POST /api/v1/likeness-consents/{consentId}/revoke` | `revokeLikenessConsent` | story:write | yes | response:StudioLikenessConsent |
| `POST /api/v1/likeness-consents/links` | `createLikenessConsentLink` | story:write | yes | response:LikenessConsentLinkResponse |
| `GET /api/v1/voice-migrations` | `listVoiceMigrations` | story:read | no | response:VoiceMigrationListResponse |
| `POST /api/v1/voice-migrations` | `migrateVoice` | story:write | yes | request:VoiceMigrationRequest<br>response:VoiceMigrationEnvelope |
| `POST /api/v1/voice-migrations/{migrationId}/undo` | `undoVoiceMigration` | story:write | yes | response:VoiceMigrationUndoResponse |
| `GET /api/v1/voices` | `listStudioVoices` | story:read | no | response:StudioVoiceListResponse |
| `DELETE /api/v1/voices/{voiceId}` | `deleteStudioVoice` | story:write | no | response:StudioVoiceDeleteResponse |
| `PATCH /api/v1/voices/{voiceId}` | `renameStudioVoice` | story:write | no | request:StudioVoiceRenameRequest<br>response:StudioVoiceRenameResponse |
| `POST /api/v1/voices/{voiceId}/consent-link` | `createVoiceConsentLink` | story:write | yes | response:VoiceConsentLinkResponse |
| `POST /api/v1/voices/{voiceId}/revoke-consent` | `revokeVoiceConsent` | story:write | yes | response:VoiceConsentRevokeResponse |
| `POST /api/v1/voices/clone-link` | `createVoiceCloneLink` | story:write | yes | response:VoiceCloneLinkResponse |
| `POST /api/v1/voices/preview` | `previewStudioVoice` | story:write | yes | request:VoicePreviewRequest<br>response:VoicePreviewResponse |
| `GET /api/v1/voices/pro/{jobId}` | `getProVoiceClone` | story:read | no | response:ProVoiceCloneStatus |
| `GET /api/v1/voices/pro/script` | `getProVoiceCloneScript` | story:read | no | response:ProVoiceCloneScript |
| `POST /api/v1/voices/pro/start` | `startProVoiceClone` | story:write | yes | request:ProVoiceCloneStartRequest<br>response:ProVoiceCloneStartResult |
| `POST /api/v1/voices/pro/train` | `startProVoiceCloneTraining` | story:write | yes | request:ProVoiceCloneTrainRequest<br>response:ProVoiceCloneTrainResult |

## Error codes

Every error response has the same shape: `{ "error": { "code", "message", "requestId" } }`. Match on `code`; the message text can change.

| Code | Meaning |
|---|---|
| `authentication_required` | No bearer API key was supplied in the Authorization header. |
| `invalid_api_key` | The API key is malformed, unknown, or does not match the stored secret. |
| `api_key_revoked` | The API key has been revoked by an account owner. |
| `api_key_expired` | The API key has passed its configured expiration timestamp. |
| `account_disabled` | The account that owns this API key has been disabled. |
| `ip_not_allowed` | The request originated from an IP that is not in the key allowlist. |
| `insufficient_scope` | The credential does not include the scope required for this endpoint. The endpoint exists — `error.details.missingScopes` names what is missing. On an OAuth connector, disconnect and sign in again approving that access; on an `sh_` key, mint a new key with those scopes. |
| `rate_limited` | The per-key + per-IP rate limit for this route family was exceeded. |
| `idempotency_key_required` | The endpoint requires an Idempotency-Key header for safe retries. |
| `idempotency_conflict` | A request with the supplied Idempotency-Key is already processing or used incompatible parameters. |
| `validation_failed` | The request payload failed validation. The error message names the offending field. |
| `project_not_found` | The story project does not exist, was deleted, or is owned by another account. |
| `source_not_found` | The story source does not exist, was deleted, or is owned by another account. |
| `source_type_unsupported` | The requested source type is not yet supported by this API phase. Check `/api/v1/capabilities`. |
| `source_too_large` | The source payload exceeds the documented size limit. See `details.maxBytes`. |
| `source_incomplete` | A source declared as full context failed its producer-specific completeness proof and was rejected before generation. |
| `source_fetch_failed` | A URL or PDF source could not be safely fetched (SSRF block, redirect limit, size limit, content-type mismatch, or upstream error). |
| `capture_not_found` | The app capture does not exist in this project, was deleted, or is owned by another account. |
| `capture_not_uploaded` | Finalize found no file for the capture yet: PUT the file to the capture's upload URL first (it works for an hour; after that, start a new capture). Nothing changed. |
| `story_plan_not_found` | The story plan does not exist, was deleted, or is owned by another account. |
| `story_plan_state_invalid` | The story plan is in a state that does not allow the requested operation. See `details.status` and `details.allowed`. |
| `story_plan_failed` | StoryPlan generation or refinement failed. See `details` for the underlying reason when available. |
| `season_run_not_found` | The season run does not exist, was deleted, or is owned by another account. |
| `season_run_state_invalid` | The season run or episode is not at the required lifecycle state. Reload it and follow `nextAction` before retrying. |
| `story_job_not_found` | The story job does not exist, was deleted, or is owned by another account. |
| `story_job_state_invalid` | The story job is in a state that does not allow the requested operation (e.g. canceling a job that has already reached a terminal state). See `details.status` and `details.allowed`. |
| `artifact_not_found` | The story artifact does not exist or is owned by another account. |
| `artifact_not_ready` | The artifact is not in a state where this operation is valid yet (e.g. requesting an MP4 render before the share link exists). See `details.status`. |
| `artifact_generation_failed` | The artifact adapter failed to produce a shareable output. The job and artifact rows stay inspectable; see the artifact `failureCode` / `failureMessage`. |
| `publishing_series_not_found` | The publishing series does not exist, was deleted, or is owned by another account. |
| `publishing_destination_not_found` | The publishing destination does not exist, was deleted, or is owned by another account. |
| `publishing_release_not_found` | The publishing release does not exist, was deleted, or is owned by another account. |
| `publishing_release_state_invalid` | The publishing release is in a state that does not allow the requested operation. See `details.status` and `details.allowed`. |
| `publishing_asset_not_ready` | The release does not have a finalized, ready asset for the requested publish operation. |
| `publishing_asset_stale` | The source artifact changed after the release asset was promoted. Finalize the current source before publishing. |
| `project_precondition_failed` | The project has not reached the development stage this call needs, so nothing was created, queued or charged. `details.stage` names the stage it has not finished (`premise`, `premise_coverage`, `planning`, `full_coverage`; `ready` when the stage is passed and a narrower precondition refused, such as a mood-board render with no image model bound), `details.missingFields` what that stage still lacks, and `details.canPlan` / `details.canStartEpisode` what the project can do now. Finish the named stage — do not retry: the identical call refuses the same way until it is done. `GET /story-projects/{projectId}` reports the same position as `project.workspaceGate` without attempting anything — read it first. |
| `spend_subject_unresolved` | The project gate could not find the artifact, screenplay version, episode or project this paid call is for under your account, so it refused instead of guessing — nothing was created, queued or charged. `details.subject` is the kind it looked for (`artifact`, `script_upload`, `script`, `project`) and `details.id` the id. Check the id; retrying the same one refuses the same way. |
| `insufficient_credits` | The account does not have enough Studio Credits to reserve this job. See `details.required` and `details.available`. A story job's refusal also carries `details.jobId`: null when the job was refused before it was created, or the id of the one job the Idempotency-Key is bound to — retrying that key after topping up resumes that job, it never creates another. |
| `internal_error` | The Sleeper Hit Story API failed unexpectedly. Retrying with the same Idempotency-Key is safe. |
| `trailer_not_found` | No trailer with that id, or it belongs to another account. |
| `trailer_state_invalid` | The trailer exists and the request is well-formed, but the trailer, its beat, its cast or its plates are not in a state that allows it — a plan approved outside `draft`, a take chosen or a beat rendered outside `review`/`complete`, an export before the trailer is finished, a finish while a call has no selected take, a narrated cut with no narrator, a re-voiced take selected (it is never played), a beat with no caption or footage-only when text was asked for, or a render the compile blocked. Nothing was reserved. A render refusal carries `details.refusal` = `{ code, action, fix }` — `action` is what unblocks it (`recast_figure`, `add_signature_items`, `recast_voice` or `remake_location_plate`) and `fix` names the MCP tool and CLI command that take it — plus `details.blocked[]`, one entry per beat the compile could not render. The identical request succeeds once the trailer is put right. |
| `pitch_deck_not_found` | No pitch deck with that id, or it belongs to another account. |
| `pitch_deck_state_invalid` | The deck exists and the request is well-formed, but the deck, its chapter, its cast or its plates are not in a state that allows it; `details.refusal` names the fix. State, not payload. |
| `pitch_deck_version_not_found` | The deck has no version with that number. Read its versions (`GET /pitch-decks/{jobId}/versions`) for the numbers it has; a version is kept each time the deck's master is published. |
| `trailer_version_not_found` | The trailer has no version with that number. Read its versions (`GET /trailers/{jobId}/versions`) for the numbers it has; a version is kept each time the trailer's master is published. |
| `story_job_not_resumable` | The job exists but has no resumable checkpoint — it either finished or failed past the point a resume can start from. |
| `script_not_found` | No screenplay with that id, or it belongs to another account. |
| `cast_precondition_failed` | A cast is not ready, so nothing was created, quoted or charged. On the casting step: nobody castable, nobody with a description, or no image model bound. On a table_read job: the project cast canon (+ the request's voiceMap) does not cover every character (`details.reason`: `no_cast_canon`, `cast_canon_collision`, `cast_canon_incomplete`), an adopted `scriptUploadId` is a draft or not cast (`script_not_finalized`, `script_not_cast_ready`), or a resumed job's own screenplay voices someone uncast (`episode_cast_incomplete`); `details.missing` lists each member and what it lacks (`voice`, and unless `details.audioOnly`, `face`, `body`, `sheet`). Complete the cast — do not retry: the identical call refuses the same way until it is done. `GET /story-projects/{projectId}` reports the canon half as `project.tableReadReadiness`. |
| `table_read_not_prepared` | The read's table read is not paid for yet, so nothing was started, rendered or charged. A live read, a durable audio render (finalize) and the read's sound effects and music wait until it is PREPARED — charged once per screenplay version: `POST /scripts/{scriptUploadId}/table-read/prepare` (MCP `prepare_table_read`, CLI `script table-read prepare`), or Continue to Table Read in the Writing Lab. A read made by a Story API job is paid by that job. Prepare it, then retry. |
| `table_read_not_failed` | The read's creative analysis is not in a FAILED state, so there is nothing to retry. Only a read whose analysis failed can be re-run (`POST /scripts/{scriptUploadId}/table-read/retry-analysis`, MCP `retry_creative_analysis`, CLI `script table-read retry-analysis`, or the Writing Lab failed card's Try Again). Nothing was changed or charged. |
| `voice_clone_not_found` | No voice clone with that id, or it belongs to another account. |
| `mood_board_full` | The mood board already holds the maximum number of frames that can bind a render. Drop one before keeping another. |
| `fal_account_locked` | The upstream generation provider has locked the studio account, usually for billing. Nothing was charged. |
| `location_plate_not_found` | The script stores no location plate at that key. Read the location plates (`GET /artifacts/{artifactId}/location-plates`) for the keys it does store; a plate the script has not made yet is made when a video renders. |
| `location_plate_precondition_failed` | The plate cannot be remade yet, and nothing was priced or charged: it is not ready, a remake is already running for it, the plate it is cropped from is missing, or no image model is bound. The message says which; the identical call succeeds once the plate is put right. |
| `voice_not_found` | No voice with that id in your library (`GET /voices` lists the ones you own), or a voice you may not use: another writer's cloned or designed voice is never previewed, cast, pinned or migrated onto. A catalog voice can be previewed and assigned, but only your own voices can be renamed or deleted. |
| `voice_design_not_found` | No cast voice design with that id in this project. `designId` comes from the answer of `POST /story-projects/{projectId}/cast-voice/design` for the same project. |
| `voice_in_use` | The voice is still pinned in a cast canon or spoken in a read, so it was not deleted. `details.uses` names where; repoint those first, or send the delete again with `confirmed: true` to delete it anyway (those characters will have no voice until they are recast). |
| `cast_voice_precondition_failed` | The read or project is not ready for this cast voice operation, and nothing was priced or charged: the character has no lines to speak, there is no earlier voice to restore, or the read has no finished recording to re-voice. The message says which and what to do first. |
| `script_in_use` | The script was not deleted: something live still depends on it. `details.uses` names each (a published or scheduled release, a running job, a season-run slot, a pitch deck or a trailer); unpublish, cancel or delete those first, then delete the script again. |
| `script_state_invalid` | The script's state refuses this change: the most common is deleting a script's only version, which leaves no script — delete the whole script instead (DELETE /scripts/{scriptId}). The message says which. |
| `plan_limit_reached` | Your plan does not allow this: an upload that would take you past the plan's Writers Lab page allowance (the message gives the pages in use and the pages this upload adds) or a second version of a script on the free plan, or a second screenplay coverage report on the free plan (it includes one). `details` carries the plan and the numbers. Upgrade the plan, or delete scripts you no longer need, then try again. |
| `usage_limit_reached` | This month's cap on your coverage reports (screenplay, Series Bible premise and full, outline) or table-read generations (scene music, scene atmosphere, backdrop prompts) is used up, so nothing was queued or charged. `details` carries `feature` (`COVERAGE` or `TABLE_READ`), `limit`, `used` and `resetsAt` (the first of next month, 00:00 UTC). It is state, not a rate: do not retry before `resetsAt` — the same call refuses the same way until then. |
| `cast_voice_casting_coverage_failed` | The candidate voice you chose FAILED its casting coverage (a model listened to the candidates and scored each against the brief), so nothing was cloned or pinned. `details.castingCoverage` carries its score, the bar, why (`flags`, `gateReasons`), the candidate it recommends instead and, when every candidate failed, a revised brief to design again from. Choose the recommendation, design again, or send `acknowledgeCastingCoverage: true` only once the writer has heard the failure and chosen it anyway. |
| `cast_portrait_casting_coverage_failed` | The headshot you chose FAILED its casting coverage (a model looked at the candidates and scored each against the look and the Series Bible), so nothing was pinned. `details.castingCoverage` carries its score, the bar, why (`flags`, `gateReasons`), the headshot it recommends instead and, when every one failed, a revised look to draw again from. Choose the recommendation, draw again, or send `acknowledgeCastingCoverage: true` only once the writer has seen the failure and chosen it anyway. |
| `voice_retired` | A voice its provider has retired (it can no longer speak) is in the way, and nothing was queued, charged or changed. A finalize, export, render, live read, re-voice or preview that would speak it refuses before anything is synthesized; a cast, pin or choose refuses to add it; a migration refuses a replacement whose provider is retiring or retired. `details.voices` names each voice (`character`, `voiceId`, `voiceName`, `provider`, `retiresAt`). Migrate those voices to a new voice (`POST /voice-migrations`; MCP `migrate_voice`; CLI `voices migrate`), then retry. |
| `voice_consent_required` | A CLONED voice has no consent on record from the person whose voice it is (it was made before consent records began), so nothing was cast, pinned, previewed, spoken or charged. `details.voices` names each (`voiceId`, `name`). Mint a consent link (`POST /voices/{voiceId}/consent-link`; MCP `create_voice_consent_link`; CLI `voices consent <voiceId>`) and give it to that person; once they confirm, retry. A Pro clone with no consent from any recording session cannot be trained. |
| `voice_consent_revoked` | The speaker of a cloned voice revoked their consent, so the voice is never used for anything new (work already published stays up). Nothing was cast, pinned, previewed, spoken, recorded or charged. `details.voices` names each (`voiceId`, `name`, `revokedAt`). Choose another voice; revoking is final. |
| `account_email_mismatch` | `confirmEmail` is not this account's email address, so no deletion was scheduled. Ask the account owner for the address on the account. |
| `account_deletion_refused` | This account cannot delete itself (`message` says why): it is an admin account, it hosts workshop cohorts other members belong to, or it owns a live organization (transfer or delete it first). Also returned when cancelling a deletion whose purge has already started. Nothing was changed. |
| `project_not_restorable` | The project cannot be restored: it is not deleted, its 30-day restore window has passed, or its purge has started (`message` says which). Its episodes went private with the project and are deleted with it at the purge. |
| `content_refused` | The request asks for, or what would be published or shared contains, content our rules never allow (sexual content involving anyone under 18, or a real person, however long ago they lived, shown in a sexual or intimate way), so nothing was made, saved, published, shared or charged. It is not retried. Rephrase or remove that content. |
| `safety_hold` | Publishing and sharing are paused on this account while a review is open, so nothing was published or shared. Writing is not affected. Write to help@sleeperhit.studio if you think this is a mistake. |
| `safety_check_unavailable` | The content safety check that runs before anything is published or shared could not run, so nothing was published or shared. Retry in a few minutes; a queued publish retries on its own. |
| `likeness_consent_required` | The picture or video would show a real, identifiable person who is living or died within the last 70 years (`details.people` names them). A real person's likeness needs their approval, or their estate's, so nothing was made or charged. Mint a link for them to approve (POST /likeness-consents/links; the account holder approves their own likeness in the dashboard), then retry; or describe a fictional character instead. Fictional characters, including ones who share a name with real people, and long-dead historical figures are not affected, and nobody real is ever shown in a sexual or intimate way. |
| `reference_image_not_owned` | A face or likeness reference (a portrait or body reference, a cast canon picture, a roster portrait) must be a picture stored in your own account: one you uploaded, or one made in your own projects. A link to anywhere else, or to another writer's picture, is refused (`details.field` names it), and nothing was changed or charged. Use a picture from your library or one of your cast's portraits. |
| `likeness_consent_not_found` | No likeness approval with that id belongs to this account. List them with GET /likeness-consents. |
| `character_not_found` | Nobody in the project's cast canon or Series Bible answers to that name (any name or alias counts). Read `GET /story-projects/{projectId}/cast-look` for the people the project has, or add the character to the Series Bible first. Nothing was changed or charged. |
| `portrait_design_not_found` | No portrait design with that id in this project. `designId` comes from `POST /story-projects/{projectId}/cast-portraits/design` (confirmed) or `GET .../cast-portraits/designs` for the same project. |
| `series_bible_version_conflict` | The Series Bible changed since the version this edit was based on (`details.expectedVersion` vs `details.currentVersion`), so nothing was saved. Read the character again and send the edit with the current version, or omit `expectedVersion` to edit whatever is saved now. |
| `show_cap_reached` | A recurring show's next paid step would cross one of its spending caps, so it was not taken: `details.scope` is `episode` (the episode cap), `month` (the monthly cap, in the series' timezone) or `unset` (the show has no caps, so it spends nothing). `details.cap`, `details.credits` (the step's quote) and `details.wouldSpend` give the numbers. Raise the cap with the creator (`PATCH /publishing-series/{seriesId}` `episodeCreditCap` / `monthlyCreditCap`) or wait for the next month; do not retry — the same step refuses the same way. |
| `not_a_sign_in_key` | A sign-out (`DELETE /device/credential`) was made with a credential no browser sign-in minted — a key made on the API page, or a connector's OAuth grant — so nothing was revoked and the credential keeps working. Revoke a key, or disconnect a connector, on the API page (`/dashboard/api`). |
| `not_found` | No endpoint at that path. This is a routing problem, not an authentication or method one — check the path against the API reference. |
| `method_not_allowed` | The path exists but does not accept that HTTP method. The `Allow` response header lists the methods it does accept. |
