---
title: "Price, then render, a draft trailer's beat plan."
method: POST
path: "/trailers/{jobId}/plan/approve"
tags: ["Trailer"]
---

# Price, then render, a draft trailer's beat plan.

`POST /trailers/{jobId}/plan/approve`

## Request

- Base URL: `https://docs.sleeperhit.studio/api/v1`
- URL: `https://docs.sleeperhit.studio/api/v1/trailers/{jobId}/plan/approve`
- Auth: HTTP bearer

Approves a draft trailer's beat plan into the render — the next step of EVERY trailer job, which stops at its plan (a story job's `progress.trailerJobId`, `progress.nextStep: approve_trailer_plan`): approving THIS job renders the cut that was scored, while creating a new plan re-runs the stochastic planner and renders an ungraded draw. TWO CALLS, PRICE FIRST: sent WITHOUT `confirmed` it runs the coverage gate, compiles and voices every footage beat (free to the writer) and prices the 480p render — a line per call at its voiced seconds and a line per location plate the renders make first — reserving nothing (`quotedOnly: true`, `lines`, `calls`, `warnings`); sent again with `confirmed: true` it reserves exactly the quoted calls and starts the render (`rendering`, then `review`). Every call renders at 480p 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 automatically at our cost. Refused before the price with the action that fixes it (`details.refusal`, `details.blocked`): an unscored or failing plan, a narrated cut with no narrator, a beat whose cast has no body figure or cast voice. A TRAILER WITH NO SCREENPLAY (POST /story-projects/{projectId}/trailers) is approved here too: it needs at least one capture beat and every other beat a capture or a title card (else 409 `trailer_capture_only`, naming the beats), compiles and renders nothing, has no plan coverage to wait for, binds no video model and quotes 0 credits; the confirm assembles the cut from its captures. Preparing the price (voicing each line and planning each shot) can take a few minutes, so it runs in the background: an answer with `status: "preparing"` carries no price yet and reserved nothing — tell the writer, then ask again with the same call and the same inputs after `checkAgainSeconds` (about 20) until the price (or a refusal) comes back. Asking again while it is being prepared never starts a second one. A confirmed call that finds the price no longer prepared (the plan changed since) reserves nothing and answers the same way: price it again. A plan the gate refuses on the judge's scores alone says so (`details.overridable: true`, `details.overrideWhy`): the writer may render it anyway by sending `acknowledgePlanCoverage: { reason }` on the price and the confirm alike — only on their explicit choice. `details.overridable: false` cannot be rendered over (a blocking defect decided in code, or a missing, running, failed, stale or old-rubric report). The confirmed approval records the override on the report (the coverage read's `writerOverride`); the answer carries it as `planCoverageOverride`. Draft only. Requires `Idempotency-Key`. Scope: `story:write`. A trailer is priced by the second: 1 credit a second to render at 480p, plus 1 credit a second to finish at 720p (2 credits a second in all; a 1080p finish is 4 credits a second instead). Planning is free. The exact price is quoted per shot once the plan is ready, before anything renders; location plates and on-screen text are itemized in the same quotes.

## Headers

- `Idempotency-Key` string, required

## Request body

- TrailerPlanApproveRequest: Say whether this call is the price or the purchase.
  - `confirmed` boolean: Omit (or false) to get the 480p render quote WITHOUT spending anything — the beats are compiled and voiced (free to the writer), nothing is reserved. Send true only after the writer has seen that price and said go: it reserves exactly the quoted calls and starts the render.
  - `acknowledgePlanCoverage` PlanCoverageAcknowledgement: Only on the writer's explicit choice, with their reason: render over a plan the coverage judge scored under the bar. It overrides the judge's verdict only (an overall score or a judged veto under the bar on a complete report of the plan as it stands), never a code-checked blocking defect, and never a missing, running, failed, stale or old-rubric report. The confirmed approval records who, when, why and what it overrode on the report. Send it on the price and the confirm alike. Refused (400, with `details.overridable: false` and `details.overrideWhy`) for anything but a judged fail of a complete, current report.
    - `reason` string, required: Why the writer renders over the plan gate, in their own words. Recorded on the report.

## Response `200`

The render price (no `confirmed`) or the queued render — or, while the price is being prepared, `status: "preparing"` (nothing reserved; ask again).

