---
title: "Cancel a story job and release its reserved credits."
method: POST
path: "/story-jobs/{jobId}/cancel"
tags: ["Jobs"]
---

# Cancel a story job and release its reserved credits.

`POST /story-jobs/{jobId}/cancel`

## Request

- Base URL: `https://docs.sleeperhit.studio/api/v1`
- URL: `https://docs.sleeperhit.studio/api/v1/story-jobs/{jobId}/cancel`
- Auth: HTTP bearer

Valid only while the job is `PENDING` or `RESERVED`, or `FAILED` before it ever started (a refused reservation or a lost enqueue) — canceling that one means it can no longer be resumed. A `FAILED` job that had started generating is refused. Releases any reserved (not-yet-settled) credit line items. Requires `Idempotency-Key`; canceling an already-canceled job is a no-op.

## Headers

- `Idempotency-Key` string, required

## Response `200`

Canceled job envelope.

- StoryJobEnvelope
  - `job` StoryJob, required: A queued artifact-generation job against an APPROVED StoryPlan. Holds the reserved credit quote, durable progress/checkpoint state, and generated artifacts. Provider-capacity recovery may move RUNNING back to RESERVED with progress.stage=provider_backoff until the core-managed retry wakes it.
    - `id` string, required
    - `label` string, required: What the job is doing, what is ready, or what failed, in plain words: "Making the trailer", "The trailer is ready", "Making the trailer failed". The same words GET /background-activity gives its task.
    - `projectId` string, required
    - `storyPlanId` string, required
    - `status` 'PENDING' | 'RESERVED' | 'RUNNING' | 'PARTIAL' | 'READY' | 'FAILED' | 'CANCELED', required: Lifecycle of a queued artifact-generation job. `RESERVED` means credits are held; when progress.stage is `provider_backoff`, Sleeper Hit has scheduled a delayed retry and clients should continue polling. `PARTIAL` means at least one artifact is READY while others are still running; `READY` is fully complete. Cancelable while `PENDING` or `RESERVED`, or `FAILED` before it ever started (canceling that one means it can no longer be resumed); a `RUNNING` job, and a `FAILED` job that had started generating, cannot be canceled. `CANCELED` is final.
    - `artifactRequests` StoryArtifactRequest[], required
      - `type` 'pitch_deck' | 'table_read' | 'trailer', required
      - `mode` string
      - `deckMode` 'pitch' | 'showcase': For pitch_deck: what the deck is for. "pitch" (default) tells the approved story to a buyer and is judged as a pitch. "showcase" is an epic sizzle of the show's world, shown as its story: built on its intent (what the show is about, the one question it leaves open, an arc that builds to a turn), carried by a narrator and the characters' own lines, with world chapters (vistas, crowds, life, scale) that carry the theme interleaved with chapters of the characters in that world doing things; it is judged as a showcase, not as a pitch of the approved story. Every deck, in either mode, is planned from its intent. Set when the deck is planned; it never changes after.
      - `showcaseCharacters` string[]: For a showcase deck only: the characters it must feature, by their exact canon names, in the order they first appear (at most 12). Each is on camera in at least one chapter, among the world chapters. Omit to let the planner choose from the cast.
      - `shotDensity` 'held' | 'standard' | 'dense': For pitch_deck and trailer: how many angles (cuts) each rendered shot of a chapter or beat carries. Cuts inside a rendered shot are free, so density never changes the price, the plan or a line. "held" lets one long camera move carry a shot (1-2 angles); "standard" (default) cuts 2-3 angles into each shot where its seconds allow; "dense" cuts 3-5. No angle is planned under about 1.5 seconds, and a spoken line keeps its own seconds. Set when the deck or trailer is planned.
      - `durationSeconds` integer: Target duration in seconds. Video artifacts (trailer, pitch_deck) are planned at CUT density, not minimum-call density: round(target / mean) units of 4-30s each, with a ~5s mean trailer beat and a ~9s mean pitch-deck chapter (one render call, at most 10s) — so a 90s trailer plans about 18 beats and a 180s deck about 20 chapters. Each type carries its own ceiling: trailer 90s, pitch_deck 180s. Audio runtimes (table_read) are not unit-rendered and may run longer.
      - `aspectRatio` string: For a trailer, the shape the whole cut renders at: 16:9 (default), 9:16 or 1:1. Its shots, text composites and stitched master all follow it.
      - `videoModelResolution` '480p' | '720p' | '1080p': For a trailer, the class the finished cut is DELIVERED at: 720p or 1080p (default 720p; 480p delivers the default). Every shot renders at 480p first; the takes you keep are upscaled to this class when the trailer is finished, never re-rendered. Every class renders its first pass at 480p, finished by upscaling: 1 Studio Credit per second to render, plus 1 Studio Credit per second to finish at 720p or 4 Studio Credits per second at 1080p. For 90 seconds of generated footage, about 180 Studio Credits at 720p or 450 Studio Credits at 1080p. The exact per-shot quote, including location plates and on-screen text, is shown before anything is spent.
      - `includePdf` boolean: The deck's companion PDF, laid out from poster frames of the finished takes. Off unless true: decks ship without a PDF by default; render_pitch_deck_pdf renders one afterwards.
      - `narrationPolicy` 'auto' | 'include' | 'suppress': For table_read: auto lets the planner decide; include requests a narrator; suppress makes the read speaker-only. This is a user/plan choice, not tied to mode.
      - `deferMusic` boolean: For table_read: skip automatic soundtrack planning/rendering (SFX detection is unaffected). Use when you supply your own beds — attach them through POST /artifacts/{id}/music after the artifact exists. The manifest reports audio.adaptiveMusic=false with audio.musicFailure=null, since no music was requested rather than attempted and failed.
      - `deferSfx` boolean: For table_read: never SYNTHESISE a sound effect. Cues are still detected and placed, but each may only resolve to an effect the SFX bank already holds; anything with no match is left silent for you to fill via POST /artifacts/{id}/sfx. Use when the show has a fixed sound palette, so a one-off effect is never rendered, banked, or billed.
      - `deferAudioRender` boolean: For table_read: skip the automatic durable-MP3 render when the read reaches READY. Use when you run your own post-production — without it the whole read is synthesised once on ready, for a mix you then invalidate, and again on your finalize. You must POST /artifacts/{artifactId}/finalize yourself; until then `audio.recordingUrl` stays null and share players show a preparing state.
      - `coverageTarget` number: For table_read: minimum development-coverage score (out of 10) the screenplay must reach BEFORE the read is performed. Defaults to 8. Below the bar the script is rewritten against the coverage notes and re-scored, so parse/casting/voices/soundtrack/mix are never spent on a draft that did not clear it. Progress is reported as stage `coverage_gate`.
      - `coverageMaxRewrites` integer: For table_read: how many coverage-driven rewrites to attempt before performing the best draft. Defaults to 3. Set 0 to score without rewriting. Bounded because coverage scores are not monotonic — a rewrite can come back lower — so the best draft is kept rather than the last.
      - `punchUp` boolean: For table_read on a job that writes its screenplay: true runs a punch-up pass over the finished draft before it is scored or performed, one scene at a time. Every exchange is made to land, bits are topped, the register is brought up to what the notes and the Series Bible ask for (swearing included when they ask for it), and speeches, explained jokes and dead air are cut, at the same length or shorter. A scene keeps its punch-up only if every verbatim quote survives, it adds no neverSay term, it keeps its slugline, it adds no speaker and it grows by no more than 15%. A scene that only ran long is asked once to cut back to its length; any other failure, or a second miss, and the written scene stands. Default false. Not combined with scriptUploadId.
      - `neverSay` string[]: For table_read with punchUp: up to 20 words or phrases (each up to 40 characters) the punch-up must never introduce, matched as whole words, ignoring case. These are a show's hard lines, e.g. ["goddamn", "Jesus", "Christ"]. A punched scene that adds one keeps its written version instead. Requires punchUp.
      - `firstLineClean` boolean: For table_read with punchUp: true keeps the episode's first spoken line clean. If the punched opening scene's first spoken line swears and the written one did not, that scene keeps its written version (no retry). The writer's own first line is never changed. Requires punchUp.
      - `scopeKind` 'episode' | 'season' | 'project': For pitch_deck and trailer: "episode" (default, one installment), "season" (a whole season), or "project" (the whole project across all episodes).
      - `projectId` string: For season/project pitch decks and trailers: the StoryProject id (defaults to the plan's project).
      - `seasonNumber` integer: For season pitch decks and trailers: which season number.
      - `voiceMap` object: For table_read: a complete character-to-voice map applied before generation to skip automatic voice assignment. Use saved voice IDs from GET /voices. Each value requires voiceId; voiceName, gender, and provider are optional hints and authoritative saved-voice metadata wins.
      - `scriptUploadId` string: For trailer on a job: the script this episode cut is of (a ScriptUpload id in the plan's project; any version names its script). The cut is planned from that script's own newest completed table read or, for a story film with no read yet, from the film's own script, and never from another script's read. Without it an episode trailer is cut from the plan's own read, else a product project's one written story film (several are refused by name), else a story project's newest read. Not on a season or project trailer. For table_read on a job: perform this finalized, cast-ready screenplay version (a ScriptUpload id in the plan's project) as written, instead of writing a new one from the plan. Its episode cast must be complete — every voice and, unless the project is audio-only, every headshot, body shot and sheet — and its own screenplay coverage must be open (a report at or above the bar, or the writer's acknowledgement of it), or the job is refused before anything is created with 409 `cast_precondition_failed` (`details.reason`: `script_not_finalized`, `screenplay_coverage_before_cast` or `script_not_cast_ready`); a screenplay outside the project is 404 `script_not_found`. No coverage rewrite runs and no second coverage report is queued. Priced by its length the way Prepare Table Read prices a read (the table-read rate for 60 pages or fewer, or for over 60); the job's quote shows which. Charged once per screenplay version, both ways: a screenplay whose read is already paid for (prepared in the Writing Lab with POST /scripts/{scriptUploadId}/table-read/prepare, or held by another job) is quoted at 0 for the read and nothing is reserved, and a read the job holds is one a later prepare charges nothing for. Not combined with voiceMap, notes, durationSeconds, coverageTarget, coverageMaxRewrites, punchUp or neverSay.
      - `notes` string
    - `quote` StoryPlanQuote, required
      - `currency` 'studio_credits', required
      - `total` integer, required
      - `lineItems` StoryPlanQuoteLineItem[], required
        - `kind` 'planning' | 'source_digest' | 'artifact' | 'addon', required
        - `label` string, required
        - `artifactType` 'pitch_deck' | 'table_read' | 'trailer'
        - `opKey` string, nullable
        - `credits` integer, required
        - `note` string
      - `notes` string[], required: When `balanceAfter` is present and the price is above zero, the first note is the price and what it would leave (or how short the writer is, with the top-up link).
      - `computedAt` string, date-time, required
      - `balanceAfter` integer: StoryPlan reads only, and only while the plan is still a price before a spend: status READY or REQUIRES_APPROVAL, or APPROVED with no live StoryJob (none, or only FAILED/CANCELED ones), read by a caller holding `credits:read`. Absent otherwise: once create_job exists it has reserved `total`, so the plan is paid for; PENDING/RUNNING quotes are preliminary; REJECTED/SUPERSEDED/FAILED plans will not be bought; and never on a StoryJob's recorded quote. A live balance, never stored. Studio Credits the writer would have left once `total` is spent: the balance available now (as Billing shows it) minus `total`. Negative means they are that many short.
      - `sufficient` boolean: Present exactly when `balanceAfter` is. True when the available balance covers `total`. False means the writer must top up before creating the job; say so.
    - `creditReservationId` string, nullable, required
    - `progress` object, nullable, required
    - `modelUsed` string, nullable, required
    - `failureCode` string, nullable, required
    - `failureMessage` string, nullable, required
    - `usableArtifactId` string, nullable, required: The artifact you can act on, or null — the single "is there something to collect?" answer. Do not rebuild it from status + artifacts[] + failureMessage: a job can be FAILED with a complete read (only its planned soundtrack missed) or PARTIAL with a READY artifact, and reconstructing that has cost callers finished, paid-for work.
    - `failureClass` 'quota' | 'provider' | 'contract' | 'transient' | 'internal', nullable, required: What KIND of failure this was. `quota` = balance/billing ceiling (needs a top-up or rebind). `provider` = upstream model provider throttling (needs its cooldown). `contract` = we rejected your request deterministically; the identical request fails identically. `transient` = timeout/reset, worth retrying as-is. `internal` = unclassified defect on our side. Branch on this rather than pattern-matching failureMessage, which is prose and will change. Null unless the job is `FAILED` or `PARTIAL`: a `CANCELED` job keeps the failureCode/failureMessage it had before it was canceled, but it has no failure class.
    - `retryable` boolean, nullable, required: Whether repeating the SAME request could plausibly succeed later. False for contract (deterministic), quota, and provider (both need an external change first, so an immediate retry only adds load). Always false for a `CANCELED` job: cancel is final and resume refuses it. Null when there is no failure.
    - `runStale` boolean, required: True when the job is a RUNNING table read whose worker stopped answering: no heartbeat for five minutes, because its server died or was replaced mid-run. POST /story-jobs/{jobId}/resume sends its run again from the last checkpoint at no new cost (action `generation_requeued`); Sleeper Hit also sends it again on its own when its workers restart. False for every other job, including a RUNNING one that is still working: keep polling it.
    - `currentVersion` integer, required
    - `startedAt` string, date-time, nullable, required
    - `completedAt` string, date-time, nullable, required
    - `artifacts` StoryArtifact[], required
      - `id` string, required
      - `storyJobId` string, required
      - `type` 'pitch_deck' | 'table_read' | 'trailer' | 'production_video', required: `production_video` is a dialogue video made before storyboarding was retired (2026-10-09). Those artifacts are still read and published; no new ones are made.
      - `status` 'PENDING' | 'RUNNING' | 'READY' | 'FAILED', required: Lifecycle of an individual artifact within a job. `READY` means the manifest URLs are populated and the artifact is consumable.
      - `manifest` union, required
        - TableReadArtifactManifest: Manifest for a `table_read` artifact. The share token authorizes the returned URLs (not the customer API key), so the consumer can embed or hand them to end-users. The artifact id, share token, and all player URLs remain STABLE across refine operations. The MP4 `video` output and the durable MP3 `audio.finalize` are opt-in.
          - `type` 'table_read'
          - `status` 'ready'
          - `shareToken` string: Expiring JWT share token. Public but unguessable ("unlisted"). Stable across refines.
          - `theaterUrl` string, uri: Framed visual theater-mode page (`/share/table-read/{token}`) with attribution chrome. Stable across refines.
          - `theaterFullscreenUrl` string, uri: Immersive, chrome-free fullscreen theater (same in-app theater stage). Ideal for embeds/kiosk. Stable across refines.
          - `script` object: About the delivered screenplay.
            - `lengthNote` object, nullable: Why the delivered screenplay sits outside the approved page band, when it does (an estimate from word count, not layout). Null on a clean run.
            - `punchUp` object, nullable: What the punch-up pass did to the screenplay, when the request set `punchUp: true`. Null when it did not.
              - …
          - `audio` object: Audio output section. `liveUrl` opens the audio player (serves the durable MP3 once available). The durable MP3 is auto-rendered on ready — UNLESS the artifact request set `deferAudioRender`, or this is a season-run episode, in which case it is rendered only when you POST /artifacts/{id}/finalize.
            - `mode` 'durable_recording' | 'live_on_demand': `durable_recording` — auto-rendered MP3; `audio.recordingUrl` is populated once complete. (`live_on_demand` is a legacy value for older artifacts.)
            - `liveUrl` string, uri: Minimal-chrome, audio-only player page (`/share/table-read-audio/{token}`). Serves the durable auto-rendered MP3; shows a "Preparing…" state until the render is complete.
            - `recordingUrl` string, uri, nullable: Populated after a successful audio finalize (auto-render on ready, or explicit POST /artifacts/{id}/finalize). `null` until finalize is complete — and stays `null` indefinitely when `deferAudioRender` was set and you have not finalized yet.
            - `recordingDurationMs` integer, nullable: Duration of the finalized MP3 recording in milliseconds. Populated alongside `recordingUrl` on finalize completion.
            - `voiceMap` object: Current cast: character name → { voiceId, voiceName, gender, provider }. Recast a single character in place with POST /artifacts/{id}/voice (scope `artifact:publish`).
            - `modifications` ArtifactVoiceModification[]: Voice modification records applied to entry ranges on this read — one per POST /artifacts/{id}/voice-modification, newest last per range. This is where a dispatched modification is polled to completion, and the ONLY place a render failure explains itself.
              - …
            - `finalize` object: State of the durable MP3 finalize. The render is triggered automatically when the table read is READY. Poll GET /artifacts/{id} until `status` is `complete`, `failed`, `stalled` or `skipped`.
              - …
          - `video` object: Video output section. Status reflects real render state once the Remotion MP4 export has been triggered.
            - `status` 'not_rendered' | 'queued' | 'rendering' | 'complete' | 'stale' | 'needs_recording' | 'failed': Render pipeline stage. `not_rendered` until POST /artifacts/{id}/render-video or POST /artifacts/{id}/finalize?mode=video is called. `complete` = a current MP4 is available; `stale` = the read changed after the last render (re-render to refresh); `needs_recording` = finalize audio first.
            - `renderUrl` string: POST to trigger the opt-in Remotion MP4 render via the render-video endpoint (free).
            - `url` string, uri, nullable: Durable MP4 URL. Populated when `status` is `complete` (or `stale`).
          - `expiresAt` string, date-time, nullable
        - object
      - `tableReadShareLinkId` string, nullable, required
      - `creditLineItemIds` string[], required
      - `sourcePlanRevision` integer, nullable, required
      - `currentVersion` integer, required: Current manifest revision. v1 is the initial generation; each refine increments. Use ?revision=N on GET /artifacts/{id} to fetch a prior version.
      - `refineCount` integer, required: Number of times this artifact has been refined. 0 for the initial generation.
      - `failureCode` string, nullable, required
      - `failureMessage` string, nullable, required
      - `createdAt` string, date-time, required
      - `updatedAt` string, date-time, required
    - `createdAt` string, date-time, required
    - `updatedAt` string, date-time, required

## Other responses

- `401`: Missing, malformed, revoked, or expired API key.
- `403`: API key is valid but lacks the scope, is blocked by IP allowlist, or the account is disabled.
- `404`: The story job does not exist, was deleted, or is owned by another account.
- `409`: The job is in a state that does not allow the requested operation (e.g. canceling a terminal job).
- `429`: Rate limit for this API key + IP + route family was exceeded.
- `500`: The API failed unexpectedly. Safe to retry with the same Idempotency-Key.

---

[API](https://skmtc.dev/sleeperhit/apis/sleeper-hit-studio-story-api.md) · [All operations](https://skmtc.dev/sleeperhit/apis/sleeper-hit-studio-story-api/llms.txt) · [OpenAPI document](https://skmtc.dev/sleeperhit/apis/sleeper-hit-studio-story-api/revisions/4534f5d25540?raw)
