---
title: "Change a storyboard"
method: PATCH
path: "/v1/storyboards/{id}"
tags: ["Storyboards"]
---

# Change a storyboard

`PATCH /v1/storyboards/{id}`

## Request

- Base URL: `https://nanostudiopro.com/api`
- URL: `https://nanostudiopro.com/api/v1/storyboards/{id}`
- Auth: HTTP bearer

Exactly ONE of `mutations`, `name` or `project_id` per call (the page's rule). `mutations` are the page's own operations, each `{ op, ...fields }`, applied all or nothing on the latest version — an operation whose target is gone refuses the whole list with the reason (`400`). You choose every id (unique, at most 80 characters). The main ones:

- `addCastEntry { entry: { entityId } }`, `addPlace { entry: { entityId } }`
- `addPanel { id, beat }`, `updatePanel { id, patch: { beat, castIds, placeId, camera, image: { generationId } | { assetId } } }`, `removePanel { id }`
- `connect { segmentId, fromPanelId, toPanelId }`, `updateSegment { id, patch: { prompt, durationS, join, model } }`, `removeSegment { id }`
- `addLine { segmentId, line: { id, atS, text, who?, how? } }` (`atS` = seconds from the segment's start, `who` = a cast entity id), `updateLine`, `removeLine`
- `addDecision { id, atPanelId, firstOptionId, firstLabel }`, `addOption { decisionId, optionId, label, segmentId, toPanelId }`, `removeDecision { id }`
- `setIdea { patch: { text, tone, aspectRatio, defaultModel } }`, `setStart { id }`
- How a moment arrives and its sound: `updateSegment { id, patch: { enter: { kind: cut | dissolve | dip, s }, soundTail: { s }, soundLead: { s } } }` (`null` = automatic again; `GET` says what plays in each path's `timeline[].enter`), `trimTake { target: { kind: "segment", id } | { kind: "ending", panelId }, takeId, patch: { inS, outS, ramp: { fromS, toS, rate } } }` (seconds into the clip; rate 0.5 to 3; `null` clears)
- The film's sound list: `addSound { sound: { id, name, kind: music | atmosphere | upload, path, prompt?, durationMs? } }` (`path` an uploaded `/uploads/audio/` file), `removeSound { id }` (refused while the title music, an end screen or a place uses it: the message says where)
- `setAmbience { placeId, sound: { soundId, volume? } | null }`: the place's sound, looped while the film is there, crossfaded when the place changes
- `updateTitleScreen { patch: { posterId?, music?: { soundId, volume?, loop? } | null } }` (the start screen of a shared film; its words are the storyboard's name), `setPoster { generationId, ifPosterIs? }` (picks one of the film's `posters`; a new id joins the list; with `ifPosterIs` it is chosen only when that is still the poster in use), `removePoster { generationId }`, `updateEndScreen { panelId, patch: { title?, image?: { generationId } | { assetId }, music? } }` (`panelId`: a keyframe a path ends on; no music: the title music returns; `null` clears a field)

Each committed change is one entry in the storyboard's log, signed as the agent. Answers the storyboard as `GET` does. `PUT` is accepted too.

## Request body

- object
  - `mutations` object[]
    - `op` string, required
  - `name` string
  - `project_id` string, nullable

## Response `200`

The storyboard as changed

- Storyboard: A storyboard as an agent plans from it. `doc` is the document the mutations address (camelCase inside, like the operations); everything around it is snake_case.
  - `id` string
  - `name` string
  - `project_id` string, nullable
  - `url` string, uri: The storyboard's page, where the owner watches.
  - `rev` integer
  - `can_undo` boolean
  - `can_redo` boolean
  - `read_only` boolean: Saved by a newer version of the app: `doc` is null and nothing can change.
  - `doc` object, nullable: idea, cast, places, panels (keyframes), segments, decisions, cues, startPanelId, agentSpend; and when set: theme (the title music), sounds (the film's sound list), ambience (place id -> its sound), endScreens (ending keyframe id -> its end screen), mix (the sound layers' levels set by hand), score (the music on the timeline: `cues`, each {id, soundId, fromPanelId, toPanelId?, path?, offsetS?, fadeInS?, fadeOutS?, level?, scored?}); each take's `effects` (sound effects at a second of its clip: {id, soundId, atS, level?, pitch?, rise?}).
  - `poster` StoryboardImage
    - `generation_id` string, nullable
    - `asset_id` string, nullable
    - `url` string, uri, nullable: Signed; expires. Null when the picture is gone.
    - `missing` boolean
  - `posters` object[]: Every poster made for the film, newest first (at most 12). `setPoster { generationId }` picks one (a new id joins the list), `removePoster { generationId }` takes one out (the picture stays in the library; the one in use falls back to the newest left).
    - `generation_id` string, nullable
    - `asset_id` string, nullable
    - `url` string, uri, nullable: Signed; expires. Null when the picture is gone.
    - `missing` boolean
    - `in_use` boolean
  - `keyframes` object[]
    - `id` string
    - `beat` string
    - `entity_ids` string[]: Pass these to `POST /v1/generations` as `entity_ids` for this keyframe's picture.
    - `entity_ids_left_out` string[]: Over the cap of 12, by priority.
    - `image` StoryboardImage
      - `generation_id` string, nullable
      - `asset_id` string, nullable
      - `url` string, uri, nullable: Signed; expires. Null when the picture is gone.
      - `missing` boolean
  - `issues` object[]
    - `target` object
      - `kind` 'panel' | 'segment' | 'decision' | 'cue'
      - `id` string
    - `level` 'error' | 'advice'
    - `message` string
    - `rule` string: The rule that fired.
    - `key` string: Stable across edits. An advice that is intended is marked so with the mutation {op: "ackIssue", key} and stops showing ({op: "unackIssue", key} brings it back). Errors cannot be marked.
  - `acknowledged` string[]: The issue keys marked as intended (doc.acknowledged).
  - `ai_feedback` object: What the owner said of each AI continuity finding, by its key: {verdict: intended|wrong|correct, note?, at, version (of the check), text (what it said), takeIds}. Set with the mutation {op: "setAiFeedback", key, verdict, note?} (note up to 300 characters; verdict null clears it). "intended" also marks it as intended; "wrong" hides it and a re-run on the same takes does not raise it again; the note is told to the next check of that clip or cut.
  - `readiness` object, nullable: Ready for video? Every issue told where it is in the film, errors first. Null when read_only.
    - `ready` boolean: No error left and every keyframe has its picture.
    - `errors` integer
    - `advice` integer
    - `acknowledged` integer: Advice marked as intended: left out of rows.
    - `ai_wrong` integer: AI findings marked "AI is wrong" (setAiFeedback): left out of rows.
    - `rows` object[]
      - `where` string: For example "Moment 3" or "Moments 2 to 3".
      - `key` string: The issue's stable key (see issues).
      - `level` 'error' | 'advice'
      - `message` string
      - `target` object
        - `kind` 'panel' | 'segment' | 'decision' | 'cue'
        - `id` string
      - `feedback` object: An AI finding the owner marked: {verdict: intended|wrong|correct, note?}.
  - `paths` object[]: Every route from the start to an ending, decisions branching it.
    - `panel_ids` string[]
    - `segment_ids` string[]
    - `choices` object: decision id -> option id taken on this path.
    - `length_s` number: The plan's length: every segment's seconds and the end hold.
    - `played_length_s` number: As the player plays it: trims, speed ramps and dissolves (which overlap) counted, with the clips finished so far.
    - `timeline` object[]
      - `panel_id` string
      - `segment_id` string, nullable
      - `start_s` number
      - `hold_s` number
      - `played_start_s` number
      - `played_s` number
      - `enter` StoryboardTransition: How a moment arrives: computed from what the storyboard knows (same place: dissolve; another place, time of day or weather: dip; a decision's keyframe: cut), its length from the shorter clip, unless set by hand (`manual`).
        - `kind` 'cut' | 'dissolve' | 'dip'
        - `s` number: Seconds; 0 for a cut. A dissolve overlaps the two pictures; a dip goes through black.
        - `manual` boolean
        - `why` string
      - `after_choice` StoryboardTransition: How a moment arrives: computed from what the storyboard knows (same place: dissolve; another place, time of day or weather: dip; a decision's keyframe: cut), its length from the shorter clip, unless set by hand (`manual`).
        - `kind` 'cut' | 'dissolve' | 'dip'
        - `s` number: Seconds; 0 for a cut. A dissolve overlaps the two pictures; a dip goes through black.
        - `manual` boolean
        - `why` string
      - `sound_tail_s` number: Seconds of the previous clip's sound carried under this moment.
      - `sound_lead_s` number: Seconds this moment's sound starts before its picture.
  - `title_screen` object, nullable: What a shared film shows before Play: its title (the storyboard's name), poster and title music (`updateTitleScreen`).
    - `title` string
    - `poster` StoryboardImage
      - `generation_id` string, nullable
      - `asset_id` string, nullable
      - `url` string, uri, nullable: Signed; expires. Null when the picture is gone.
      - `missing` boolean
    - `music` object, nullable
      - `name` string, nullable
      - `sound_id` string, nullable: Null for music chosen before the sound list.
      - `volume` number, nullable
      - `loop` boolean
  - `end_screens` object[]: One per keyframe a path ends on (`updateEndScreen`); null fields fall back to the film's title and the title music.
    - `panel_id` string
    - `title` string, nullable
    - `image` StoryboardImage
      - `generation_id` string, nullable
      - `asset_id` string, nullable
      - `url` string, uri, nullable: Signed; expires. Null when the picture is gone.
      - `missing` boolean
    - `music` object, nullable
      - `sound_id` string
      - `volume` number, nullable
      - `loop` boolean

## Other responses

- `400`: that keyframe does not exist
- `401`: Missing or invalid token
- `404`: Storyboard not found
- `409`: This storyboard was saved by a newer version of the app
- `429`: Rate limit exceeded

---

[API](https://skmtc.dev/nanostudiopro/apis/nano-studio-pro-api.md) · [All operations](https://skmtc.dev/nanostudiopro/apis/nano-studio-pro-api/llms.txt) · [OpenAPI document](https://skmtc.dev/nanostudiopro/apis/nano-studio-pro-api/revisions/7f7d4a72b17c?raw)