- union: The plan approval's answer: its price or its started render (`TrailerRenderAnswer`), or — while the price is being prepared — `RenderQuotePreparing` (`status: "preparing"`).
  - TrailerRenderAnswer: Either the PRICE of a render (plan approval, or a beat rendered again) or the queued render itself — branch on `quotedOnly`. Without `confirmed` nothing is reserved; with `confirmed: true` exactly the quoted lines are held and the calls are queued.
    - `quotedOnly` boolean, required: True when this was a price and NOTHING was spent; false when the render was reserved and queued.
    - `queued` boolean: Confirmed call only.
    - `jobId` string, required
    - `credits` number, required: What a confirmed call reserves (quote), or what it reserved (confirmed).
    - `costLabel` string, nullable: What the whole quote buys, in one label — the shots, the beats they span, their seconds at 480p and the location plates made first, e.g. "Trailer render: 7 shots across 5 beats (58s at 480p)" (quote only).
    - `lines` object[]: Quote only. Every line a confirmed call would reserve: one per call at its voiced seconds, one per location plate the renders make first, and — a plan approval — the automatic-fix allowance (kind `allowance`).
      - `kind` 'call' | 'plate' | 'allowance': `allowance`: a plan approval's automatic-fix allowance (see `autoFixAllowance`).
      - `idempotencyKey` string
      - `label` string
      - `credits` number
      - `callId` string, nullable
      - `plateKey` string, nullable
      - `seconds` number, nullable
    - `calls` union: Quote: the priced calls `[{ beatIndex, callId, take, seconds, audioMode, credits }]`. Confirmed: how many calls were queued.
      - object[]
        - `beatIndex` integer
        - `callId` string
        - `take` integer
        - `seconds` number
        - `audioMode` 'dialogue' | 'ambient' | 'silent'
        - `credits` number
      - integer
    - `captureBeats` object[]: A plan approval's quote only: the beats that play an app capture, never rendered and in no line. A capture beat plays one of the project's app captures (a real screen recording, `ready`) as it was recorded: set it with the beat refine (`compositionKind: 'capture'` and the capture's `captureId`). It plays the whole capture, or only the stretch of it you keep (`captureKeep`: a start and an end, in seconds of the capture); what it plays runs 4 to 30 seconds, so a longer capture plays a kept stretch of it. The capture must be in the trailer's shape. The beat carries no on-screen text (its caption is cleared and none can be set), and is never rendered or retaken, so it costs nothing. A beat that already has rendered shots cannot become one: add a beat for the capture instead.
      - `beatIndex` integer, required
      - `captureId` string, required
      - `seconds` number, required
    - `warnings` string[]: Quote only. Compile warnings worth showing (advisory; nothing blocks on them).
    - `balanceAfter` integer: Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short.
    - `sufficient` boolean: Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse.
    - `note` string: The price in the writer's own words, what it would leave (or how short they are, with the top-up link), and how to proceed (quote only).
    - `message` string: Confirmed call only: what was queued.
    - `previewUrl` string: A beat render only: a shareable link to watch the beat's current cut.
    - `autoFixAllowance` object, nullable: A plan approval's quote only: the AUTOMATIC-FIX ALLOWANCE, part of the one approval and included in `credits`. A ceiling the engine draws on to retake the shots the review of the 480p preview flags (and render flagged on-screen text again), reserved with the render; only what the fixes draw is charged and the rest comes back when the loop stops. 0 when the cut has nothing a paid new take could redo (app captures, title cards) or the balance holds only the render; free fixes (re-timing an app capture's narration to its picture) run whatever it holds, and `note` says plainly what is and is not fixed by itself. The 720p finish is a separate approval.
      - `credits` integer, required
      - `note` string, required: The allowance in the writer's words.
    - `planCoverageOverride` PlanCoverageOverride: A render over a failed plan gate on the writer's word: the report it overrode, the plan revision it graded, its score, the bar, and the reasons it gave. A recorded override (on the confirmed approval and on the coverage read) also carries `at` and `by`.
      - `reason` string, required
      - `reportId` string, required
      - `planRevision` integer, required
      - `overallScore` number, nullable, required
      - `gateThreshold` number, required
      - `gateReasons` string[], required
      - `at` string, date-time: Recorded overrides only: when the confirmed approval recorded it.
      - `by` object: Recorded overrides only: who approved it — `api` (the Story API: MCP and the CLI) with the API key or `oauth:…` connection, or `app` (the Writers Lab).
        - `userId` string, required
        - `surface` 'api' | 'app', required
        - `keyId` string, nullable, required
  - RenderQuotePreparing: A plan approval whose price is still being prepared in the background (every chapter or beat voiced and planned): no price yet, and nothing was reserved — confirmed or not. Ask again with the same request after `checkAgainSeconds`; the price comes back once it is ready.
    - `quotedOnly` true, required
    - `status` 'preparing', required
    - `jobId` string, required
    - `progress` string, nullable, required: What is being prepared now, e.g. "Preparing chapters 9 and 10 of 17; 8 of 17 done…".
    - `checkAgainSeconds` integer, required
    - `note` string, required: One line to tell the writer.

## Other responses

- `401`: Missing, malformed, revoked, or expired API key.
- `402`: Not enough Studio Credits to reserve this job (`insufficient_credits`), and nothing was reserved. `details` is InsufficientCreditsDetails: `required` and `available`, and `autoTopup` when auto top-up is on but did not cover it — `pending` (retry in a moment), `over_cap` (raise the monthly spending cap), `too_small` (buy a bigger pack) or `failed`.
- `403`: API key is valid but lacks the scope, is blocked by IP allowlist, or the account is disabled.
- `404`: The named project or source does not exist or is owned by another account.
- `409`: The trailer exists and the request is well-formed, but the trailer, its beat, its cast or its plates are not in a state that allows it (`trailer_state_invalid`), and nothing was reserved: a plan approved outside draft, a take chosen or a beat rendered outside review/complete, an export before the finish, a finish while a call has no selected take, a narrated cut with no narrator, a render the compile blocked, or a finish while the master failed its video coverage (`trailer_video_coverage_failed`, with `details.videoCoverage`). `details.refusal` is `{ code, action, fix }` (see TrailerRefusalDetails): `action` is what unblocks it — `recast_figure`, `add_signature_items`, `recast_voice` or `remake_location_plate` — and `fix` names the MCP tool and CLI command that take it; `details.blocked[]` lists every beat a blocked compile could not render. The identical request succeeds once the trailer is put right.
- `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)
