Sleeper Hit makes produced episodes: the script, the voices and the mix. Hear one

Sleeper Hit Docs

CLI

Install, configure, and automate Sleeper Hit Studio from a terminal.

View as Markdown

Command-line client for the Sleeper Hit Studio B2B Story API (/api/v1). It maps 1:1 onto the documented endpoints — create projects, build a Series Bible, map and run a podcast season, generate gated episodes, publish an RSS feed, and open the live table-read theater.

The endpoints, auth, error envelope, and flow mirror the Story API docs (quickstart, concepts, API reference). Nothing here contradicts those docs — the CLI is a thin, scriptable wrapper around the same HTTP surface.

Requirements

  • Node.js >= 20 (uses the built-in global fetch and node:crypto.randomUUID).
  • A Sleeper Hit Studio customer API key. Create one at https://sleeperhit.studio/dashboard/developers.
  • Or sign in from the browser with sleeperhit login. A plain login asks for every scope (the approval page lists each one); --scopes story:read,story:write asks for exactly that list instead. The key it saves lasts 90 days; logging in again renews it and revokes the key the previous login saved, and sleeperhit logout revokes it on the server. sleeperhit whoami prints the key's scopes and expiry. The saved file is shared with the stdio MCP server's login tool.

Install

npm install -g @sleeperhit/cli sleeperhit --help

Or run it once without installing: npx @sleeperhit/cli --help. The package is one bundled file with no runtime dependencies.

From the monorepo

