{"version":"v1","baseUrl":"https://docs.sleeperhit.studio/api/v1","authentication":{"header":"Authorization","format":"Bearer sh_<prefix>_<secret>","obtainAt":"https://docs.sleeperhit.studio/dashboard/developers"},"errorCodes":["authentication_required","invalid_api_key","api_key_revoked","api_key_expired","account_disabled","ip_not_allowed","insufficient_scope","rate_limited","idempotency_key_required","idempotency_conflict","validation_failed","project_not_found","source_not_found","source_type_unsupported","source_too_large","source_incomplete","source_fetch_failed","capture_not_found","capture_not_uploaded","capture_job_not_found","app_connection_not_found","app_connection_limit_reached","app_connection_sealing_unavailable","app_connection_test_limited","app_connection_test_changed","video_destination_not_found","video_destination_unavailable","video_deliverable_not_found","deliverable_script_version_conflict","admin_required","role_switch_not_enabled","product_kind_not_for_role","organization_required","organization_member_not_found","organization_member_elsewhere","organization_member_not_business","organization_forbidden","integration_not_found","integration_forbidden","integration_unavailable","project_read_only","project_owner_only","project_members_product_only","project_member_not_found","demo_account_not_allowed","demo_session_not_found","demo_session_credential_refused","unlimited_credits_not_allowed","story_plan_not_found","story_plan_state_invalid","story_plan_failed","season_run_not_found","season_run_state_invalid","story_job_not_found","story_job_state_invalid","artifact_not_found","artifact_not_ready","artifact_generation_failed","publishing_series_not_found","publishing_destination_not_found","publishing_release_not_found","publishing_release_state_invalid","publishing_asset_not_ready","publishing_asset_stale","project_precondition_failed","spend_subject_unresolved","insufficient_credits","internal_error","trailer_not_found","trailer_state_invalid","pitch_deck_not_found","pitch_deck_state_invalid","pitch_deck_version_not_found","trailer_version_not_found","story_job_not_resumable","script_not_found","cast_precondition_failed","table_read_not_prepared","table_read_not_failed","voice_clone_not_found","mood_board_full","fal_account_locked","location_plate_not_found","location_plate_precondition_failed","voice_not_found","voice_design_not_found","voice_in_use","cast_voice_precondition_failed","script_in_use","capture_in_use","script_state_invalid","plan_limit_reached","usage_limit_reached","cast_voice_casting_coverage_failed","cast_portrait_casting_coverage_failed","voice_retired","voice_consent_required","voice_consent_revoked","account_email_mismatch","account_deletion_refused","project_not_restorable","project_merge_refused","content_refused","safety_hold","safety_check_unavailable","likeness_consent_required","reference_image_not_owned","likeness_consent_not_found","character_not_found","portrait_design_not_found","series_bible_version_conflict","show_cap_reached","spend_cap_reached","credit_billing_invalid","project_kind_locked","project_kind_conflict","project_not_story","project_not_product","show_made_outside","show_item_already_aired","show_episode_not_queued","show_episode_not_found","show_not_ready","claim_not_found","not_a_sign_in_key","not_found","method_not_allowed","tour_not_found","tour_revision_conflict","tour_limit_reached","tour_draft_not_found","product_video_plan_not_found","product_video_no_captures","product_video_not_ready","references_unavailable","reference_not_found","reference_not_chosen","reference_check_no_master","reference_allowance_reached","strategy_item_not_found","strategy_unavailable","strategy_watch_not_runnable"],"examples":[{"id":"discover-capabilities","title":"Discover supported features before integrating","status":"available","scopes":["story:read"],"description":"Read the machine-readable capability manifest to see which sources, artifacts, scopes, and docs are available right now. Use this before writing any flow-specific code so your agent adapts as new features ship.","whenToUse":"First call from any new integration, and after any unexpected `*_unsupported` error from a generation endpoint.","steps":[{"title":"GET /capabilities","description":"Returns the capability manifest. Cache it for at most a minute — availability moves as new artifact adapters launch.","request":{"method":"GET","path":"/capabilities","headers":{"Authorization":"Bearer sh_<prefix>_<secret>"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/capabilities \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\"","node":"const res = await fetch('https://docs.sleeperhit.studio/api/v1/capabilities', {\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}` },\n})\nif (!res.ok) throw new Error(`capabilities ${res.status}`)\nconst capabilities = await res.json()"}]},{"id":"check-credits","title":"Check Studio Credit balance before generating","status":"available","scopes":["credits:read"],"description":"Read the current Studio Credit balance so the agent can short-circuit before triggering an expensive job that would fail with `insufficient_credits`.","whenToUse":"Before any generation step (once those endpoints are live), and on any `insufficient_credits` error to confirm the new balance.","steps":[{"title":"GET /credits","description":"Returns the available credit balance, monthly bucket usage, and the active cycle window.","request":{"method":"GET","path":"/credits","headers":{"Authorization":"Bearer sh_<prefix>_<secret>"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/credits \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\"","node":"const res = await fetch('https://docs.sleeperhit.studio/api/v1/credits', {\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}` },\n})\nif (!res.ok) throw new Error(`credits ${res.status}`)\nconst { credits } = await res.json()\nif (credits.balance < estimatedQuote) {\n  // Surface insufficient credits to the operator before reserving.\n}"}]},{"id":"handle-errors","title":"Recover from auth, rate limit, and idempotency errors","status":"available","scopes":[],"description":"Every error response uses the same envelope: `{ error: { code, message, requestId } }`. Match on `code`, not the human message. Surface `requestId` when reporting integration issues — the API logs the same id.","whenToUse":"Wrap every API call. The same recovery rules apply to every endpoint in this spec.","steps":[{"title":"Match on the stable error code","description":"Codes that warrant immediate action: `authentication_required` and `invalid_api_key` (rotate the key), `api_key_revoked`/`api_key_expired` (issue a new key), `insufficient_scope` (the endpoint exists but the credential is too narrow — read `error.details.missingScopes`, then reconnect the OAuth connector approving that access, or mint an `sh_` key with those scopes), `rate_limited` (respect `Retry-After`), `idempotency_conflict` (regenerate the Idempotency-Key or wait for completion).","request":{"method":"GET","path":"/capabilities","headers":{"Authorization":"Bearer sh_<prefix>_<secret>"}},"curl":"curl -i https://docs.sleeperhit.studio/api/v1/capabilities \\\n  -H \"Authorization: Bearer sh_invalid\"","node":"const res = await fetch('https://docs.sleeperhit.studio/api/v1/capabilities', {\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}` },\n})\nif (!res.ok) {\n  const { error } = await res.json().catch(() => ({ error: { code: 'unknown' } }))\n  switch (error.code) {\n    case 'authentication_required':\n    case 'invalid_api_key':\n    case 'api_key_revoked':\n    case 'api_key_expired':\n      // Rotate the key in your secret manager and stop the run.\n      throw new Error(error.message)\n    case 'insufficient_scope':\n      // Ask the operator to issue a key with the listed scope.\n      throw new Error(error.message)\n    case 'rate_limited': {\n      const retryAfter = Number(res.headers.get('Retry-After') ?? '5')\n      await new Promise((r) => setTimeout(r, retryAfter * 1000))\n      return retry()\n    }\n    default:\n      throw new Error(`${error.code}: ${error.message} (request ${error.requestId})`)\n  }\n}","responseExample":{"error":{"code":"invalid_api_key","message":"The API key is invalid.","requestId":"7f9d4b6a-4f0a-4a8c-9c2e-d5b3b8e6a0f1"}}}]},{"id":"create-project-and-attach-text-source","title":"Create a project and attach an inline text source","status":"available","scopes":["story:write","source:write"],"description":"Two-step flow: POST the project, then POST one or more sources to it. Inline text and markdown are accepted today and extract synchronously (source returns with `status: \"READY\"`). URL and PDF source types come in follow-on phases.","whenToUse":"Any time the agent has a body of text or markdown to register against a project before later generation phases land.","steps":[{"title":"POST /story-projects","description":"Create the workspace. Idempotency-Key is required so retries are safe.","request":{"method":"POST","path":"/story-projects","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"name":"Launch storytelling","description":"Q3 product launch"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/story-projects \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"Launch storytelling\",\"description\":\"Q3 product launch\"}'","node":"import { randomUUID } from 'node:crypto'\n\nconst projectRes = await fetch('https://docs.sleeperhit.studio/api/v1/story-projects', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`,\n    'Idempotency-Key': randomUUID(),\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({ name: 'Launch storytelling', description: 'Q3 product launch' }),\n})\nif (!projectRes.ok) throw new Error(`project create ${projectRes.status}`)\nconst { project } = await projectRes.json()","responseExample":{"project":{"id":"cmp...","name":"Launch storytelling","description":"Q3 product launch","metadata":null,"archivedAt":null,"sourceCount":0,"createdAt":"2026-05-28T15:00:00.000Z","updatedAt":"2026-05-28T15:00:00.000Z"}}},{"title":"POST /story-projects/{projectId}/sources","description":"Attach inline text. `type: \"markdown\"` is also accepted. Response includes `status: \"READY\"` immediately for inline content, plus contentHash, byteSize, tokenEstimate, and the first 4000 characters of extracted text as a preview. Add `metadata: { sourceContextMode: \"full\" }` when both planning and final artifact writing must consume the complete hash-verified retained source rather than the digest + preview. A producer that sends one source per item (an hourly show covering one thread each) adds `producer` + `externalId`; resending the same item returns the source already stored with `deduplicated: true`.","request":{"method":"POST","path":"/story-projects/{projectId}/sources","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Content-Type":"application/json"},"body":{"type":"text","label":"press-release-draft.md","content":"<the body of the source>","metadata":{"sourceContextMode":"full"}}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/story-projects/$PROJECT_ID/sources \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"type\":\"text\",\"label\":\"press-release\",\"content\":\"...\"}'","node":"const sourceRes = await fetch(`https://docs.sleeperhit.studio/api/v1/story-projects/${project.id}/sources`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({ type: 'text', label: 'press-release', content: bodyText }),\n})\nif (!sourceRes.ok) throw new Error(`source create ${sourceRes.status}`)\nconst { source } = await sourceRes.json()","responseExample":{"source":{"id":"cmq...","projectId":"cmp...","type":"TEXT","label":"press-release","status":"READY","retentionPolicy":"EXTRACTED_ONLY","byteSize":4123,"tokenEstimate":1031,"contentHash":"a1b2c3...","extractedTextPreview":"<first 4000 chars>","producer":null,"externalId":null,"originatedAt":null},"deduplicated":false}}]},{"id":"project-series-bible","title":"Read, save, or generate a project Series Bible","status":"available","scopes":["story:read","story:write"],"description":"The Series Bible is the durable project-level canon shared by the web UI, Story API, CLI, and MCP. Treat a full bible as the preferred non-blocking gate before screenplay generation so format, episode maps, characters, style, and audio direction stay consistent.","whenToUse":"When an agent is planning a series, building an episode arc, preparing the first screenplay-backed table read, saving a user-authored bible, or refreshing project memory from sources/plans/scripts.","steps":[{"title":"POST /story-projects/{projectId}/series-bible","description":"Generate or refresh the bible from project context through DB sidecar `story_api_series_bible`. Use PATCH instead when you already have caller-authored content to save.","request":{"method":"POST","path":"/story-projects/{projectId}/series-bible","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"instructions":"Build a 10-episode animated comedy bible with a clear pilot, season arc, character roster, and audio style."}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/story-projects/$PROJECT_ID/series-bible \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"instructions\":\"Build a 10-episode animated comedy bible with a clear pilot, season arc, character roster, and audio style.\"}'","node":"import { randomUUID } from 'node:crypto'\n\nconst bibleRes = await fetch(`https://docs.sleeperhit.studio/api/v1/story-projects/${projectId}/series-bible`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`,\n    'Idempotency-Key': randomUUID(),\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({ instructions: 'Refresh the 10-episode arc and character roster.' }),\n})\nif (!bibleRes.ok) throw new Error(`series bible ${bibleRes.status}`)\nconst { document } = await bibleRes.json()","responseExample":{"document":{"id":"cmdoc...","projectId":"cmp...","kind":"series_bible","title":"Series Bible","generatedFrom":null,"currentVersion":2,"createdAt":"2026-06-07T12:00:00.000Z","updatedAt":"2026-06-07T12:34:56.000Z","content":{"logline":"<one-sentence series premise>","format":"10 x 22-minute animated comedy","genre":"","audience":"","tone":"","premise":"","seasonArc":"","themes":"","visualStyle":"","audioStyle":"","writingStyle":"","worldRules":"","canonTimeline":"","openQuestions":"","notes":"","episodes":[{"id":"episode-1","label":"Episode 1","title":"Pilot","summary":"<summary>","status":"planned","seasonNumber":1}],"seasons":[{"number":1,"arc":"<what happens in season 1>"}],"characters":[{"id":"character-1","name":"Sam","role":"<role>","want":"<want>","wound":"<wound>","arc":"<arc>","voice":"<voice>","relationships":"<relationships>"}]}}}},{"title":"POST, then poll /story-projects/{projectId}/series-bible/coverage","description":"Queue the exact-revision foundational report. Poll with GET until complete or failed; for series work, revise the Bible until current=true and gatePassed=true before relying on individual script coverage.","request":{"method":"POST","path":"/story-projects/{projectId}/series-bible/coverage","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"focusPrompt":"Pressure-test the thematic differentiator, showing strategy, and audio scene engine."}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/story-projects/$PROJECT_ID/series-bible/coverage \\\n  -X POST \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"focusPrompt\":\"Pressure-test the thematic differentiator, showing strategy, and audio scene engine.\"}'","node":"import { randomUUID } from 'node:crypto'\n\nconst coverageRes = await fetch(`https://docs.sleeperhit.studio/api/v1/story-projects/${projectId}/series-bible/coverage`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`,\n    'Idempotency-Key': randomUUID(),\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({ focusPrompt: 'Pressure-test the audio scene engine.' }),\n})\nif (!coverageRes.ok) throw new Error(`bible coverage ${coverageRes.status}`)\nconst { coverage } = await coverageRes.json()","responseExample":{"coverage":{"status":"generating","current":true,"stale":false,"gatePassed":false,"gateThreshold":7,"reportId":"cmbiblecov...","documentVersion":2,"reportVersion":2,"overallScore":null,"progressDetail":"Queued for foundational analysis...","failureMessage":null,"report":null,"exemplarIds":[],"createdAt":"2026-07-18T12:00:00.000Z","updatedAt":"2026-07-18T12:00:00.000Z"}}}]},{"id":"generate-story-plan","title":"Create a StoryPlan from sources and poll until terminal","status":"available","scopes":["story:write","story:read"],"description":"POST a StoryPlan with target (audience + objective + outcome), artifactRequests, creativeBrief, and sourceIds. Non-blocking warning gate: before a screenplay/table_read plan, complete or inspect the project Series Bible and review the generated plan against it. The planner auto-loads the saved Series Bible as canonical project truth. The API returns immediately with `status: PENDING` and a preliminary quote. The planner runs async via DB sidecar `story_api_plan`. Table-read plans land at `REQUIRES_APPROVAL` with a scriptBlueprint to review; they cannot use autoApprove and generate one script at a time.","whenToUse":"After each sourceId you will pass has status READY, ideally with a digest in hand, and after the Series Bible has been reviewed for screenplay-backed work. The planner will use source digests when available and fall back to extracted-text previews.","steps":[{"title":"POST /story-projects/{projectId}/story-plans","description":"Synchronous validation: target shape, artifact request shape, and sourceIds owned by this project with status READY. Worker runs async.","request":{"method":"POST","path":"/story-projects/{projectId}/story-plans","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"target":{"audience":"enterprise buyers","objective":"explain the Q3 product launch","outcome":"book a demo within 7 days","tone":"confident, specific, executive"},"artifactRequests":[{"type":"pitch_deck","durationSeconds":45,"includePdf":true},{"type":"trailer","durationSeconds":15,"aspectRatio":"9:16"}],"sourceIds":["<source-id-1>","<source-id-2>"],"autoApprove":false}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/story-projects/$PROJECT_ID/story-plans \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"target\":{\"audience\":\"enterprise buyers\",\"objective\":\"explain the Q3 product launch\",\"outcome\":\"book a demo within 7 days\"},\"artifactRequests\":[{\"type\":\"pitch_deck\",\"durationSeconds\":45,\"includePdf\":true}],\"sourceIds\":[]}'","node":"import { randomUUID } from 'node:crypto'\n\nconst planRes = await fetch(`https://docs.sleeperhit.studio/api/v1/story-projects/${projectId}/story-plans`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`,\n    'Idempotency-Key': randomUUID(),\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    target: {\n      audience: 'enterprise buyers',\n      objective: 'explain the Q3 product launch',\n      outcome: 'book a demo within 7 days',\n    },\n    artifactRequests: [{ type: 'pitch_deck', durationSeconds: 45, includePdf: true }],\n    sourceIds,\n    autoApprove: false,\n  }),\n})\nif (!planRes.ok) throw new Error(`plan create ${planRes.status}`)\nconst { plan } = await planRes.json()"},{"title":"Poll the plan until terminal","description":"Terminal states: `REQUIRES_APPROVAL`, `READY`, `APPROVED`, `REJECTED`, `FAILED`. For table_read, inspect `plan.plan.scriptBlueprint` before approval: page target, narration policy, scene outline, cast, music, and SFX direction.","request":{"method":"GET","path":"/story-plans/{planId}","headers":{"Authorization":"Bearer sh_<prefix>_<secret>"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/story-plans/$PLAN_ID \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\"","node":"async function poll(planId) {\n  while (true) {\n    const res = await fetch(`https://docs.sleeperhit.studio/api/v1/story-plans/${planId}`, {\n      headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}` },\n    })\n    if (!res.ok) throw new Error(`poll ${res.status}`)\n    const { plan } = await res.json()\n    if (['REQUIRES_APPROVAL', 'READY', 'APPROVED', 'REJECTED', 'FAILED'].includes(plan.status)) return plan\n    await new Promise((r) => setTimeout(r, 3000))\n  }\n}"},{"title":"Approve the plan","description":"Required only when `autoApprove: false`. This is a mandatory human hold: send userConfirmed=true only after the user reviews and approves the exact plan. Idempotent — re-approving an already-approved plan is a no-op.","request":{"method":"POST","path":"/story-plans/{planId}/approve","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Content-Type":"application/json","Idempotency-Key":"<stable-uuid>"},"body":{"userConfirmed":true}},"curl":"curl -s -X POST https://docs.sleeperhit.studio/api/v1/story-plans/$PLAN_ID/approve \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: <stable-uuid>\" \\\n  --data '{\"userConfirmed\":true}'","node":"await fetch(`https://docs.sleeperhit.studio/api/v1/story-plans/${plan.id}/approve`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`,\n    'Content-Type': 'application/json',\n    'Idempotency-Key': crypto.randomUUID(),\n  },\n  body: JSON.stringify({ userConfirmed: true }),\n})"}]},{"id":"ingest-pdf-source","title":"Ingest a PDF source (async, polled)","status":"available","scopes":["source:write","source:read"],"description":"POST a `pdf` source with a public URL pointing at the file. Runs through the same SSRF + redirect + size guards as `url` sources, plus the upstream Content-Type must be `application/pdf`. Worker extracts text with pdfjs and writes `metadata.pageCount`.","whenToUse":"When the agent has a public PDF (white paper, research report, board deck) it wants to register as source material.","steps":[{"title":"POST a pdf source","description":"Returns `status: PENDING`. Poll the source by id until terminal.","request":{"method":"POST","path":"/story-projects/{projectId}/sources","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Content-Type":"application/json"},"body":{"type":"pdf","uri":"https://example.com/whitepaper.pdf","label":"Q3 whitepaper"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/story-projects/$PROJECT_ID/sources \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"type\":\"pdf\",\"uri\":\"https://example.com/whitepaper.pdf\"}'","node":"const res = await fetch(`https://docs.sleeperhit.studio/api/v1/story-projects/${project.id}/sources`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({ type: 'pdf', uri: 'https://example.com/whitepaper.pdf' }),\n})\nif (!res.ok) throw new Error(`pdf source create ${res.status}`)\nconst { source } = await res.json()","responseExample":{"source":{"id":"cmq...","projectId":"cmp...","type":"PDF","uri":"https://example.com/whitepaper.pdf","status":"READY","byteSize":124500,"tokenEstimate":8421,"contentHash":"b2c3d4...","extractedTextPreview":"<first 4000 chars>","metadata":{"pageCount":12}}}}]},{"id":"ingest-url-source","title":"Ingest a URL source (async, polled)","status":"available","scopes":["source:write","source:read"],"description":"POST a `url` source, then poll until `status: READY` (or `FAILED`). The fetch happens in a pg-boss worker behind SSRF, redirect, content-type, and size guards. Source `failureCode` and `failureMessage` describe rejection reasons (e.g. `source_fetch_failed` with `details.reason = \"ip_blocked\"`).","whenToUse":"When the agent has a public URL whose extracted text should be available for later planning. Use only on URLs reachable from the public internet — private/cloud-metadata addresses are blocked.","steps":[{"title":"POST a url source","description":"Returns immediately with `status: \"PENDING\"`.","request":{"method":"POST","path":"/story-projects/{projectId}/sources","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Content-Type":"application/json"},"body":{"type":"url","uri":"https://example.com/article","label":"launch article"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/story-projects/$PROJECT_ID/sources \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"type\":\"url\",\"uri\":\"https://example.com/article\",\"label\":\"launch article\"}'","node":"const res = await fetch(`https://docs.sleeperhit.studio/api/v1/story-projects/${project.id}/sources`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({ type: 'url', uri: 'https://example.com/article', label: 'launch article' }),\n})\nif (!res.ok) throw new Error(`source create ${res.status}`)\nconst { source } = await res.json()"},{"title":"Poll until terminal","description":"Poll once every 2–5 seconds. Terminal states are `READY` (extracted text + content hash + token estimate available) and `FAILED` (see `failureCode`).","request":{"method":"GET","path":"/story-projects/{projectId}/sources/{sourceId}","headers":{"Authorization":"Bearer sh_<prefix>_<secret>"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/story-projects/$PROJECT_ID/sources/$SOURCE_ID \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\"","node":"async function poll(projectId, sourceId) {\n  while (true) {\n    const res = await fetch(`https://docs.sleeperhit.studio/api/v1/story-projects/${projectId}/sources/${sourceId}`, {\n      headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}` },\n    })\n    if (!res.ok) throw new Error(`poll ${res.status}`)\n    const { source } = await res.json()\n    if (source.status === 'READY' || source.status === 'FAILED') return source\n    await new Promise((r) => setTimeout(r, 3000))\n  }\n}","responseExample":{"source":{"id":"cmq...","projectId":"cmp...","type":"URL","uri":"https://example.com/article","status":"READY","byteSize":8421,"tokenEstimate":2106,"contentHash":"a1b2c3...","extractedTextPreview":"<first 4000 chars>","extractedAt":"2026-05-28T15:10:00.000Z"}}}]},{"id":"refine-table-read","title":"Refine a table read artifact with a free-form instruction","status":"available","scopes":["artifact:publish","story:read","artifact:read"],"description":"POST a conversational instruction to revise the screenplay, recast characters, or adjust tone without creating a new artifact. Share URLs remain stable. TWO CALLS: without `confirmed` it returns the price and reserves and enqueues NOTHING; with `confirmed: true` it returns the `storyJob` to poll. TABLE_READ_REFINE (8 credits) is reserved only when confirmed.","whenToUse":"After a `table_read` artifact is `READY` and you want to adjust cast or tone without re-running the full plan-to-job flow. Requires scope `artifact:publish`.","steps":[{"title":"POST /artifacts/{artifactId}/refine","description":"Supply a free-form `instruction`. `scope`: `\"auto\"` (default), `\"screenplay\"` (cast + screenplay only), or `\"plan\"` (re-grounds against sources first). Omit `confirmed` to price the revision (nothing reserved); pass `confirmed: true` to reserve exactly the quote and create the job. Requires `Idempotency-Key`.","request":{"method":"POST","path":"/artifacts/{artifactId}/refine","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"instruction":"rename the host to Dana and drop the narrator; make the skeptic warmer","scope":"screenplay"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/artifacts/$ARTIFACT_ID/refine \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"instruction\":\"rename the host to Dana and drop the narrator; make the skeptic warmer\",\"scope\":\"screenplay\"}'","node":"import { randomUUID } from 'node:crypto'\nconst refineRes = await fetch(`https://docs.sleeperhit.studio/api/v1/artifacts/${artifactId}/refine`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  body: JSON.stringify({ instruction: 'rename the host to Dana; make the skeptic warmer', scope: 'screenplay' }),\n})\nif (!refineRes.ok) throw new Error(`refine ${refineRes.status}`)\nconst { storyJob } = await refineRes.json()","responseExample":{"storyJob":{"id":"cmj...","status":"RESERVED"}}},{"title":"Poll GET /story-jobs/{jobId} until terminal","description":"Terminal states: `READY`, `FAILED`. On `READY`, GET /artifacts/{artifactId} shows updated `currentVersion` + `refineCount`. Fetch prior revisions with `?revision=N`.","request":{"method":"GET","path":"/story-jobs/{jobId}","headers":{"Authorization":"Bearer sh_<prefix>_<secret>"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/story-jobs/$JOB_ID \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\"","node":"// Poll until refine job is terminal\nwhile (true) {\n  const { job } = await fetch(`https://docs.sleeperhit.studio/api/v1/story-jobs/${storyJob.id}`, {\n    headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}` },\n  }).then((r) => r.json())\n  if (['READY', 'FAILED', 'CANCELED'].includes(job.status)) {\n    if (job.status !== 'READY') throw new Error(`refine ${job.status}`)\n    break\n  }\n  await new Promise((r) => setTimeout(r, 6000))\n}\n// Original artifact id + share URLs are unchanged. Check the updated version:\nconst { artifact } = await fetch(`https://docs.sleeperhit.studio/api/v1/artifacts/${artifactId}`, {\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}` },\n}).then((r) => r.json())\nconsole.log(`v${artifact.currentVersion}, refineCount: ${artifact.refineCount}`)"}]},{"id":"modify-voice-range","title":"Apply a voice effect to a range of lines","status":"available","scopes":["artifact:publish"],"description":"POST /voice-modification to apply a voice effect (currently only autotune) to a contiguous range of dialogue entries on a table-read artifact. No new revision and share URLs stay stable. The autotune recipe is optional and partial: omitted fields fall back to the proven defaults (key D, scale minpent, strength 1.0, smooth 1, reverb chapel). Returns { artifactId, modificationId, status }; the rendered audio is projected onto the read once status is `ready`.","whenToUse":"After a `table_read` artifact is `READY` and you want a stylized (autotuned) pass over specific lines without re-running the plan-to-job flow. Requires scope `artifact:publish`.","steps":[{"title":"POST /artifacts/{artifactId}/voice-modification","description":"Supply the inclusive `startEntryIndex`/`endEntryIndex` range. `effect` defaults to `\"autotune\"`; the `params` recipe is optional and merged over the defaults. Requires `Idempotency-Key`.","request":{"method":"POST","path":"/artifacts/{artifactId}/voice-modification","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"startEntryIndex":4,"endEntryIndex":9,"effect":"autotune","params":{"key":"D","scale":"minpent","strength":1,"smooth":1,"reverb":"chapel"}}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/artifacts/$ARTIFACT_ID/voice-modification \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"startEntryIndex\":4,\"endEntryIndex\":9,\"effect\":\"autotune\",\"params\":{\"key\":\"D\",\"scale\":\"minpent\",\"strength\":1.0,\"smooth\":1,\"reverb\":\"chapel\"}}'","node":"import { randomUUID } from 'node:crypto'\nconst modRes = await fetch(`https://docs.sleeperhit.studio/api/v1/artifacts/${artifactId}/voice-modification`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  // params is optional; omit it to use the proven defaults.\n  body: JSON.stringify({ startEntryIndex: 4, endEntryIndex: 9 }),\n})\nif (!modRes.ok) throw new Error(`voice-modification ${modRes.status}`)\nconst { modificationId, status } = await modRes.json()","responseExample":{"artifactId":"cma...","modificationId":"cmvm...","status":"queued"}}]},{"id":"render-and-finish-a-trailer","title":"Render a trailer, choose its takes, and finish it","status":"available","scopes":["story:read","story:write","artifact:publish"],"description":"A trailer moves draft → rendering → review → finishing → complete. Approving the plan quotes the 480p render and, confirmed, renders every call with the cast's own recorded lines (a take that leaves a line unheard or says it in another character's voice, has lips out of step with its lines, or misses its opening framing is retaken once at our cost; a line said in the take's own voice, heard and distinct, is kept). In review you read the beats, choose takes, and render a beat again (priced) if you want; then the finish upscales the selected takes to the delivery class — 720p by default, 1080p on request — and stitches the finished master. Priced steps are TWO CALLS: the price first, then `confirmed: true`.","whenToUse":"After a story job with a `trailer` artifact request completes at its plan (every trailer job stops there: `progress.trailerJobId` names the `draft` trailer, `progress.nextStep` is approve_trailer_plan) and you want to price its render, render it, review its takes and deliver it.","steps":[{"title":"POST /trailers/{jobId}/plan/approve — the price","description":"Without `confirmed` this runs the coverage gate, compiles and voices every footage beat (free to the writer) and returns the 480p render quote. Nothing is reserved. A plan that cannot render is refused with a 409 whose `details.refusal.fix` names the action that unblocks it.","request":{"method":"POST","path":"/trailers/{jobId}/plan/approve","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{}},"curl":"curl -s -X POST https://docs.sleeperhit.studio/api/v1/trailers/$TRAILER_ID/plan/approve \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","node":"import { randomUUID } from 'node:crypto'\nconst res = await fetch(`https://docs.sleeperhit.studio/api/v1/trailers/${trailerId}/plan/approve`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  body: JSON.stringify({}),\n})\nif (!res.ok) throw new Error(`POST /trailers/{jobId}/plan/approve ${res.status}`)\nconst body = await res.json()","responseExample":{"quotedOnly":true,"jobId":"cmtr...","credits":412,"costLabel":"Trailer render: 7 shots across 5 beats (58s at 480p)","lines":[{"kind":"call","label":"Render trailer beat-1 · 8s at 480p (take 1)","credits":55,"callId":"c1a2...","plateKey":null,"seconds":8}],"calls":[{"beatIndex":0,"callId":"c1a2...","take":1,"seconds":8,"audioMode":"dialogue","credits":55}],"warnings":[],"balanceAfter":4588,"sufficient":true,"note":"Rendering this trailer (7 shots, 58s at 480p, 3 with the cast's voices) costs 412 credits — you'd have 4,588 left. Nothing has been spent — call again with confirmed: true and it will render."}},{"title":"POST /trailers/{jobId}/plan/approve — confirmed","description":"Only after the writer has seen the price and said go. Reserves exactly the quoted calls and starts the render; poll the beats until `status` is `review`.","request":{"method":"POST","path":"/trailers/{jobId}/plan/approve","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"confirmed":true}},"curl":"curl -s -X POST https://docs.sleeperhit.studio/api/v1/trailers/$TRAILER_ID/plan/approve \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"confirmed\":true}'","node":"import { randomUUID } from 'node:crypto'\nconst res = await fetch(`https://docs.sleeperhit.studio/api/v1/trailers/${trailerId}/plan/approve`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  body: JSON.stringify({\"confirmed\":true}),\n})\nif (!res.ok) throw new Error(`POST /trailers/{jobId}/plan/approve ${res.status}`)\nconst body = await res.json()","responseExample":{"quotedOnly":false,"queued":true,"jobId":"cmtr...","credits":412,"calls":7,"message":"Rendering 7 shots at 480p. You review each shot before it is finished."}},{"title":"GET /trailers/{jobId}/beats","description":"Each beat's calls and every take: whether each line was heard in a voice of its own (`voiceMissLineRefs`: a line not heard, or said in a voice too like another character's — a flag; `ownReadingLineRefs`: a line said in the take's own voice, heard and distinct, kept and never a flag), whether it opened on its planned framing (`openingMiss`), whether a mouth did not do what its line asks (`mouthMiss`), whether it was the automatic retake at our cost (`atOurCost`), and which take is selected. A read: nothing compiles or renders.","request":{"method":"GET","path":"/trailers/{jobId}/beats","headers":{"Authorization":"Bearer sh_<prefix>_<secret>"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/trailers/$TRAILER_ID/beats \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\"","node":"const res = await fetch(`https://docs.sleeperhit.studio/api/v1/trailers/${trailerId}/beats`, {\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}` },\n})\nif (!res.ok) throw new Error(`GET /trailers/{jobId}/beats ${res.status}`)\nconst body = await res.json()","responseExample":{"jobId":"cmtr...","title":"Gothic trailer","status":"review","reviewUrl":"/dashboard/writing-lab/cmp.../cms.../videos?subtab=trailer&trailerJobId=cmtr...","deliveryResolution":"720p","master":{"url":"https://files.sleeperhit.studio/.../preview.mp4","stage":"preview","resolution":"480p","interim":false,"durationSeconds":92.4,"posterUrl":"https://files.sleeperhit.studio/.../poster.jpg"},"warnings":[],"beats":[{"beatIndex":2,"compositionKind":"image","stale":false,"calls":[{"callId":"c3f9...","durationSeconds":8,"audioMode":"dialogue","plateKey":"INT_TAVERN__MEDIUM","selectedTake":2,"takes":[{"take":1,"status":"accepted","voiceMissLineRefs":[41],"ownReadingLineRefs":[],"openingMiss":false,"mouthMiss":false,"atOurCost":false,"selected":false},{"take":2,"status":"accepted","voiceMissLineRefs":[],"ownReadingLineRefs":[],"openingMiss":false,"mouthMiss":false,"atOurCost":true,"selected":true}],"retake":{"take":3,"credits":55}}]}]}},{"title":"POST /trailers/{jobId}/beats/{beatIndex}/select-take","description":"Free and reversible. Any accepted take may be selected, a flagged one included: `warning` names what it is flagged for.","request":{"method":"POST","path":"/trailers/{jobId}/beats/{beatIndex}/select-take","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"callId":"c3f9...","take":1}},"curl":"curl -s -X POST https://docs.sleeperhit.studio/api/v1/trailers/$TRAILER_ID/beats/2/select-take \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"callId\":\"c3f9...\",\"take\":1}'","node":"import { randomUUID } from 'node:crypto'\nconst res = await fetch(`https://docs.sleeperhit.studio/api/v1/trailers/${trailerId}/beats/2/select-take`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  body: JSON.stringify({\"callId\":\"c3f9...\",\"take\":1}),\n})\nif (!res.ok) throw new Error(`POST /trailers/{jobId}/beats/{beatIndex}/select-take ${res.status}`)\nconst body = await res.json()","responseExample":{"jobId":"cmtr...","beatIndex":2,"callId":"c3f9...","take":1,"warning":"Take 1 has line 41 said in a voice too like another character's."}},{"title":"GET /trailers/{jobId}/video-coverage","description":"The video coverage of the master you are reviewing: it was watched with its sound, by itself and free, and scored on its quality and its adherence to the shot list against a bar. Each finding carries its timecode, the beat, call and shot it fell in, and its remedy. While `blocksFinish` is true the finish is refused (409, `details.videoCoverage`): retake the shots it names, or finish anyway with `acknowledgeVideoCoverage: true` only once the writer has chosen to. POST the same path (free, no `confirmed`) to cover the master again.","request":{"method":"GET","path":"/trailers/{jobId}/video-coverage","headers":{"Authorization":"Bearer sh_<prefix>_<secret>"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/trailers/$TRAILER_ID/video-coverage \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\"","node":"const res = await fetch(`https://docs.sleeperhit.studio/api/v1/trailers/${trailerId}/video-coverage`, {\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}` },\n})\nif (!res.ok) throw new Error(`GET /trailers/{jobId}/video-coverage ${res.status}`)\nconst body = await res.json()","responseExample":{"coverage":{"status":"complete","planKind":"trailer","jobId":"cmtr...","stage":"preview","masterUrl":"https://files.sleeperhit.studio/.../preview.mp4","currentMasterUrl":"https://files.sleeperhit.studio/.../preview.mp4","current":true,"available":true,"gatePassed":false,"gateThreshold":7,"vetoDimensions":["lipSync","promptAdherence","textPlacement"],"gateReasons":["Lip sync scored 3.5, below the 7.0 bar. This dimension fails the video on its own."],"blocksFinish":true,"summary":"The preview master scored 4.6/10 and did not clear the 7.0 video coverage bar.","trigger":"auto","reportId":"cmvc...","reportVersion":1,"rubricVersion":1,"overallScore":4.6,"model":"gemini-3.8-flash","progressDetail":null,"failureMessage":null,"report":{"overallScore":5,"verdict":"fail","oneLineDiagnosis":"Mouths stay shut while the lines play.","dimensions":{"lipSync":{"score":3.5,"finding":"The speaker never moves his lips."},"promptAdherence":{"score":6,"finding":"One shot is a face where a back view was asked for."},"visualFidelity":{"score":7,"finding":"Clean."},"motionNaturalness":{"score":6.5,"finding":"Some stiff posing."},"identityContinuity":{"score":8,"finding":"Consistent."},"textPlacement":{"score":5,"finding":"A caption covers a face."},"audioSync":{"score":7.5,"finding":"Voices sit on their shots."}},"findings":[{"dimension":"lipSync","severity":"critical","startSeconds":21.5,"endSeconds":26,"timecode":"0:21.5–0:26","observation":"His voice plays while his mouth stays closed.","fix":"Retake the shot with his mouth moving through the line.","shouldShow":"His lips and jaw forming every word of the line as it is heard.","unit":{"kind":"beat","index":2,"number":3,"title":"The Tavern"},"callId":"c3f9...","shotIndex":0,"remedy":{"kind":"retake_call","callId":"c3f9..."}}],"priorityFixes":["Retake the shot at 0:21.5 so the speaker says the line on camera."]},"createdAt":"2026-10-01T18:00:00.000Z","updatedAt":"2026-10-01T18:01:10.000Z"}}},{"title":"POST /trailers/{jobId}/finish — the price","description":"Without `confirmed` this prices the finish at the delivery class (720p unless the writer chose 1080p): one line per selected take, one per text layer, and a warning per flagged take kept. Nothing is reserved.","request":{"method":"POST","path":"/trailers/{jobId}/finish","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{}},"curl":"curl -s -X POST https://docs.sleeperhit.studio/api/v1/trailers/$TRAILER_ID/finish \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","node":"import { randomUUID } from 'node:crypto'\nconst res = await fetch(`https://docs.sleeperhit.studio/api/v1/trailers/${trailerId}/finish`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  body: JSON.stringify({}),\n})\nif (!res.ok) throw new Error(`POST /trailers/{jobId}/finish ${res.status}`)\nconst body = await res.json()","responseExample":{"quotedOnly":true,"jobId":"cmtr...","deliveryResolution":"720p","credits":18,"costLabel":"Trailer finish at 720p: 7 shots (58s)","lines":[],"warnings":["Beat 3: Take 1 has line 41 said in a voice too like another character's."],"balanceAfter":4570,"sufficient":true,"note":"Finishing this trailer at 720p (7 shots upscaled from the 480p takes) costs 18 credits — you'd have 4,570 left. Nothing has been spent — call again with confirmed: true and it will render."}},{"title":"POST /trailers/{jobId}/finish — confirmed","description":"Reserves the quoted lines and starts the finish; the trailer is `complete` when the finished master is stitched, and only then does it export.","request":{"method":"POST","path":"/trailers/{jobId}/finish","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"confirmed":true}},"curl":"curl -s -X POST https://docs.sleeperhit.studio/api/v1/trailers/$TRAILER_ID/finish \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"confirmed\":true}'","node":"import { randomUUID } from 'node:crypto'\nconst res = await fetch(`https://docs.sleeperhit.studio/api/v1/trailers/${trailerId}/finish`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  body: JSON.stringify({\"confirmed\":true}),\n})\nif (!res.ok) throw new Error(`POST /trailers/{jobId}/finish ${res.status}`)\nconst body = await res.json()","responseExample":{"quotedOnly":false,"queued":true,"jobId":"cmtr...","deliveryResolution":"720p","credits":18,"takes":7,"warnings":["Beat 3: Take 1 has line 41 said in a voice too like another character's."],"message":"Finishing 7 shots at 720p."}}]},{"id":"render-and-finish-a-pitch-deck","title":"Render a pitch deck, choose its takes, and finish it","status":"available","scopes":["story:read","story:write","artifact:publish"],"description":"A pitch deck moves draft → rendering → review → finishing → complete. There are no stills and no storyboard or draft approval. Approving the plan quotes the 480p render and, confirmed, renders every chapter as a few short shots in one room (a line spoken by a cast member plays in their own recorded voice; a narrated line is laid over the cut). A take that leaves a line unheard or says it in another character's voice, has lips out of step with its lines, or misses its opening framing is retaken once at our cost; a line said in the take's own voice, heard and distinct, is kept. In review you read the chapters, choose takes, and render a chapter again (priced) if you want; then the finish upscales the selected takes to the delivery class — 720p by default, 1080p on request — lays the text layers over the finished cuts, stitches the finished master, and lays the PDF out too when the deck was requested with `includePdf: true`. A finished deck is unpublished until its writer publishes it. Priced steps are TWO CALLS: the price first, then `confirmed: true`.","whenToUse":"After a story job with a `pitch_deck` artifact request completes at its plan (every deck job stops there: `progress.pitchDeckJobId` names the `draft` deck, `progress.nextStep` is approve_pitch_deck_plan) and you want to price its render, render it, review its takes and deliver it.","steps":[{"title":"POST /pitch-decks/{jobId}/plan/approve — the price","description":"Without `confirmed` this runs the coverage gate, compiles and voices every footage chapter (free to the writer) and returns the 480p render quote, with a line for any location plate the render makes first. Nothing is reserved. A plan that cannot render is refused with a 409 whose `details.refusal.fix` (and `details.blocked[]`, per chapter) names the action that unblocks it.","request":{"method":"POST","path":"/pitch-decks/{jobId}/plan/approve","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{}},"curl":"curl -s -X POST https://docs.sleeperhit.studio/api/v1/pitch-decks/$DECK_ID/plan/approve \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","node":"import { randomUUID } from 'node:crypto'\nconst res = await fetch(`https://docs.sleeperhit.studio/api/v1/pitch-decks/${deckId}/plan/approve`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  body: JSON.stringify({}),\n})\nif (!res.ok) throw new Error(`POST /pitch-decks/{jobId}/plan/approve ${res.status}`)\nconst body = await res.json()","responseExample":{"quotedOnly":true,"jobId":"cmpd...","credits":520,"costLabel":"Pitch deck render: 8 shots across 4 chapters (72s at 480p) + 1 location plate","lines":[{"kind":"call","label":"Render pitch deck chapter 1 · 9s at 480p (take 1)","credits":62,"callId":"d1a2...","plateKey":null,"seconds":9},{"kind":"plate","label":"Location plate INT_STUDY","credits":4,"callId":null,"plateKey":"INT_STUDY","seconds":null}],"calls":[{"chapterIndex":0,"callId":"d1a2...","take":1,"seconds":9,"audioMode":"dialogue","credits":62}],"warnings":[],"balanceAfter":4480,"sufficient":true,"note":"Rendering this deck (8 shots, 72s at 480p) costs 520 credits — you'd have 4,480 left. Nothing has been spent — call again with confirmed: true and it will render."}},{"title":"POST /pitch-decks/{jobId}/plan/approve — confirmed","description":"Only after the writer has seen the price and said go. Reserves exactly the quoted calls and starts the render; poll the chapters until `status` is `review`.","request":{"method":"POST","path":"/pitch-decks/{jobId}/plan/approve","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"confirmed":true}},"curl":"curl -s -X POST https://docs.sleeperhit.studio/api/v1/pitch-decks/$DECK_ID/plan/approve \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"confirmed\":true}'","node":"import { randomUUID } from 'node:crypto'\nconst res = await fetch(`https://docs.sleeperhit.studio/api/v1/pitch-decks/${deckId}/plan/approve`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  body: JSON.stringify({\"confirmed\":true}),\n})\nif (!res.ok) throw new Error(`POST /pitch-decks/{jobId}/plan/approve ${res.status}`)\nconst body = await res.json()","responseExample":{"quotedOnly":false,"queued":true,"jobId":"cmpd...","credits":520,"calls":8,"message":"Rendering 8 shots at 480p. You review each chapter before it is finished."}},{"title":"GET /pitch-decks/{jobId}/chapters","description":"Each chapter's calls and every take: whether each line was heard in a voice of its own (`voiceMissLineRefs`: a line not heard, or said in a voice too like another character's — a flag; `ownReadingLineRefs`: a line said in the take's own voice, heard and distinct, kept and never a flag), whether it opened on its planned framing (`openingMiss`), whether a mouth did not do what its line asks (`mouthMiss`), whether it was the automatic retake at our cost (`atOurCost`), and which take is selected; plus the cut, the poster frame, the master and the delivery class. A read: nothing compiles or renders.","request":{"method":"GET","path":"/pitch-decks/{jobId}/chapters","headers":{"Authorization":"Bearer sh_<prefix>_<secret>"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/pitch-decks/$DECK_ID/chapters \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\"","node":"const res = await fetch(`https://docs.sleeperhit.studio/api/v1/pitch-decks/${deckId}/chapters`, {\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}` },\n})\nif (!res.ok) throw new Error(`GET /pitch-decks/{jobId}/chapters ${res.status}`)\nconst body = await res.json()","responseExample":{"jobId":"cmpd...","title":"Karamazov v2","status":"review","reviewUrl":"/dashboard/writing-lab/cmp.../cms.../videos?subtab=pitch-deck","deliveryResolution":"720p","master":{"url":"https://files.sleeperhit.studio/.../preview.mp4","stage":"preview","resolution":"480p","interim":false,"durationSeconds":92.4,"posterUrl":"https://files.sleeperhit.studio/.../poster.jpg"},"motion":{"rendered":4,"requested":4,"stillOnly":0,"complete":true},"warnings":[],"videoModelPreflight":null,"chapters":[{"chapterIndex":1,"title":"The Return","compositionKind":"both","speaker":"ALYOSHA","speakerOnCamera":true,"narrated":false,"location":"INT_STUDY","stale":false,"inMaster":true,"renderedWith":null,"posterUrl":"https://files.sleeperhit.studio/.../poster.jpg","calls":[{"callId":"d3f9...","durationSeconds":9,"audioMode":"dialogue","plateKey":"INT_STUDY__MEDIUM","selectedTake":2,"takes":[{"take":1,"status":"accepted","voiceMissLineRefs":[3],"ownReadingLineRefs":[],"openingMiss":false,"mouthMiss":false,"atOurCost":false,"selected":false},{"take":2,"status":"accepted","voiceMissLineRefs":[],"ownReadingLineRefs":[],"openingMiss":false,"mouthMiss":false,"atOurCost":true,"selected":true}],"retake":{"take":3,"credits":62}}]}]}},{"title":"POST /pitch-decks/{jobId}/chapters/{chapterIndex}/select-take","description":"Free and reversible. Any accepted take may be selected, a flagged one included: `warning` names what it is flagged for.","request":{"method":"POST","path":"/pitch-decks/{jobId}/chapters/{chapterIndex}/select-take","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"callId":"d3f9...","take":1}},"curl":"curl -s -X POST https://docs.sleeperhit.studio/api/v1/pitch-decks/$DECK_ID/chapters/1/select-take \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"callId\":\"d3f9...\",\"take\":1}'","node":"import { randomUUID } from 'node:crypto'\nconst res = await fetch(`https://docs.sleeperhit.studio/api/v1/pitch-decks/${deckId}/chapters/1/select-take`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  body: JSON.stringify({\"callId\":\"d3f9...\",\"take\":1}),\n})\nif (!res.ok) throw new Error(`POST /pitch-decks/{jobId}/chapters/{chapterIndex}/select-take ${res.status}`)\nconst body = await res.json()","responseExample":{"jobId":"cmpd...","chapterIndex":1,"callId":"d3f9...","take":1,"warning":"Take 1 has line 3 said in a voice too like another character's."}},{"title":"GET /pitch-decks/{jobId}/video-coverage","description":"The video coverage of the master you are reviewing: it was watched with its sound, by itself and free, and scored on its quality and its adherence to the shot list against a bar. Each finding carries its timecode, the chapter, call and shot it fell in, and its remedy. While `blocksFinish` is true the finish is refused (409, `details.videoCoverage`): retake the shots it names, or finish anyway with `acknowledgeVideoCoverage: true` only once the writer has chosen to. POST the same path (free, no `confirmed`) to cover the master again.","request":{"method":"GET","path":"/pitch-decks/{jobId}/video-coverage","headers":{"Authorization":"Bearer sh_<prefix>_<secret>"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/pitch-decks/$DECK_ID/video-coverage \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\"","node":"const res = await fetch(`https://docs.sleeperhit.studio/api/v1/pitch-decks/${deckId}/video-coverage`, {\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}` },\n})\nif (!res.ok) throw new Error(`GET /pitch-decks/{jobId}/video-coverage ${res.status}`)\nconst body = await res.json()","responseExample":{"coverage":{"status":"complete","planKind":"pitch_deck","jobId":"cmpd...","stage":"preview","masterUrl":"https://files.sleeperhit.studio/.../preview.mp4","currentMasterUrl":"https://files.sleeperhit.studio/.../preview.mp4","current":true,"available":true,"gatePassed":false,"gateThreshold":7,"vetoDimensions":["lipSync","promptAdherence","textPlacement"],"gateReasons":["Lip sync scored 3.5, below the 7.0 bar. This dimension fails the video on its own."],"blocksFinish":true,"summary":"The preview master scored 4.6/10 and did not clear the 7.0 video coverage bar.","trigger":"auto","reportId":"cmvc...","reportVersion":1,"rubricVersion":1,"overallScore":4.6,"model":"gemini-3.8-flash","progressDetail":null,"failureMessage":null,"report":{"overallScore":5,"verdict":"fail","oneLineDiagnosis":"Mouths stay shut while the lines play.","dimensions":{"lipSync":{"score":3.5,"finding":"The speaker never moves his lips."},"promptAdherence":{"score":6,"finding":"One shot is a face where a back view was asked for."},"visualFidelity":{"score":7,"finding":"Clean."},"motionNaturalness":{"score":6.5,"finding":"Some stiff posing."},"identityContinuity":{"score":8,"finding":"Consistent."},"textPlacement":{"score":5,"finding":"A caption covers a face."},"audioSync":{"score":7.5,"finding":"Voices sit on their shots."}},"findings":[{"dimension":"lipSync","severity":"critical","startSeconds":21.5,"endSeconds":26,"timecode":"0:21.5–0:26","observation":"His voice plays while his mouth stays closed.","fix":"Retake the shot with his mouth moving through the line.","shouldShow":"His lips and jaw forming every word of the line as it is heard.","unit":{"kind":"chapter","index":3,"number":4,"title":"The Father"},"callId":"d3f9...","shotIndex":0,"remedy":{"kind":"retake_call","callId":"d3f9..."}}],"priorityFixes":["Retake the shot at 0:21.5 so the speaker says the line on camera."]},"createdAt":"2026-10-01T18:00:00.000Z","updatedAt":"2026-10-01T18:01:10.000Z"}}},{"title":"POST /pitch-decks/{jobId}/finish — the price","description":"Without `confirmed` this prices the finish at the delivery class (720p unless the writer chose 1080p): one line per selected take, one per text layer, and a warning per flagged take kept. Nothing is reserved.","request":{"method":"POST","path":"/pitch-decks/{jobId}/finish","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{}},"curl":"curl -s -X POST https://docs.sleeperhit.studio/api/v1/pitch-decks/$DECK_ID/finish \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","node":"import { randomUUID } from 'node:crypto'\nconst res = await fetch(`https://docs.sleeperhit.studio/api/v1/pitch-decks/${deckId}/finish`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  body: JSON.stringify({}),\n})\nif (!res.ok) throw new Error(`POST /pitch-decks/{jobId}/finish ${res.status}`)\nconst body = await res.json()","responseExample":{"quotedOnly":true,"jobId":"cmpd...","deliveryResolution":"720p","credits":22,"costLabel":"Pitch deck finish at 720p: 8 shots (72s)","lines":[],"warnings":["Chapter 2: Take 1 has line 3 said in a voice too like another character's."],"balanceAfter":4458,"sufficient":true,"note":"Finishing this deck at 720p (8 shots upscaled from the 480p takes) costs 22 credits — you'd have 4,458 left. Nothing has been spent — call again with confirmed: true and it will render."}},{"title":"POST /pitch-decks/{jobId}/finish — confirmed","description":"Reserves the quoted lines and starts the finish; the deck is `complete` when the finished master is stitched, and only then does it export and publish (its PDF follows).","request":{"method":"POST","path":"/pitch-decks/{jobId}/finish","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"confirmed":true}},"curl":"curl -s -X POST https://docs.sleeperhit.studio/api/v1/pitch-decks/$DECK_ID/finish \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"confirmed\":true}'","node":"import { randomUUID } from 'node:crypto'\nconst res = await fetch(`https://docs.sleeperhit.studio/api/v1/pitch-decks/${deckId}/finish`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  body: JSON.stringify({\"confirmed\":true}),\n})\nif (!res.ok) throw new Error(`POST /pitch-decks/{jobId}/finish ${res.status}`)\nconst body = await res.json()","responseExample":{"quotedOnly":false,"queued":true,"jobId":"cmpd...","deliveryResolution":"720p","credits":22,"takes":8,"warnings":["Chapter 2: Take 1 has line 3 said in a voice too like another character's."],"message":"Finishing 8 shots at 720p."}}]},{"id":"recast-avatars","title":"Regenerate a character avatar, restyle the cast, and poll the enriched cast","status":"available","scopes":["artifact:publish","artifact:read"],"description":"POST /avatar to re-render ONE character avatar, POST /cast to restyle ALL portraits in one call, then GET /cast to poll each character's avatarStatus until the renders are ready. Avatar work is async + queue-backed; voice reassignments apply immediately. No new revision and share URLs stay stable.","whenToUse":"After a `table_read` artifact is `READY` and you want to refresh one character's portrait, restyle the whole cast, or batch-reassign voices — without re-running the plan-to-job flow. Avatar ops need scope `artifact:publish`; reading the cast needs `artifact:read`.","steps":[{"title":"POST /artifacts/{artifactId}/avatar — regenerate one character","description":"Supply the `character` to re-render. `refinement` is a freeform art-direction nudge; `style` persists an episode portrait override without changing project Series Bible canon. Requires `Idempotency-Key`. Returns `{ character, status, chatToolJobId }`; the render is async.","request":{"method":"POST","path":"/artifacts/{artifactId}/avatar","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"character":"MAYA","refinement":"older, weathered, dim backlight"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/artifacts/$ARTIFACT_ID/avatar \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"character\":\"MAYA\",\"refinement\":\"older, weathered, dim backlight\"}'","node":"import { randomUUID } from 'node:crypto'\nconst avatarRes = await fetch(`https://docs.sleeperhit.studio/api/v1/artifacts/${artifactId}/avatar`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  body: JSON.stringify({ character: 'MAYA', refinement: 'older, weathered, dim backlight' }),\n})\nif (!avatarRes.ok) throw new Error(`avatar ${avatarRes.status}`)\nconst { character, status, chatToolJobId } = await avatarRes.json()","responseExample":{"artifactId":"cma...","character":"MAYA","status":"queued","chatToolJobId":"cmtj..."}},{"title":"POST /artifacts/{artifactId}/cast — restyle the whole cast","description":"A top-level `avatarStyle` with no `entries` re-renders EVERY character against the new art-direction (provide at least one `entries` item OR an `avatarStyle`). Mix in `entries` to also reassign voices or target per-character avatar renders. Requires `Idempotency-Key`. Returns the updated `voiceMap` plus one async avatar op per queued render.","request":{"method":"POST","path":"/artifacts/{artifactId}/cast","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"avatarStyle":"noir graphite ink portraits"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/artifacts/$ARTIFACT_ID/cast \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"avatarStyle\":\"noir graphite ink portraits\"}'","node":"import { randomUUID } from 'node:crypto'\nconst castRes = await fetch(`https://docs.sleeperhit.studio/api/v1/artifacts/${artifactId}/cast`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  // Restyle every portrait; you could also send entries:[{ character, voiceId, voiceName }] to reassign voices.\n  body: JSON.stringify({ avatarStyle: 'noir graphite ink portraits' }),\n})\nif (!castRes.ok) throw new Error(`cast ${castRes.status}`)\nconst { voiceMap, avatars } = await castRes.json()\nconsole.log(`${avatars.length} avatar renders queued`)","responseExample":{"artifactId":"cma...","voiceMap":{"MAYA":{"voiceId":"v_123","voiceName":"Maya","provider":"cartesia"}},"avatars":[{"character":"MAYA","status":"queued","chatToolJobId":"cmtj..."}]}},{"title":"Poll GET /artifacts/{artifactId}/cast until avatars are ready","description":"Read the enriched cast; each character carries `avatarUrl` / `avatarStyle` / `avatarStatus`. Poll until every in-flight `avatarStatus` leaves `queued` / `rendering` (terminal: `ready` or `failed`). `none` means the character has no avatar.","request":{"method":"GET","path":"/artifacts/{artifactId}/cast","headers":{"Authorization":"Bearer sh_<prefix>_<secret>"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/artifacts/$ARTIFACT_ID/cast \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" | jq .cast","node":"async function pollCast(artifactId) {\n  const pending = new Set(['queued', 'rendering'])\n  while (true) {\n    const res = await fetch(`https://docs.sleeperhit.studio/api/v1/artifacts/${artifactId}/cast`, {\n      headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}` },\n    })\n    if (!res.ok) throw new Error(`poll ${res.status}`)\n    const { cast } = await res.json()\n    if (!cast.some((c) => pending.has(c.avatarStatus))) return cast\n    await new Promise((r) => setTimeout(r, 5000))\n  }\n}\nconst cast = await pollCast(artifactId)\nconsole.log(cast.map((c) => `${c.character}: ${c.avatarStatus} ${c.avatarUrl ?? ''}`).join('\\n'))","responseExample":{"artifactId":"cma...","avatarStyle":"noir graphite ink portraits","cast":[{"character":"MAYA","voiceId":"v_123","voiceName":"Maya","gender":"female","provider":"cartesia","avatarUrl":"https://example.com/avatars/maya.png","avatarStyle":"noir graphite ink portraits","avatarStatus":"ready"}]}}]},{"id":"finalize-audio-mp3","title":"Finalize a table read artifact as a durable MP3","status":"available","scopes":["artifact:publish"],"description":"POST /finalize to freeze the completed Pipecat/Daily table-read recording into a permanent MP3 alongside the live share. Free. Replay-safe: re-calling when complete or in-flight returns current state without rendering again.","whenToUse":"When you need a downloadable audio file (email delivery, archival, audio platform upload) alongside the live performance.","steps":[{"title":"POST /artifacts/{artifactId}/finalize","description":"Omit `mode` or pass `\"audio\"` to finalize an MP3 from the completed live-read recording. Requires `Idempotency-Key`. Returns `{ artifactId, mode, finalize: { status } }` immediately; `needs_recording` means start/record the live read first.","request":{"method":"POST","path":"/artifacts/{artifactId}/finalize","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"mode":"audio"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/artifacts/$ARTIFACT_ID/finalize \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"mode\":\"audio\"}'","node":"import { randomUUID } from 'node:crypto'\nconst finalizeRes = await fetch(`https://docs.sleeperhit.studio/api/v1/artifacts/${artifactId}/finalize`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  body: JSON.stringify({ mode: 'audio' }),\n})\nif (!finalizeRes.ok) throw new Error(`finalize ${finalizeRes.status}`)\nconst { finalize } = await finalizeRes.json()\nconsole.log('Finalize status:', finalize.status) // 'queued' or 'needs_recording'","responseExample":{"artifactId":"cma...","mode":"audio","finalize":{"status":"queued"}}},{"title":"Poll GET /artifacts/{artifactId} until finalize is complete","description":"Check `manifest.audio.finalize.status`: `needs_recording`, or `queued → rendering → uploading → complete` (or `failed`). On `complete`: `manifest.audio.recordingUrl` (MP3) and `manifest.audio.recordingDurationMs`.","request":{"method":"GET","path":"/artifacts/{artifactId}","headers":{"Authorization":"Bearer sh_<prefix>_<secret>"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/artifacts/$ARTIFACT_ID \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" | jq .artifact.manifest.audio.finalize","node":"async function pollFinalize(artifactId) {\n  const terminal = new Set(['complete', 'failed'])\n  while (true) {\n    const res = await fetch(`https://docs.sleeperhit.studio/api/v1/artifacts/${artifactId}`, {\n      headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}` },\n    })\n    if (!res.ok) throw new Error(`poll ${res.status}`)\n    const { artifact } = await res.json()\n    const s = artifact.manifest?.audio?.finalize?.status\n    if (terminal.has(s)) return artifact\n    await new Promise((r) => setTimeout(r, 7000))\n  }\n}\nconst finalized = await pollFinalize(artifactId)\nif (finalized.manifest.audio.finalize.status === 'failed') {\n  throw new Error(finalized.manifest.audio.finalize.error ?? \"finalize failed\")\n}\nconsole.log(\"MP3 URL:\", finalized.manifest.audio.recordingUrl)\nconsole.log(\"Duration (ms):\", finalized.manifest.audio.recordingDurationMs)"}]},{"id":"publish-finalized-table-read-podcast","title":"Promote a finalized table-read artifact to a podcast feed","status":"available","scopes":["publishing:write","publishing:publish","publishing:read"],"description":"Publishing promotes and schedules durable assets that already exist. For podcast v1, finalize the Table Read MP3 through the artifact/table-read flow first, then create a release from that StoryArtifact and publish it to the first-party RSS feed.","whenToUse":"After the writer has finished refining the episode in Table Read and the artifact manifest has a finalized MP3 recording URL.","steps":[{"title":"POST /story-projects/{projectId}/publishing-series","description":"Create the project-owned series. Audio series automatically get a first-party RSS destination; no OAuth is needed for RSS v1.","request":{"method":"POST","path":"/story-projects/{projectId}/publishing-series","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"title":"The Attention Engine","medium":"audio","format":"podcast","cadence":{"frequency":"weekly","dayOfWeek":"tuesday"}}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/story-projects/$PROJECT_ID/publishing-series \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"title\":\"The Attention Engine\",\"medium\":\"audio\",\"format\":\"podcast\",\"cadence\":{\"frequency\":\"weekly\",\"dayOfWeek\":\"tuesday\"}}'","node":"import { randomUUID } from 'node:crypto'\n\nconst seriesRes = await fetch(`https://docs.sleeperhit.studio/api/v1/story-projects/${projectId}/publishing-series`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`,\n    'Idempotency-Key': randomUUID(),\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({ title: 'The Attention Engine', medium: 'audio', format: 'podcast' }),\n})\nif (!seriesRes.ok) throw new Error(`publishing series ${seriesRes.status}`)\nconst { series } = await seriesRes.json()","responseExample":{"series":{"id":"cmpubseries...","projectId":"cmp...","title":"The Attention Engine","medium":"audio","format":"podcast","feedUrl":"https://sleeperhit.studio/api/v1/publishing-feeds/the-attention-engine.xml"}}},{"title":"POST /publishing-series/{seriesId}/releases","description":"Promote an already-finalized table-read StoryArtifact. If the artifact has no finalized MP3, the API returns `publishing_asset_not_ready`.","request":{"method":"POST","path":"/publishing-series/{seriesId}/releases","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"title":"Pilot: The Signal That Learns You","description":"A radio-drama opening episode for The Attention Engine.","sourceArtifactId":"<finalized-table-read-artifact-id>","seasonNumber":1,"episodeNumber":1}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/publishing-series/$SERIES_ID/releases \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"title\":\"Pilot: The Signal That Learns You\",\"sourceArtifactId\":\"<finalized-table-read-artifact-id>\",\"seasonNumber\":1,\"episodeNumber\":1}'","node":"const releaseRes = await fetch(`https://docs.sleeperhit.studio/api/v1/publishing-series/${series.id}/releases`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`,\n    'Idempotency-Key': randomUUID(),\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    title: 'Pilot: The Signal That Learns You',\n    sourceArtifactId: finalizedTableReadArtifactId,\n    seasonNumber: 1,\n    episodeNumber: 1,\n  }),\n})\nif (!releaseRes.ok) throw new Error(`publishing release ${releaseRes.status}`)\nconst { release } = await releaseRes.json()","responseExample":{"release":{"id":"cmpubrelease...","status":"ready","sourceArtifactId":"cma...","assets":[{"kind":"mp3","status":"ready","url":"https://cdn.example.com/final.mp3"}]}}},{"title":"POST /publishing-releases/{releaseId}/schedule","description":"Schedule the ready release. Scheduling is a readiness signal; Publishing does not add a separate draft or approval loop.","request":{"method":"POST","path":"/publishing-releases/{releaseId}/schedule","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"scheduledAt":"2026-07-07T14:00:00.000Z"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/publishing-releases/$RELEASE_ID/schedule \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"scheduledAt\":\"2026-07-07T14:00:00.000Z\"}'","node":"const scheduleRes = await fetch(`https://docs.sleeperhit.studio/api/v1/publishing-releases/${release.id}/schedule`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  body: JSON.stringify({ scheduledAt: '2026-07-07T14:00:00.000Z' }),\n})\nif (!scheduleRes.ok) throw new Error(`schedule ${scheduleRes.status}`)\nconst scheduled = await scheduleRes.json()"},{"title":"POST /publishing-releases/{releaseId}/refresh-media (repair only)","description":"If the same table-read artifact is re-finalized after its episode is already published, explicitly replace that episode's RSS enclosure without creating a duplicate release. sourceArtifactId must match the release's existing source and the new media must be finalized.","request":{"method":"POST","path":"/publishing-releases/{releaseId}/refresh-media","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"sourceArtifactId":"<same-finalized-table-read-artifact-id>"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/publishing-releases/$RELEASE_ID/refresh-media \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"sourceArtifactId\":\"<same-finalized-table-read-artifact-id>\"}'","node":"const refreshRes = await fetch(`https://docs.sleeperhit.studio/api/v1/publishing-releases/${release.id}/refresh-media`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`, 'Idempotency-Key': randomUUID(), 'Content-Type': 'application/json' },\n  body: JSON.stringify({ sourceArtifactId: finalizedTableReadArtifactId }),\n})\nif (!refreshRes.ok) throw new Error(`refresh media ${refreshRes.status}`)\nconst refreshed = await refreshRes.json()"},{"title":"GET /publishing-feeds/{seriesSlug}.xml","description":"Read the public RSS feed after the release is published. The feed points back to the durable MP3 asset on the release.","request":{"method":"GET","path":"/publishing-feeds/{seriesSlug}.xml"},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/publishing-feeds/the-attention-engine.xml","node":"const feedRes = await fetch('https://docs.sleeperhit.studio/api/v1/publishing-feeds/the-attention-engine.xml')\nif (!feedRes.ok) throw new Error(`feed ${feedRes.status}`)\nconst rssXml = await feedRes.text()"}]},{"id":"idempotent-post-recipe","title":"Safely retry expensive POSTs with Idempotency-Key","status":"planned","scopes":["story:write"],"description":"Every idempotent POST that reserves Studio Credits, queues generation work, or mutates an artifact requires `Idempotency-Key`. A retry with the same key returns the original response with `Idempotency-Replayed: true` instead of double-charging or double-queuing.","whenToUse":"Once generation endpoints land in a later phase. The mechanics here are stable today via `runCustomerApiIdempotentJson`.","steps":[{"title":"POST with Idempotency-Key","description":"Generate one stable identifier per logical request (e.g., a job UUID owned by your system). Retry the exact same request body if the network fails — the API replays the original response.","request":{"method":"POST","path":"/story-jobs","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"projectId":"proj_<id>","artifactRequests":[{"type":"table_read","mode":"markdown"}]}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/story-jobs \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"projectId\":\"proj_<id>\",\"artifactRequests\":[{\"type\":\"table_read\",\"mode\":\"markdown\"}]}'","node":"import { randomUUID } from 'node:crypto'\n\nconst idempotencyKey = randomUUID()\nasync function submit() {\n  return fetch('https://docs.sleeperhit.studio/api/v1/story-jobs', {\n    method: 'POST',\n    headers: {\n      Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`,\n      'Idempotency-Key': idempotencyKey,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({ projectId: 'proj_<id>', artifactRequests: [{ type: 'table_read', mode: 'markdown' }] }),\n  })\n}\n\nlet res = await submit()\nif (!res.ok && res.status >= 500) {\n  // Network or 5xx — retry with the same key returns the original outcome safely.\n  res = await submit()\n}"}]},{"id":"generate-table-read","title":"Generate a table read from an approved plan, then read the share URLs","status":"available","scopes":["story:write","story:read","artifact:read"],"description":"Once a StoryPlan is APPROVED, POST a story job to generate a `table_read`. The approved plan must include a reviewed scriptBlueprint, and each job produces one script/installment. The job reserves Studio Credits, runs the existing table-read pipeline headlessly (parse, voice assignment, creative analysis, adaptive soundtrack, SFX), and mints a public share link. To keep a known cast, GET /voices first and include a complete voiceMap on the table_read request; supplied saved voices skip automatic assignment. Poll the job until terminal, then read the artifact manifest and/or GET the backing script content. The MP4 `video` output is opt-in and never rendered by default.","whenToUse":"After a plan reaches `APPROVED`. The table read is the first live artifact adapter. The returned share token (not your API key) authorizes the URLs, so you can embed them or hand them to end-users.","steps":[{"title":"POST /story-jobs","description":"Reference the approved plan. Omit `artifactRequests` to use the plan's own requests, or pass one `[{ \"type\": \"table_read\", \"mode\": \"documentary\", \"narrationPolicy\": \"include\" }]` (modes: `documentary | podcast | drama`; narrationPolicy: `auto | include | suppress`). For a known cast, add `voiceMap: { CHARACTER: { voiceId, voiceName?, gender?, provider? } }` using caller-owned saved IDs from GET /voices; provide the complete cast. Idempotency-Key is required. A `402 insufficient_credits` means the reservation could not be covered.","request":{"method":"POST","path":"/story-jobs","headers":{"Authorization":"Bearer sh_<prefix>_<secret>","Idempotency-Key":"<your-stable-uuid>","Content-Type":"application/json"},"body":{"storyPlanId":"<approved-plan-id>","artifactRequests":[{"type":"table_read","mode":"documentary","narrationPolicy":"include"}]}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/story-jobs \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"storyPlanId\":\"<approved-plan-id>\",\"artifactRequests\":[{\"type\":\"table_read\",\"mode\":\"documentary\",\"narrationPolicy\":\"include\"}]}'","node":"import { randomUUID } from 'node:crypto'\n\nconst jobRes = await fetch('https://docs.sleeperhit.studio/api/v1/story-jobs', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}`,\n    'Idempotency-Key': randomUUID(),\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({ storyPlanId, artifactRequests: [{ type: 'table_read', mode: 'documentary', narrationPolicy: 'include' }] }),\n})\nif (!jobRes.ok) throw new Error(`job create ${jobRes.status}`)\nconst { job } = await jobRes.json()","responseExample":{"job":{"id":"cmj...","projectId":"cmp...","storyPlanId":"cmpl...","status":"RESERVED","artifactRequests":[{"type":"table_read","mode":"documentary","narrationPolicy":"include"}],"artifacts":[],"createdAt":"2026-05-28T15:20:00.000Z","updatedAt":"2026-05-28T15:20:00.000Z"}}},{"title":"Poll GET /story-jobs/{jobId} until terminal","description":"Terminal states: `READY`, `PARTIAL`, `FAILED`, `CANCELED`. When `READY`, the `artifacts[]` entries carry their manifest URLs.","request":{"method":"GET","path":"/story-jobs/{jobId}","headers":{"Authorization":"Bearer sh_<prefix>_<secret>"}},"curl":"curl -s https://docs.sleeperhit.studio/api/v1/story-jobs/$JOB_ID \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\"","node":"async function poll(jobId) {\n  while (true) {\n    const res = await fetch(`https://docs.sleeperhit.studio/api/v1/story-jobs/${jobId}`, {\n      headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}` },\n    })\n    if (!res.ok) throw new Error(`poll ${res.status}`)\n    const { job } = await res.json()\n    if (['READY', 'PARTIAL', 'FAILED', 'CANCELED'].includes(job.status)) return job\n    await new Promise((r) => setTimeout(r, 5000))\n  }\n}","responseExample":{"job":{"id":"cmj...","status":"READY","artifacts":[{"id":"cma...","type":"table_read","status":"READY","manifest":{"type":"table_read","status":"ready","shareToken":"<jwt>","theaterUrl":"https://sleeperhit.studio/share/table-read/<token>","theaterFullscreenUrl":"https://sleeperhit.studio/share/table-read/<token>?view=fullscreen","audio":{"mode":"durable_recording","liveUrl":"https://sleeperhit.studio/share/table-read-audio/<token>","recordingUrl":null},"video":{"status":"not_rendered","renderUrl":"/api/v1/artifacts/cma.../render-video","url":null},"expiresAt":"2026-06-28T15:20:00.000Z"}}]}}},{"title":"Read actual script content","description":"After the table-read artifact is READY, GET the backing script by page, scene, character, or entry range. Use this for terminal/MCP agents that need to answer \"what is on page 4?\" or \"which scene is strongest?\" from the real generated text.","request":{"method":"GET","path":"/artifacts/{artifactId}/script?scope=page&page=1","headers":{"Authorization":"Bearer sh_<prefix>_<secret>"}},"curl":"curl -s \"https://docs.sleeperhit.studio/api/v1/artifacts/$ARTIFACT_ID/script?scope=page&page=1\" \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\"","node":"const artifact = job.artifacts.find((a) => a.type === 'table_read')\nconst scriptRes = await fetch(`https://docs.sleeperhit.studio/api/v1/artifacts/${artifact.id}/script?scope=page&page=1`, {\n  headers: { Authorization: `Bearer ${process.env.SLEEPERHIT_API_KEY}` },\n})\nconst { script } = await scriptRes.json()\nconsole.log(script.selection.entries.map((e) => `${e.character}: ${e.text}`).join(\"\\n\"))","responseExample":{"script":{"artifactId":"cma...","scope":"page","pageCount":4,"totalEntries":42,"scenes":[{"sceneIndex":0,"heading":"INT. LAB - NIGHT","startEntry":0,"endEntry":12}],"characters":[{"character":"NARRATOR","lineCount":8,"wordCount":140}],"selection":{"pageNumber":1,"startEntry":0,"endEntry":10,"entries":[{"entryIndex":0,"character":"NARRATOR","text":"The lab is quiet except for the relay clicks.","isNarration":true,"sceneHeading":"INT. LAB - NIGHT","parenthetical":null,"page":1}]}}}},{"title":"Open a player or embed the durable recording","description":"Three ready-to-open players, all authorized by the share token: `theaterUrl` (framed theater), `theaterFullscreenUrl` (immersive, chrome-free), and `audio.liveUrl` (audio-only). The audio player serves the durable MP3 auto-rendered when the read is READY — no live session needed. Poll GET /artifacts/{id} until `audio.finalize.status === \"complete\"`, then read `audio.recordingUrl` for the direct MP3 URL. The opt-in MP4 lives at `video.renderUrl` (separate charge).","request":{"method":"GET","path":"/artifacts/{artifactId}","headers":{"Authorization":"Bearer sh_<prefix>_<secret>"}},"curl":"curl -s \"https://docs.sleeperhit.studio/api/v1/artifacts/$ARTIFACT_ID\" \\\n  -H \"Authorization: Bearer sh_<prefix>_<secret>\"","node":"const artifact = job.artifacts.find((a) => a.type === 'table_read')\nconst manifest = artifact.manifest\n// Ready-to-open players (share token authorizes them):\nconsole.log(manifest.theaterUrl)            // framed theater\nconsole.log(manifest.theaterFullscreenUrl)  // immersive, no chrome\nconsole.log(manifest.audio.liveUrl)         // audio-only player (serves durable MP3)\n// Poll until the durable MP3 is ready, then read the direct URL:\n// manifest.audio.finalize.status === \"complete\" → manifest.audio.recordingUrl"}]}]}