---
title: "Write a chosen story film as its script."
method: POST
path: "/story-projects/{projectId}/story-films/{storyId}/script"
tags: ["Story Films"]
---

# Write a chosen story film as its script.

`POST /story-projects/{projectId}/story-films/{storyId}/script`

## Request

- Base URL: `https://docs.sleeperhit.studio/api/v1`
- URL: `https://docs.sleeperhit.studio/api/v1/story-projects/{projectId}/story-films/{storyId}/script`
- Auth: HTTP bearer

Writing a story film's script turns the chosen pitch into a short screenplay the writer can edit — about 30 to 45 seconds, or the length the writer asks for up to 60: a person's own scenes, then 2 to 3 real moments of the product cut on the click, then back to their life — and each product moment uses a recording of the product the project already has when one fits, or is listed plainly as a recording still to make. One screenplay pass (the model call is booked to the ledger like a pitch; nothing else is charged): the script is saved as a Writers Lab screenplay in this project — parsed and analysed like any upload, so the table read, cast and film read it — and the story is stored `scripted` with its `script` and its product moments as `captureRequests`. Every product moment is an `INSERT` scene carrying a `[[capture:<id>]]` note (Fountain notes are never read aloud); nobody speaks on camera inside one. A draft that breaks those rules is written once more with the problems named, then refused 422 `artifact_generation_failed` with nothing saved. The body is empty, or `{ screenView?, seconds? }` — the writer's view of every product moment's screen (`full-screen`, `over-the-shoulder` or `hands-then-full-screen`; empty, Slug chooses each moment's) and how long the film runs (a whole number of seconds, 30 to 60; empty, the usual 30-45, and the script is held to that length's scenes and words); anything else, or another field, is 400 `validation_failed` naming it. A product project not ready for a story film (the app not connected or signed in to, or no shipped feature) is 409 `product_video_not_ready` in that step's own sentence, with `details.next` its verb, before any screenplay call. A film that already has its script is 409 `script_state_invalid` unless `seconds` asks for another length: then a new film of the same pitch is written beside it at that length, keeping the written one (the answer names the new `storyId`); a project that cannot take a new screenplay yet is 409 `project_precondition_failed`. Requires `Idempotency-Key`. Scope: `story:write`.

## Headers

- `Idempotency-Key` string, required

## Request body