The package lives at packages/cli and is picked up by the packages/* workspace glob:

pnpm install pnpm --filter @sleeperhit/cli build # bundles src (and @sleeperhit/shared) -> dist/index.js cd packages/cli pnpm link --global # now `sleeperhit` is on your PATH

Run without building (dev)

# from packages/cli pnpm dev -- projects list # or directly with a TS-aware runner: pnpm exec tsx src/index.ts projects list

Configuration

Env varRequiredDefaultPurpose
SLEEPERHIT_API_KEYyes—Bearer key, sh_<prefix>_<secret>. Sent as Authorization: Bearer <key>.
SLEEPERHIT_BASE_URLnohttps://sleeperhit.studioOrigin to target. /api/v1 is appended automatically. Point at https://stage.sleeperhit.studio or a local dev origin when testing.
export SLEEPERHIT_API_KEY="sh_xxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # optional export SLEEPERHIT_BASE_URL="https://sleeperhit.studio"

The CLI auto-generates an Idempotency-Key (UUID) for every POST. Override it with --idempotency-key <key> when you want a stable key tied to your own job record for safe retries.

Errors are printed in the standard { error: { code, message, requestId } } form. On rate_limited the exact Retry-After is surfaced; jobs watch honors it automatically. The CLI exits non-zero on any error (1 API/runtime, 64 usage, 2 when a requested operation is unavailable in the current environment).

Commands

login [--scopes <list>] [--no-browser] [--base-url <url>]   # every scope unless --scopes narrows it; 90-day key; revokes the previous login's key
logout                                                      # revokes the saved key on the server, then removes it
whoami [--api-key <key>]                                    # the key's scopes and expiry
capabilities
guidance
credits

projects create --name <name> [--description <text>] [--metadata <json>]
projects from-script <scriptId> | --script <scriptUploadId>
projects list [--limit N] [--cursor C] [--role writer|creator|business]   # a recurring show also prints "262 episodes · Next: tonight 7 pm Central · Latest S4E429 · 10:56 · Oct 19" on stderr
projects get <projectId>        # prints the project JSON, then its workspace gate and table-read cast readiness on stderr
projects episodes <projectId> [--season N]          # seasons, episodes (made in the app included), versions, videos
projects seasons [list] <projectId> [--season N] [--months N]  # the full arc: seasons, episodes, the next step
projects seasons add <projectId> [--name <text>]     # name a new season (free)
projects episodes start <projectId> <episodeId>      # a Series Bible episode as an empty draft
projects update <projectId> [--name <text>] [--description <text>]
        [--metadata <json>] [--archived true|false]

bible get <projectId>
bible save <projectId> --json '<object>' | --file <json> [--title <text>]
bible generate <projectId> [--instructions <text>]
bible coverage <projectId> [--generate] [--focus <text>]
outline coverage <projectId> [--generate] [--focus <text>]

season-runs create <projectId> [--title <text>] [--season-number <n>]
        [--publishing-series-id <id>] [--coverage-policy <json>] [--run-key <key>]
season-runs list <projectId>
season-runs get <runId>
season-runs map <runId> <episodeId> --expected-map-version <n>
        [--patch <json>] [--title <text>] [--summary <text>] [--objective <text>]
        [--audience <text|null>] [--tone <text|null>] [--notes <text|null>]
        [--duration-minutes <n>] [--episode-number <n>]
        [--confirmed true|false] [--confirm]
season-runs approve <runId> --expected-map-version <n> --confirm
season-runs advance <runId> [--episode-id <id>]
season-runs replan <runId> [--episode-id <id>] [--instruction <text>] --confirm
season-runs retry-coverage <runId> [--episode-id <id>] --confirm
season-runs revise <runId> [--episode-id <id>] --instruction <text> --confirm
season-runs publish <runId> [--episode-id <id>] --confirm
season-runs end <runId> --confirm
season-runs delete <runId> --confirm

sources add <projectId> --type text|markdown|url|pdf
        [--content <text> | --file <path>]   (text / markdown)
        [--uri <http(s) url>]                (url / pdf)
        [--label <text>] [--retention EXTRACTED_ONLY|STORE_ORIGINAL|METADATA_ONLY]
        [--metadata <json>]                  use {"sourceContextMode":"full"} for complete planner + writer context
sources list <projectId> [--limit N] [--cursor C]
sources get <projectId> <sourceId>

capture record <tour.json> --out <dir> [--storage-state <file> | --storage-state-env <VAR> |
        --login-url <url> --user <name> --password-env <VAR> [--totp-env <VAR>] | --redeem-url <url> --code-env <VAR>]
        [--extra-origin <origin>]... [--origin-header-env <Header-Name>=<VAR>]... [--secret-env <secretRef>=<VAR>]...
        [--sandbox [--egress-proxy <url>]] [--audio-from <url|file> [--offset <seconds>]]
        [--browser chrome|chromium] [--app-version <tag>] [--upload --project <projectId> [--label <text>]]
        record one take of an app tour on this machine (needs the optional recorder package,
        @sleeperhit/capture: Playwright, Google Chrome, ffmpeg); --upload then imports the take. A protected
        preview: --origin-header-env sends a header only to the app's origins; --secret-env names the variable
        holding each secret the tour types by secretRef (typed only into the app). Values come from environment
        variables by name and are never printed. A stored login: --login-url signs in through the app's own
        form first (with the current code from --totp-env's authenticator secret when it asks), --redeem-url
        POSTs a magic link's one-time code; the sign-in runs in its own browser, closed before the take. Free.
capture import <file|take-dir> --project <projectId> [--label <text>] [--origin-url <url>] [--route <path>] [--app-version <text>] [--tour-id <tourId>] [--feature-id <featureId>]
        a real screen recording of an app (MP4: H.264, constant 30 fps, at most 1920x1080, bt709), or a
        capture record take's directory with its events.json, stills and tour, uploaded straight to storage,
        then checked: ready, or rejected with the ffmpeg command that fixes it. --tour-id names the stored
        tour whose run it is (its feature is copied on); --feature-id names the product feature it shows. Free.
capture list --project <projectId> [--feature-id <featureId>] [--tour-id <tourId>]
capture get|finalize|delete <captureId> --project <projectId>
capture set-feature <captureId> --project <projectId> (--feature-id <featureId> | --none)
        set the product feature a capture shows, or none; choosing the feature a match named confirms it. Free.
capture match-features --project <projectId> [--apply]
        match the captures that name no feature to the feature each most likely shows (its name, page and tour
        against each feature's): a dry run; --apply tags the strong matches and leaves the rest to you. Free.
capture runner --token-env <VAR> [--port 8791] [--host 127.0.0.1] [--browser chrome|chromium]
        [--box <name>] [--isolation vm|container|shared-host] [--gpu metal|nvidia|none]
        [--app-origin <https://origin>]... [--allow-from <ip>]... [--min-draw-fps <n>]
        [--egress-proxy <http://host:port>] [--no-chrome-sandbox]
        serve "Test sign-in" from this machine (the capture-runner image): Sleeper's worker asks it, over requests
        signed with the token, to sign in to an app and say what it saw. Logins arrive sealed to a key this process
        makes and keeps in memory. The browser always leaves through an egress proxy that refuses private addresses:
        --egress-proxy (smokescreen), or the runner's own, which resolves every name and connects only to the public
        address it checked. The proxy is proven to refuse the metadata address, private ranges, the tailnet and
        loopback before the runner serves and before every test. It advertises what it can do (GET
        /v1/capabilities: box, platform, GPU, app sound, isolation, busy, a self-bench of a heavy page and whether
        that makes it degraded, the app origins it opens). A shared-host runner (a Mac's default) opens only the
        --app-origin list it is given. Point Sleeper at it with CAPTURE_RUNNERS='<box>=<url>' and
        CAPTURE_RUNNER_TOKEN_<BOX>. A capture box on a Mac: docs/capture-box.md.
capture egress-proxy [--port 4750] [--host 127.0.0.1]
        serve the runner's guarded egress proxy on its own (a capture box runs it as a separate user); point
        capture runner --egress-proxy at it.

app connect --project <projectId> --url <origin> [--label <text>] [--login-url <url>]
        [--user <name>] [--password-stdin | --password-env <VAR>] [--totp-env <VAR>]
        [--cf-access-id <id> --cf-access-secret-env <VAR> | --vercel-bypass-env <VAR>]
        [--storage-state <file> | --storage-state-env <VAR>] [--provider <url> --grant-env <VAR>]
        [--extra-origin <origin>]... [--mask <selector>]... [--retention-days <n>] [--logged-in-when <json>]
        store the login hosted capture uses to sign in to your app (demo or test accounts only). Each secret
        is sealed when it is saved and is never shown again by any surface; it comes from stdin or a named
        environment variable, never the command line. --provider with --grant-env stores a Sleeper capture
        grant instead: your app's sleeper.capture/1 endpoint hands out a fresh demo session for each recording,
        so no password is stored. Needs a key with the app:connect scope. Free.
app list --project <projectId>         which secrets each connection has saved (never their values)
app get|delete <connectionId> --project <projectId>
app preview <connectionId> <testId> --project <projectId>
        Read only this exact check’s safe metadata and owner viewUrl. Starts nothing and takes no
        picture. No image download (--save is refused), code input, or automatic replacement-test tracking.
app test <connectionId> --project <projectId> [--no-wait | --result] [--save-screenshot <file>]
        test sign-in: a machine that records captures signs in to the app as a recording would and keeps a
        picture of what it saw (yours alone). Waits for the outcome and prints it in plain words, with a private
        link to the picture (or saves it with --save-screenshot). Exit 1 while a secret is missing or when the
        sign-in failed. A running test can wait up to five minutes for a requested code: enter it only in that
        test on Connected apps; Story API, CLI, MCP and Slug never accept it. Retry there with a person available
        to enter a code. For unattended sign-in, save an authenticator setup key if supported, or use a saved
        session or capture grant. Sleeper cannot read email/texts, follow sign-in links, approve push requests,
        or use a passkey. Free.
app update <connectionId> --project <projectId> [--url <origin>] [--login-url <url>] [--login-method ...] [--header ...]
        adding an address deletes the saved secrets: a login is only used on the site it was saved for; a new --provider
        endpoint deletes the saved grant
app secret <connectionId> <kind> --project <projectId> (--value-stdin | --value-env <VAR>)
app learn --project <projectId> [--url <site>] [--no-wait] [--wait <seconds>]
        learn a product project's product from its public site and docs (pages, llms.txt, sitemap, OpenAPI; up to
        24 pages, robots.txt obeyed): its features, each with the pages it was read from, claims, pricing and brand
        colours. Public pages only; nothing secret is sent. Waits for the run by default. Free, up to 10 runs a day.
app inventory --project <projectId>    the newest run and the product's feature inventory
app claims --project <projectId>       every claim the product's site makes, with the founder's decision on each
app claim <claimId> --project <projectId> (--confirm | --reject | --reopen) [--note <text>]
        the founder's decision on one claim: confirm (may be said), reject (never said) or reopen. The product
        Bible written next (bible generate on a product project) follows it. Free.

tour list --project <projectId> [--feature <featureId>]
        the project's stored tours — one JSON walkthrough of a product feature each — newest first,
        with the feature it walks through, its revision and its re-run check; and the project's
        features with how many tours each holds (what a create binds to)
tour get <tourId> --project <projectId>
tour create --project <projectId> --feature <featureId> --file <tour.json> [--label <text>]
        store a tour for one feature of the project (a Series Bible episode). The document is
        checked with the recorder's own rules first: one that cannot run is refused with those
        problems, verbatim, and nothing is stored. Free.
tour update <tourId> --project <projectId> [--file <tour.json>] [--feature <featureId>] [--label <text>] [--expected-revision <n>]
        --expected-revision (the revision you last read) makes the change safe against a second
        writer: a stale one is refused and nothing is written
tour delete <tourId> --project <projectId>
tour check [<tourId>] [--file <tour.json>] --project <projectId>
        check a tour — the stored one (<tourId>) or the document in hand (--file), one of the two —
        without storing anything: the recorder's own problems, what a recording needs (its secrets)
        and what it hears. Exits 1 when it is not ok. Free.
tour draft --project <projectId> --feature <featureId> [--connection <connectionId>] [--wait]
tour drafts --project <projectId> [--feature <featureId>]
tour draft-status <runId> --project <projectId>
        DRAFT a tour for one product feature: Sleeper writes the walkthrough from the feature's Bible
        entry, its pages and the app connection's address, checks it the way `capture record` would and
        saves it as a DRAFTED tour you edit before anything is recorded — the app is never opened and no
        frame is recorded. `tour draft` answers at once (202 when a run starts, 200 when one is already
        drafting — a repeat never starts a second run); `--wait` polls the run every 3 seconds for up to
        10 minutes and exits 1 when the run failed — or is still drafting when the window closes. `tour
        drafts` lists the project's runs and `tour draft-status` reads one: its status, what the drafting
        held back and the staged tour. Free, up to 50 runs a day.

admin demo-accounts | admin demo-account add|remove <userId>
        ADMINS ONLY. The demo-account allowlist (dedicated non-admin accounts agents make showcase videos with)
admin demo-session --user <userId> [--ttl 2h] [--out <file>] | admin demo-session revoke <sessionId>
        ADMINS ONLY. A real sign-in as a demo account (no impersonation banner), at most 8h; the Playwright storage
        state goes to a mode-600 file outside any repository, never stdout. `capture record … --demo-user <userId>`
        does the same in memory for one take and ends the session after it.
admin capture-runners | admin capture-runner allow <connectionId> [--note <text>] | admin capture-runner disallow <connectionId>
        ADMINS ONLY. The capture runners Sleeper records on (never a token): what each can do now (GPU, app sound,
        isolation, busy, its bench and whether it is degraded, the origins it opens), and the shared-host
        allow-list: the connections a shared-host runner (a roscoe) may record. Sleeper's own and approved pilots only.
admin capture-runner route [--connection <id>] [--sound app|off] [--draw-fps <n>] [--runner <name>] [--purpose record|sign-in-test]
        ADMINS ONLY. Where a take would record now, its candidates in order and why. Nothing is recorded.
destination list
        where a finished video can go and what each place asks of it: its shape and length, its sound, what it
        must show and the checks it must pass. A row whose spec fails its check is listed as unavailable, never
        guessed; `specMayBeStale` means its spec was last checked more than 90 days ago. Free.
destination get <key>

plans create <projectId>
        --target audience=...,objective=...,outcome=...   (repeatable)
        --artifact type=table_read,mode=podcast|documentary|drama[,narrationPolicy=auto|include|suppress][,durationSeconds=N]
        [--brief genre="..." --brief audience="..." --brief pageTarget=4]
        [--source <sourceId> ...] [--title <text>] [--auto-approve]  (non-table artifacts only)
        [--json '<full payload>']    power-user escape hatch
plans list <projectId> [--limit N] [--cursor C]
plans get <planId>
plans approve <planId> --confirm
plans reject <planId> [--reason <text>]

jobs create <planId> [--project <projectId>] [--artifact type=...,mode=...]
        [--voice-map '{"GARY":{"voiceId":"...","provider":"hume"}}'] [--script <scriptUploadId>]
jobs list [--project <id>] [--plan <id>] [--limit N] [--cursor C]
jobs get <jobId>
jobs watch <jobId> [--interval <ms>]   Poll until terminal; print manifest URLs on READY
jobs cancel <jobId>
jobs artifacts <jobId>

artifacts get <artifactId> [--revision N]
artifacts render-video <artifactId>

script get <artifactId> [--scope all|page|scene|character|range] [--page N] [--scene N]
        [--character <name>] [--start N --end N] [--limit N]
script page <artifactId> <page>
script scene <artifactId> <sceneIndex>
script character <artifactId> <character...>
script replace <artifactId> --file <path> [--instruction <text>] [--narration-policy include|suppress]
script upload --title <title> (--file <path> | --source <sourceId> | --file-url <url> [--file-name <name>])
        [--format text|fountain|fdx] [--project <projectId>] [--season N] [--episode <episodeId>]
        # a new script in no project, or in --project; --season files it in that season and --episode
        # links it to a planned episode. From a file: --file-url adds it (needs --project) and answers
        # waiting; check the source, then --source <sourceId> makes it. One source makes its script once.
script show <scriptId>                    # the script's id, its Writers Lab URL id, or any version's id
script update <scriptId> [--title <t>] [--slug <s>] [--logline <text>] [--media-type film|tv]
        [--genre <text>] [--season N] [--clear logline|genre|media-type]...
script versions add <scriptId> (--file <path> | --source <sourceId> | --file-url <url> [--file-name <name>]) [--format text|fountain|fdx]
script write <scriptUploadId> --file <path> [--expected-file <path>]   # a draft's or version's whole text
script finish <scriptUploadId> [--coverage adopt|fresh]                # the recommendation, then finish
script versions delete <scriptId> <scriptUploadId> --confirm
script export <scriptUploadId> --format fountain|fdx|pdf [--out <path>] [--no-title-page]
        [--from-page N] [--to-page N] [--font screenplay-sim|courier-prime|courier-new|final-draft]
script overrides get <scriptUploadId>
script overrides update <scriptUploadId> [--avatar <text>] [--theater <text>] [--soundscape <text>]
        [--pitch-deck <text>] [--locations <text>] [--clear <override>]...
script table-read prepare <scriptUploadId> [--confirm]
        Prepare a cast episode's table read (the Writers Lab's Continue to Table Read): prints the price;
        --confirm charges it once for this screenplay version and adds its sound effects and music.

cast <artifactId>
cast update <artifactId> --json '<entries-or-{entries,avatarStyle}>'
cast restyle <artifactId> --style <s>
avatar set <artifactId> --character <c> [--style <s>] [--refine <r>] [--reference-image-url <u>]
voice recast <artifactId> --character <c> --voice-id <id> --voice-name <name> [--gender <g>] [--provider <p>]
voice modify <artifactId> --start <n> --end <n>
        [--effect autotune] [--key C..B] [--scale major|minor|majpent|minpent|chromatic]
        [--strength <0..1>] [--smooth <int 1+>] [--reverb none|light|chapel]

refine <artifactId> <instruction...> [--scope auto|screenplay|plan]
finalize <artifactId> [--mode audio|video]

coverage generate <artifactId> [--focus <text>]
coverage get <artifactId> [--report <id>]
coverage watch <artifactId> [--interval <ms>]

music generate <artifactId> [--coverage <0..1|0..100>]
music update <artifactId> [--scope screenplay|scene|entry] [--scene <n>] [--entry <n>]
        [--mode merge|replace] [--prompt <text>] [--summary <text>] [--weight <0..2>]
        [--mute | --enable]
music get <artifactId>
music watch <artifactId> [--interval <ms>]

sfx add <artifactId> --entry <n> --label <text> --prompt <text>
        [--volume <0..1>] [--trigger before|with|after] [--offset-ms <n>] [--duration <s>] [--disable]
sfx update <artifactId> --id <cueId>
        [--entry <n>] [--label <text>] [--prompt <text>] [--volume <0..1>]
        [--trigger before|with|after] [--offset-ms <n>] [--duration <s>] [--regenerate]
        [--enable | --disable]
sfx list <artifactId>
sfx remove <artifactId> --id <cueId>

Run sleeperhit --help for the same reference at the terminal.

product-video

Plan a product video from the feature map (P-101): one feature, or a named release and its features, planned from the project's recorded captures into an ordinary capture-only draft trailer — each beat names its capture, the stretch it plays, its narration line, whether it keeps its own sound, and its depth. Free: nothing is charged, reserved or rendered. The run is polled until it is ready; the draft is approved afterwards with sleeperhit trailer approve-plan, behind its plan check.

product-video plan --project <projectId> (--feature <featureId> | --release <name> --features <id,id,...>)
        [--seconds <n>] [--aspect 16:9|9:16|1:1] [--title <text>] [--notes <text>] [--reference <REF-id>] [--destination <key>] [--music score|none] [--wait]
        Start a plan run: one feature, or a named release's 2-8 distinct features. --seconds is 10-360 (default
        30 for a feature, 60 for a release); every capture the plan plays must have --aspect's shape. --destination
        names the destination the plan is cut for (a video destination key); left out, the plan follows the
        deliverable it is made from. --music is score (the default: the draft is scored to picture, free; a sketch
        to the planned cut plays before anything renders) or none (no music bed). --wait
        polls every 3 s for up to 10 minutes, prints each status change and then the run and its plan check,
        and exits 1 on a failed run or a blocked plan check.
product-video runs --project <projectId>       the project's plan runs, its feature map and the eligible capture count per shape
product-video run <runId> --project <projectId> one plan run, with its draft trailer and its live plan check
product-video coverage <jobId>                  a planned draft's plan check (its approval gate); exits 1 when it is blocked
product-video references --project <projectId> [--scope feature|release] [--category <name>] [--aspect 16:9|9:16|1:1] [--seconds <n>]
        Up to four reference films from refs.video for the video, each credited "References by refs" with its refs
        link. Free. Pass one to `plan --reference <REF-id>`: the plan is paced by its numbers, never its content.
product-video reference <jobId> [--choose <REF-id> | --clear]   read, choose or clear a video's reference (free)
product-video check-reference <jobId> [--confirm]
        Compare the video's latest master with its reference: an advisory score out of 100 that never blocks
        approval, finishing or export. Without --confirm it prints the quote (2 units of the shared monthly
        reference allowance, no credits); --confirm starts it — read it back with `product-video reference <jobId>`.

Post-creation artifact operations

refine, finalize, cast, avatar, voice, coverage, music, and sfx are wired against the live /api/v1/artifacts/{artifactId} operation families. They mutate or analyze the same table read artifact while keeping the artifact id and share URLs stable. Commands that queue work return JSON immediately; use the matching watch command or poll artifacts get until the status is terminal.

Voice commands name the action: voice recast assigns a character's voice, voice restore returns its previous voice and recording, and voice modify applies an effect over a line range. voice revoice speaks a character's lines again in a finished read; it quotes first and requires --confirm to spend.

cast-voice set pins a voice on the project cast. Casting coverage has two actions: cast-voice coverage reads the saved report, while cast-voice judge runs it again with the current cast. These keep their project scope.

End-to-end example

Copy-pasteable. Replace the captured IDs as you go (or pipe through jq).

export SLEEPERHIT_API_KEY="sh_..." # 1. Confirm the table read is available and you have credits sleeperhit capabilities | jq '.artifacts.table_read.availability' sleeperhit credits | jq '.credits.balance' # 2. Create a project PROJECT_ID=$(sleeperhit projects create --name "Q3 Launch Story" \ --description "Product launch table read" | jq -r '.project.id') # 3. Attach an inline text source (instant READY) SOURCE_ID=$(sleeperhit sources add "$PROJECT_ID" \ --type text --label press-release \ --content "Acme Corp launches its next-gen platform in Q3 2026, cutting onboarding time 60%." \ | jq -r '.source.id') # 4. Generate the plan PLAN_ID=$(sleeperhit plans create "$PROJECT_ID" \ --target audience="enterprise buyers" \ --target objective="introduce the Q3 platform launch" \ --target outcome="book a demo within 7 days" \ --target tone="confident, specific" \ --artifact type=table_read,mode=documentary \ --source "$SOURCE_ID" | jq -r '.plan.id') # 5. Poll until REQUIRES_APPROVAL, review the exact plan, then cross the human hold sleeperhit plans get "$PLAN_ID" | jq '{status:.plan.status, plan:.plan.plan, total:.plan.quote.total}' sleeperhit plans approve "$PLAN_ID" --confirm # 6. Create the job (reserves credits) and watch it to completion JOB_ID=$(sleeperhit jobs create "$PLAN_ID" | jq -r '.job.id') sleeperhit jobs watch "$JOB_ID" # On READY, watch prints: # Theater: https://sleeperhit.studio/share/table-read/<token> # Theater (full): https://sleeperhit.studio/share/table-read/<token>?view=fullscreen # Audio (live): https://sleeperhit.studio/share/table-read-audio/<token> # Video render URL: /api/v1/artifacts/<artifactId>/render-video # 7. (Optional) opt into a durable MP4 ARTIFACT_ID=$(sleeperhit jobs get "$JOB_ID" \ | jq -r '.job.artifacts[] | select(.type=="table_read") | .id') sleeperhit artifacts render-video "$ARTIFACT_ID"

The shareToken baked into the theater/audio URLs authorizes them — not your API key — so you can hand those URLs directly to end users.

A table read performs the project's cast; it never makes one. From a plan, it starts only when the project's cast canon covers every character — a voice for everyone and, unless the project is audio-only, a headshot, body shot and character sheet. sleeperhit projects get <projectId> prints who still needs what (project.tableReadReadiness); a job that would refuse is a 409 cast_precondition_failed before anything is created or charged.

For voices the cast canon does not carry, first use sleeperhit voices list, then provide a character-to-voice map on an explicit table-read artifact request:

sleeperhit jobs create "$PLAN_ID" \ --artifact type=table_read,mode=documentary \ --voice-map '{"GARY":{"voiceId":"voice_gary","provider":"hume"},"MAEVE":{"voiceId":"voice_maeve","provider":"hume"}}'

--voice-map requires exactly one --artifact type=table_read request. Each entry requires voiceId; voiceName, gender, and provider are optional.

To perform a finalized, cast-ready screenplay as written (no new pages, no coverage rewrite), adopt its version id:

sleeperhit jobs create "$PLAN_ID" --script "$SCRIPT_UPLOAD_ID"

--script uses the single table-read request (adding one when none is given) and cannot be combined with --voice-map.

Use it in your own agent workflow

The CLI emits JSON on stdout and human progress on stderr, so it composes cleanly inside scripts or an agent's tool layer:

#!/usr/bin/env bash set -euo pipefail export SLEEPERHIT_API_KEY="${SLEEPERHIT_API_KEY:?set your key}" ARTICLE_URL="$1" PROJECT_ID=$(sleeperhit projects create --name "Agent run $(date +%s)" | jq -r '.project.id') # Async URL source — poll to READY before planning SOURCE_ID=$(sleeperhit sources add "$PROJECT_ID" --type url --uri "$ARTICLE_URL" | jq -r '.source.id') until [ "$(sleeperhit sources get "$PROJECT_ID" "$SOURCE_ID" | jq -r '.source.status')" = "READY" ]; do sleep 3 done PLAN_ID=$(sleeperhit plans create "$PROJECT_ID" \ --target audience="general" --target objective="summarize the article" --target outcome="share a 90s table read" \ --artifact type=table_read,mode=podcast,narrationPolicy=include,durationSeconds=90 \ --source "$SOURCE_ID" | jq -r '.plan.id') # Wait for the async planner, then stop for exact human review. until [ "$(sleeperhit plans get "$PLAN_ID" | jq -r '.plan.status')" = "REQUIRES_APPROVAL" ]; do sleep 4; done sleeperhit plans get "$PLAN_ID" | jq '.plan | {title, plan, quote}' read -r -p "Approve this exact plan? [y/N] " APPROVE case "$APPROVE" in y|Y|yes|YES) ;; *) exit 1 ;; esac sleeperhit plans approve "$PLAN_ID" --confirm JOB_ID=$(sleeperhit jobs create "$PLAN_ID" | jq -r '.job.id') sleeperhit jobs watch "$JOB_ID" # blocks, streams progress, prints share URLs on READY

For a power-user payload that exactly matches the API schema, bypass the flag helpers with --json:

PLAN_ID=$(sleeperhit plans create "$PROJECT_ID" --json '{ "title": "Launch narrative", "target": { "audience": "enterprise buyers", "objective": "introduce the launch", "outcome": "book a demo" }, "artifactRequests": [{ "type": "table_read", "mode": "drama", "narrationPolicy": "suppress", "durationSeconds": 120 }], "styleConstraints": { "voicePreference": "authoritative, not salesy" }, "sourceIds": ["'"$SOURCE_ID"'"] }' | jq -r '.plan.id') # After it reaches REQUIRES_APPROVAL, review the exact plan with the user. sleeperhit plans get "$PLAN_ID" | jq '.plan | {title, plan, quote}' sleeperhit plans approve "$PLAN_ID" --confirm

See also

  • Quickstart — the same flow as raw HTTP / Node.
  • API reference — every endpoint, field, and error code.
  • Concepts — the plan/job/artifact model and live post-creation operations.
  • GET /api/v1/openapi.json — the live OpenAPI 3.1 spec.

License

MIT. See LICENSE.