- WriteStoryFilmRequest: Optionally, the writer's view of every product moment's screen (empty: Slug chooses each moment's) and how long the film runs (empty: the usual 30-45 s).
  - `screenView` 'full-screen' | 'hands-then-full-screen' | 'over-the-shoulder'
  - `seconds` integer: How long the film runs, whole seconds; the script is written to that length and its cut is planned at it.

## Response `200`

The script, every product moment with the recording it uses (or null: a recording to make), and the recordings a moment may swap to.

- StoryFilmScript: Writing a story film's script turns the chosen pitch into a short screenplay the writer can edit — about 30 to 45 seconds, or the length the writer asks for up to 60: a person's own scenes, then 2 to 3 real moments of the product cut on the click, then back to their life — and each product moment uses a recording of the product the project already has when one fits, or is listed plainly as a recording still to make.
  - `projectId` string, required
  - `storyId` string, required
  - `status` 'pitched' | 'scripted', required
  - `script` object, required
    - `scriptId` string, required: The Writers Lab script.
    - `scriptUploadId` string, required: Its version the table read and the film read.
    - `title` string, required
    - `writtenAt` string, date-time, required
    - `workspacePath` string, nullable, required: Where it opens in the Writers Lab, relative to the site origin.
    - `seconds` integer: The length the writer asked for when it was written, s. Absent: the usual 30-45.
  - `moments` StoryFilmMoment[], required
    - `id` string, required: The moment id its `[[capture:<id>]]` note names in the script.
    - `featureId` string, required
    - `mustShowMoment` string, required
    - `status` 'requested' | 'drafted' | 'recorded' | 'verified' | 'rebound' | 'unreachable', required: `recorded` when it uses a recording; `requested` when one must be made; `drafted` when its tour is drafted and ready to record; `unreachable` when its last recording stopped short of it.
    - `recording` StoryFilmRecording, required: One recording of the product (an app capture of this project) a product moment may use.
      - `appCaptureId` string, required
      - `name` string, required: Its feature and its own label or tour.
      - `featureId` string, nullable, required
      - `recordedAt` string, date-time, required
      - `durationMs` integer, nullable, required
      - `chosenBy` 'match' | 'writer' | 'tour': On a moment's recording: `match` when matched as the script was written, `writer` when the writer picked it, `tour` when it was recorded from the moment's own drafted tour.
      - `why` string: On a moment's recording: why it fits, in one line.
    - `tour` object, nullable, required: The tour drafted to record it, or null: the current draft run, and its staged tour once drafted. A finished recording of that tour becomes the moment's recording by itself.
      - `runId` string, required: The current tour draft run (read it with GET …/tours/drafts/{runId}).
      - `tourId` string, nullable, required: The staged tour, once drafted.
      - `draftedAt` string, date-time, nullable, required
    - `missed` object, nullable, required: The last recording of its tour stopped short of the moment, or null: the mark it needed and the marks it did reach — the nearest real state. Never replaced by anything but a real recording.
      - `appCaptureId` string, required
      - `markId` string, required: The mark the moment needed.
      - `reachedMarkIds` string[], required
      - `nearestMarkId` string, nullable, required: The last mark it reached.
      - `at` string, date-time, required
      - `attempts` integer: How many takes of the moment's tour have missed (BL-614: 1 re-drafts the moment once, 2 is the end). Absent on a miss written before BL-614.
      - `reasons` object[]: One entry per take that missed, oldest first; `attempts` is its length. Absent on a miss written before BL-614.
        - `appCaptureId` string, required: The recording that stopped short.
        - `markId` string, required: The mark the moment needed it to reach.
        - `reachedMarkIds` string[], required: The marks it did reach, in order.
        - `nearestMarkId` string, nullable, required: The last mark it reached: the nearest real state to the moment. Null when it reached none.
        - `at` string, date-time, required: When it was checked (ISO).
    - `screenView` StoryFilmMomentScreenView, required: How we see one product moment's screen, who chose it (Slug's taste or the writer's pick) and why.
      - `kind` 'full-screen' | 'hands-then-full-screen' | 'over-the-shoulder', required
      - `setBy` 'slug' | 'writer', required
      - `reason` string, required: Why, in one line.
    - `line` string, required: The moment in one printable line.
  - `needsRecording` string[], required: The moments that still need a recording made.
  - `recordings` StoryFilmRecording[], required: The project's recordings a moment may use (one take of each recorded moment), newest first.
    - `appCaptureId` string, required
    - `name` string, required: Its feature and its own label or tour.
    - `featureId` string, nullable, required
    - `recordedAt` string, date-time, required
    - `durationMs` integer, nullable, required
    - `chosenBy` 'match' | 'writer' | 'tour': On a moment's recording: `match` when matched as the script was written, `writer` when the writer picked it, `tour` when it was recorded from the moment's own drafted tour.
    - `why` string: On a moment's recording: why it fits, in one line.
  - `momentTourFailures` object[]: BL-1148: on a write, the moments whose ready-to-record tour could NOT be started; the film is written all the same and each still needs a recording made by hand. Absent when every start succeeded, or when the answer is not the write.
    - `captureRequestId` string, required
    - `message` string, required
  - `note` string, required: What happened and what is next, to read out.

## Other responses

- `400`: Request payload failed validation. The error message names the offending field.
- `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 named project or source does not exist or is owned by another account.
- `409`: The film already has its script (`script_state_invalid`, edit it in the Writers Lab), the project cannot take a new screenplay yet (`project_precondition_failed`), or it is not ready for a story film (`product_video_not_ready`: `message` is the readiness step to do first, `details.next` its verb).
- `422`: The script could not be written so every product moment is a real recording (`artifact_generation_failed`, `details.problems`); nothing was saved.
- `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)
