{
  "openapi": "3.1.0",
  "info": {
    "title": "Storyboard integration API",
    "version": "1.0.0",
    "description": "Agent-facing API for Storyboard. Authenticate with a full-access or scoped integration token minted in the workspace under Integration. Every endpoint acts on the account that owns the token. Start with GET /api/v1/me to inspect access.",
    "contact": {
      "url": "https://storyboard.keypiece.ai/docs"
    }
  },
  "servers": [
    {
      "url": "https://storyboard.keypiece.ai"
    }
  ],
  "tags": [
    {
      "name": "start",
      "description": "Every request carries an integration token. Tokens are account-wide, scoped, and shown once when minted."
    },
    {
      "name": "projects",
      "description": "A project is one board — one piece of creative and its shot sequence."
    },
    {
      "name": "shots",
      "description": "Panels in order. Everything an agent needs to write a shot: prompt, duration, ratio, dialogue, the cast lock and the frames that bookend it. Camera direction belongs in the prompt."
    },
    {
      "name": "characters",
      "description": "Saved creators. A character's seed is what keeps the same face across every shot it is cast into."
    },
    {
      "name": "products",
      "description": "The thing being sold. A shot features one product; its packshot and name ride along in the generation payload."
    },
    {
      "name": "audio",
      "description": "Voiceover lines, music beds and effects. A shot can carry several, in order."
    },
    {
      "name": "generations",
      "description": "Queue takes against the video model. Jobs are asynchronous — start one, then poll it. The workspace's Generate button calls exactly these endpoints."
    },
    {
      "name": "settings",
      "description": "Which video model the account generates with, and the key it uses. Keys are verified before they are stored and are never returned — only a masked hint."
    }
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "operationId": "index",
        "summary": "Service index",
        "description": "Unauthenticated. Lists resource URLs, available scopes, whether server credentials are configured and which generation provider is active. A good first call for an agent orienting itself.",
        "tags": [
          "start"
        ],
        "security": [],
        "x-required-scopes": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "api_index",
                  "name": "Storyboard integration API",
                  "version": "v1",
                  "documentation": "/docs",
                  "openapi": "/api/v1/openapi.json",
                  "agent_guide": "/llms.txt",
                  "server_configured": true,
                  "generation_provider": "mock"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/me": {
      "get": {
        "operationId": "me",
        "summary": "Who am I",
        "description": "Confirms a token works and reports the scopes it carries and the account it belongs to. Call this first to find out what you are allowed to do.",
        "tags": [
          "start"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "identity",
                  "token": {
                    "id": "9f2c1ab07d4e",
                    "name": "Agent — board builder",
                    "prefix": "sb_live_7Kq2…",
                    "access": "scoped",
                    "scopes": [
                      "projects:read",
                      "projects:write"
                    ],
                    "created_at": "2026-08-09T21:00:00.000Z",
                    "last_used_at": "2026-08-10T11:40:22.000Z"
                  },
                  "account": {
                    "id": "uid_abc123",
                    "email": "you@studio.com",
                    "display_name": "Yu Xiang"
                  }
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/projects": {
      "get": {
        "operationId": "projects_list",
        "summary": "List projects",
        "description": "Every board on the account, in board order.",
        "tags": [
          "projects"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "projects:read"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "list",
                  "data": [
                    {
                      "id": "1JcQm2xUaB",
                      "object": "project",
                      "name": "Niacinamide Serum",
                      "brand": "Lumen",
                      "status": "In review",
                      "order": 0,
                      "shot_count": 5,
                      "created_at": "2026-08-01T09:12:04.000Z",
                      "updated_at": "2026-08-10T11:40:22.000Z"
                    }
                  ],
                  "has_more": false
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "projects_create",
        "summary": "Create a project",
        "description": "Starts an empty board. Add shots to it next.",
        "tags": [
          "projects"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "projects:write"
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "1JcQm2xUaB",
                  "object": "project",
                  "name": "Niacinamide Serum",
                  "brand": "Lumen",
                  "status": "In review",
                  "order": 0,
                  "shot_count": 5,
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-10T11:40:22.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Project name."
                  },
                  "brand": {
                    "type": "string",
                    "description": "Brand shown beside the name."
                  },
                  "status": {
                    "type": "string",
                    "description": "`Draft`, `In review` or `Approved`. Defaults to `Draft`."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "SPF 50 — batch B",
                "brand": "Lumen"
              }
            }
          }
        }
      }
    },
    "/api/v1/projects/{projectId}": {
      "get": {
        "operationId": "projects_get",
        "summary": "Retrieve a project",
        "description": "One board, including its shot count.",
        "tags": [
          "projects"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "projects:read"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "1JcQm2xUaB",
                  "object": "project",
                  "name": "Niacinamide Serum",
                  "brand": "Lumen",
                  "status": "In review",
                  "order": 0,
                  "shot_count": 5,
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-10T11:40:22.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "patch": {
        "operationId": "projects_update",
        "summary": "Update a project",
        "description": "Only the fields you send change.",
        "tags": [
          "projects"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "projects:write"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "1JcQm2xUaB",
                  "object": "project",
                  "name": "Niacinamide Serum",
                  "brand": "Lumen",
                  "status": "Approved",
                  "order": 0,
                  "shot_count": 5,
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-10T11:40:22.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Project name."
                  },
                  "brand": {
                    "type": "string",
                    "description": "Brand label."
                  },
                  "status": {
                    "type": "string",
                    "description": "`Draft`, `In review` or `Approved`."
                  },
                  "order": {
                    "type": "number",
                    "description": "Position in the project list."
                  }
                }
              },
              "example": {
                "status": "Approved"
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "projects_delete",
        "summary": "Delete a project",
        "description": "Removes the board, its shots and their variation history. Not reversible.",
        "tags": [
          "projects"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "projects:write"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "deleted",
                  "id": "1JcQm2xUaB",
                  "deleted": true
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/projects/{projectId}/shots": {
      "get": {
        "operationId": "shots_list",
        "summary": "List shots",
        "description": "Panels in sequence order, left to right.",
        "tags": [
          "shots"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "projects:read"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "list",
                  "data": [
                    {
                      "id": "s7YbQ1pW0k",
                      "object": "shot",
                      "project_id": "1JcQm2xUaB",
                      "order": 1,
                      "title": "Product close-up",
                      "beat": "Proof · 0:03–0:07",
                      "status": "Generated",
                      "duration_s": 4,
                      "aspect_ratio": "9:16",
                      "dialogue": "“It’s this one — 5% niacinamide.”",
                      "action": "Turns bottle so label faces lens",
                      "notes": "Label legible and centred for the full 4s.",
                      "prompt": "The same creator holds a frosted glass serum bottle close to the lens and rotates it so the label faces camera…",
                      "annotation": null,
                      "frame_url": null,
                      "reference_name": "ref-02-product.jpg",
                      "end_frame_enabled": false,
                      "end_frame_url": null,
                      "end_reference_name": "",
                      "character_id": "cMayaR001",
                      "character_name": "Maya R.",
                      "character_seed": 48211,
                      "product_id": "pSerum001",
                      "product_name": "Niacinamide Serum",
                      "audio_ids": [
                        "aVoProof01",
                        "aBed01"
                      ],
                      "created_at": "2026-08-01T09:12:04.000Z",
                      "updated_at": "2026-08-10T11:40:22.000Z"
                    }
                  ],
                  "has_more": false
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "post": {
        "operationId": "shots_create",
        "summary": "Create a shot",
        "description": "Appends a panel to the end of the sequence. Anything you leave out gets a sensible default, so a bare `{}` is a valid body.",
        "tags": [
          "shots"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "projects:write"
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "s7YbQ1pW0k",
                  "object": "shot",
                  "project_id": "1JcQm2xUaB",
                  "order": 1,
                  "title": "Product close-up",
                  "beat": "Proof · 0:03–0:07",
                  "status": "Generated",
                  "duration_s": 4,
                  "aspect_ratio": "9:16",
                  "dialogue": "“It’s this one — 5% niacinamide.”",
                  "action": "Turns bottle so label faces lens",
                  "notes": "Label legible and centred for the full 4s.",
                  "prompt": "The same creator holds a frosted glass serum bottle close to the lens and rotates it so the label faces camera…",
                  "annotation": null,
                  "frame_url": null,
                  "reference_name": "ref-02-product.jpg",
                  "end_frame_enabled": false,
                  "end_frame_url": null,
                  "end_reference_name": "",
                  "character_id": "cMayaR001",
                  "character_name": "Maya R.",
                  "character_seed": 48211,
                  "product_id": "pSerum001",
                  "product_name": "Niacinamide Serum",
                  "audio_ids": [
                    "aVoProof01",
                    "aBed01"
                  ],
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-10T11:40:22.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "Short name shown on the panel."
                  },
                  "beat": {
                    "type": "string",
                    "description": "Story beat and timing, e.g. `Proof · 0:03–0:07`."
                  },
                  "status": {
                    "type": "string",
                    "description": "`Draft`, `Ready`, `Generated` or `Approved`."
                  },
                  "duration_s": {
                    "type": "number",
                    "description": "Shot length in seconds. The board's slider covers 1–10; the API accepts 0.5–60."
                  },
                  "aspect_ratio": {
                    "type": "string",
                    "description": "`9:16`, `4:5`, `1:1` or `16:9`. Drives the output resolution."
                  },
                  "dialogue": {
                    "type": "string",
                    "description": "Spoken line or voiceover."
                  },
                  "action": {
                    "type": "string",
                    "description": "What happens with the product."
                  },
                  "notes": {
                    "type": "string",
                    "description": "Direction for whoever reviews the shot."
                  },
                  "prompt": {
                    "type": "string",
                    "description": "The prompt sent to the video model."
                  },
                  "annotation": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Handwritten margin note on the panel. `null` removes it."
                  },
                  "reference_name": {
                    "type": "string",
                    "description": "Label for the start frame."
                  },
                  "end_frame_enabled": {
                    "type": "boolean",
                    "description": "Frame the shot start-to-end. Setting it to `false` also clears the end frame."
                  },
                  "character_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Cast a saved character. The character's name and locked seed are copied onto the shot; `null` uncasts it."
                  },
                  "product_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Feature a saved product. Its packshot and slug ride along in the generation payload; `null` removes it."
                  },
                  "audio_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "The tracks laid over this shot, in order. Replaces the whole list; duplicates are collapsed."
                  }
                }
              },
              "example": {
                "title": "Creator selfie hook",
                "beat": "Hook · 0:00–0:03",
                "duration_s": 3,
                "aspect_ratio": "9:16",
                "dialogue": "“Okay. I need to talk about this serum.”",
                "prompt": "Vertical selfie video, woman in her late twenties holding her phone at arm's length in a bright tiled bathroom…",
                "character_id": "cMayaR001"
              }
            }
          }
        }
      }
    },
    "/api/v1/projects/{projectId}/shots/{shotId}": {
      "get": {
        "operationId": "shots_get",
        "summary": "Retrieve a shot",
        "description": "One panel with every field the workspace shows.",
        "tags": [
          "shots"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "projects:read"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "s7YbQ1pW0k",
                  "object": "shot",
                  "project_id": "1JcQm2xUaB",
                  "order": 1,
                  "title": "Product close-up",
                  "beat": "Proof · 0:03–0:07",
                  "status": "Generated",
                  "duration_s": 4,
                  "aspect_ratio": "9:16",
                  "dialogue": "“It’s this one — 5% niacinamide.”",
                  "action": "Turns bottle so label faces lens",
                  "notes": "Label legible and centred for the full 4s.",
                  "prompt": "The same creator holds a frosted glass serum bottle close to the lens and rotates it so the label faces camera…",
                  "annotation": null,
                  "frame_url": null,
                  "reference_name": "ref-02-product.jpg",
                  "end_frame_enabled": false,
                  "end_frame_url": null,
                  "end_reference_name": "",
                  "character_id": "cMayaR001",
                  "character_name": "Maya R.",
                  "character_seed": 48211,
                  "product_id": "pSerum001",
                  "product_name": "Niacinamide Serum",
                  "audio_ids": [
                    "aVoProof01",
                    "aBed01"
                  ],
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-10T11:40:22.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "shotId",
            "in": "path",
            "required": true,
            "description": "The shot's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "patch": {
        "operationId": "shots_update",
        "summary": "Update a shot",
        "description": "Only the fields you send change. Setting `character_id` recasts the shot and copies the character's locked seed onto it.",
        "tags": [
          "shots"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "projects:write"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "s7YbQ1pW0k",
                  "object": "shot",
                  "project_id": "1JcQm2xUaB",
                  "order": 1,
                  "title": "Product close-up",
                  "beat": "Proof · 0:03–0:07",
                  "status": "Ready",
                  "duration_s": 4,
                  "aspect_ratio": "9:16",
                  "dialogue": "“It’s this one — 5% niacinamide.”",
                  "action": "Turns bottle so label faces lens",
                  "notes": "Label legible and centred for the full 4s.",
                  "prompt": "The same creator holds a frosted glass serum bottle close to the lens and rotates it so the label faces camera…",
                  "annotation": null,
                  "frame_url": null,
                  "reference_name": "ref-02-product.jpg",
                  "end_frame_enabled": false,
                  "end_frame_url": null,
                  "end_reference_name": "",
                  "character_id": "cMayaR001",
                  "character_name": "Maya R.",
                  "character_seed": 48211,
                  "product_id": "pSerum001",
                  "product_name": "Niacinamide Serum",
                  "audio_ids": [
                    "aVoProof01",
                    "aBed01"
                  ],
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-10T11:40:22.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "shotId",
            "in": "path",
            "required": true,
            "description": "The shot's id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "Short name shown on the panel."
                  },
                  "beat": {
                    "type": "string",
                    "description": "Story beat and timing, e.g. `Proof · 0:03–0:07`."
                  },
                  "status": {
                    "type": "string",
                    "description": "`Draft`, `Ready`, `Generated` or `Approved`."
                  },
                  "duration_s": {
                    "type": "number",
                    "description": "Shot length in seconds. The board's slider covers 1–10; the API accepts 0.5–60."
                  },
                  "aspect_ratio": {
                    "type": "string",
                    "description": "`9:16`, `4:5`, `1:1` or `16:9`. Drives the output resolution."
                  },
                  "dialogue": {
                    "type": "string",
                    "description": "Spoken line or voiceover."
                  },
                  "action": {
                    "type": "string",
                    "description": "What happens with the product."
                  },
                  "notes": {
                    "type": "string",
                    "description": "Direction for whoever reviews the shot."
                  },
                  "prompt": {
                    "type": "string",
                    "description": "The prompt sent to the video model."
                  },
                  "annotation": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Handwritten margin note on the panel. `null` removes it."
                  },
                  "reference_name": {
                    "type": "string",
                    "description": "Label for the start frame."
                  },
                  "end_frame_enabled": {
                    "type": "boolean",
                    "description": "Frame the shot start-to-end. Setting it to `false` also clears the end frame."
                  },
                  "character_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Cast a saved character. The character's name and locked seed are copied onto the shot; `null` uncasts it."
                  },
                  "product_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Feature a saved product. Its packshot and slug ride along in the generation payload; `null` removes it."
                  },
                  "audio_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "The tracks laid over this shot, in order. Replaces the whole list; duplicates are collapsed."
                  }
                }
              },
              "example": {
                "prompt": "Tighter framing on the dropper.",
                "status": "Ready"
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "shots_delete",
        "summary": "Delete a shot",
        "description": "Removes the panel and its variations, and updates the project's shot count.",
        "tags": [
          "shots"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "projects:write"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "deleted",
                  "id": "s7YbQ1pW0k",
                  "deleted": true
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "shotId",
            "in": "path",
            "required": true,
            "description": "The shot's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/projects/{projectId}/shots/reorder": {
      "post": {
        "operationId": "shots_reorder",
        "summary": "Reorder the sequence",
        "description": "Rewrites the running order in one call. `shot_ids` must list every shot in the project exactly once.",
        "tags": [
          "shots"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "projects:write"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "list",
                  "data": [
                    {
                      "id": "s7YbQ1pW0k",
                      "object": "shot",
                      "project_id": "1JcQm2xUaB",
                      "order": 1,
                      "title": "Product close-up",
                      "beat": "Proof · 0:03–0:07",
                      "status": "Generated",
                      "duration_s": 4,
                      "aspect_ratio": "9:16",
                      "dialogue": "“It’s this one — 5% niacinamide.”",
                      "action": "Turns bottle so label faces lens",
                      "notes": "Label legible and centred for the full 4s.",
                      "prompt": "The same creator holds a frosted glass serum bottle close to the lens and rotates it so the label faces camera…",
                      "annotation": null,
                      "frame_url": null,
                      "reference_name": "ref-02-product.jpg",
                      "end_frame_enabled": false,
                      "end_frame_url": null,
                      "end_reference_name": "",
                      "character_id": "cMayaR001",
                      "character_name": "Maya R.",
                      "character_seed": 48211,
                      "product_id": "pSerum001",
                      "product_name": "Niacinamide Serum",
                      "audio_ids": [
                        "aVoProof01",
                        "aBed01"
                      ],
                      "created_at": "2026-08-01T09:12:04.000Z",
                      "updated_at": "2026-08-10T11:40:22.000Z"
                    }
                  ],
                  "has_more": false
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "shot_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Every shot id, in the order you want them."
                  }
                },
                "required": [
                  "shot_ids"
                ]
              },
              "example": {
                "shot_ids": [
                  "s1",
                  "s3",
                  "s2",
                  "s4",
                  "s5"
                ]
              }
            }
          }
        }
      }
    },
    "/api/v1/projects/{projectId}/shots/{shotId}/frame": {
      "put": {
        "operationId": "shots_frame",
        "summary": "Upload a panel frame",
        "description": "Attaches a still that conditions image-to-video. `?position=start` (the default) sets the opening frame; `?position=end` sets the frame the shot should land on and turns on start-to-end framing. Send raw bytes with an `image/*` content type, or JSON with an `image_url` the server fetches. Max 10 MB. Replaces whatever was in that slot.",
        "tags": [
          "shots"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "projects:write"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "s7YbQ1pW0k",
                  "object": "shot",
                  "project_id": "1JcQm2xUaB",
                  "order": 1,
                  "title": "Product close-up",
                  "beat": "Proof · 0:03–0:07",
                  "status": "Generated",
                  "duration_s": 4,
                  "aspect_ratio": "9:16",
                  "dialogue": "“It’s this one — 5% niacinamide.”",
                  "action": "Turns bottle so label faces lens",
                  "notes": "Label legible and centred for the full 4s.",
                  "prompt": "The same creator holds a frosted glass serum bottle close to the lens and rotates it so the label faces camera…",
                  "annotation": null,
                  "frame_url": "https://firebasestorage.googleapis.com/v0/b/your-bucket/o/users%2F…%2Fframe-1760000000000.jpg?alt=media&token=…",
                  "reference_name": "product-close-up.jpg",
                  "end_frame_enabled": false,
                  "end_frame_url": null,
                  "end_reference_name": "",
                  "character_id": "cMayaR001",
                  "character_name": "Maya R.",
                  "character_seed": 48211,
                  "product_id": "pSerum001",
                  "product_name": "Niacinamide Serum",
                  "audio_ids": [
                    "aVoProof01",
                    "aBed01"
                  ],
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-10T11:40:22.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "shotId",
            "in": "path",
            "required": true,
            "description": "The shot's id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "position",
            "in": "query",
            "required": false,
            "description": "`start` (default) or `end`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "description": "Either send the image as the raw request body with `Content-Type: image/jpeg` (or png/webp/gif/avif), or send JSON `{ \"image_url\": \"https://…\" }`.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "image_url": {
                    "type": "string",
                    "description": "Only for `application/json` requests — a public https URL the server downloads."
                  }
                }
              },
              "example": {
                "image_url": "https://example.com/frames/product-close-up.jpg"
              }
            },
            "image/jpeg": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/png": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/webp": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        }
      }
    },
    "/api/v1/projects/{projectId}/shots/{shotId}/variations": {
      "get": {
        "operationId": "shots_variations",
        "summary": "List variations",
        "description": "Every take generated for the shot, newest first. Exactly one is marked `current`.",
        "tags": [
          "shots"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "projects:read"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "list",
                  "data": [
                    {
                      "id": "vQ3n81aZ",
                      "object": "variation",
                      "label": "v3",
                      "seed": 48211,
                      "current": true,
                      "status": "succeeded",
                      "video_url": null,
                      "poster_url": null,
                      "generation_id": "gJ40aPz1",
                      "created_at": "2026-08-10T11:41:02.000Z"
                    }
                  ],
                  "has_more": false
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "shotId",
            "in": "path",
            "required": true,
            "description": "The shot's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/characters": {
      "get": {
        "operationId": "characters_list",
        "summary": "List characters",
        "description": "Saved creators on the account. Characters are shared across every project.",
        "tags": [
          "characters"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "characters:read"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "list",
                  "data": [
                    {
                      "id": "cMayaR001",
                      "object": "character",
                      "name": "Maya R.",
                      "handle": "@mayarivs",
                      "age": "27",
                      "locked": true,
                      "seed": 48211,
                      "look": "Warm-toned combination skin, dark wavy hair up in a towel, cream ribbed tank, no makeup.",
                      "voice": "Fast, dry, unpolished — talks like a voice note.",
                      "fields": [
                        {
                          "k": "Appearance",
                          "v": "Late 20s, warm mid-tone skin, freckles across the nose…"
                        },
                        {
                          "k": "Never",
                          "v": "Studio lighting, full glam makeup, tripod-perfect framing."
                        }
                      ],
                      "portrait_url": null,
                      "reference_urls": [],
                      "created_at": "2026-08-01T09:12:04.000Z",
                      "updated_at": "2026-08-01T09:12:04.000Z"
                    }
                  ],
                  "has_more": false
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "characters_create",
        "summary": "Create a character",
        "description": "A seed is generated if you don't supply one. Lock the character once you are happy with the look — the seed is what holds the face steady.",
        "tags": [
          "characters"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "characters:write"
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "cMayaR001",
                  "object": "character",
                  "name": "Maya R.",
                  "handle": "@mayarivs",
                  "age": "27",
                  "locked": true,
                  "seed": 48211,
                  "look": "Warm-toned combination skin, dark wavy hair up in a towel, cream ribbed tank, no makeup.",
                  "voice": "Fast, dry, unpolished — talks like a voice note.",
                  "fields": [
                    {
                      "k": "Appearance",
                      "v": "Late 20s, warm mid-tone skin, freckles across the nose…"
                    },
                    {
                      "k": "Never",
                      "v": "Studio lighting, full glam makeup, tripod-perfect framing."
                    }
                  ],
                  "portrait_url": null,
                  "reference_urls": [],
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-01T09:12:04.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Display name."
                  },
                  "handle": {
                    "type": "string",
                    "description": "Social handle, e.g. `@mayarivs`."
                  },
                  "age": {
                    "type": "string",
                    "description": "Apparent age, free text."
                  },
                  "locked": {
                    "type": "boolean",
                    "description": "Whether the appearance is locked."
                  },
                  "seed": {
                    "type": "number",
                    "description": "Seed carried into every shot."
                  },
                  "look": {
                    "type": "string",
                    "description": "One-line summary shown on the card."
                  },
                  "voice": {
                    "type": "string",
                    "description": "How they speak."
                  },
                  "fields": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "k": {
                          "type": "string"
                        },
                        "v": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "k",
                        "v"
                      ]
                    },
                    "description": "The character sheet — appearance, wardrobe, setting, voice & tone, and what to never do."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Tom V.",
                "handle": "@tomdoesskin",
                "age": "31",
                "locked": true,
                "look": "Olive skin, buzz cut, plain grey hoodie, warm-white wall.",
                "voice": "Deadpan — three words per beat.",
                "fields": [
                  {
                    "k": "Never",
                    "v": "Enthusiastic reads, music-driven edits."
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/v1/characters/{characterId}": {
      "get": {
        "operationId": "characters_get",
        "summary": "Retrieve a character",
        "description": "One saved creator and their full sheet.",
        "tags": [
          "characters"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "characters:read"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "cMayaR001",
                  "object": "character",
                  "name": "Maya R.",
                  "handle": "@mayarivs",
                  "age": "27",
                  "locked": true,
                  "seed": 48211,
                  "look": "Warm-toned combination skin, dark wavy hair up in a towel, cream ribbed tank, no makeup.",
                  "voice": "Fast, dry, unpolished — talks like a voice note.",
                  "fields": [
                    {
                      "k": "Appearance",
                      "v": "Late 20s, warm mid-tone skin, freckles across the nose…"
                    },
                    {
                      "k": "Never",
                      "v": "Studio lighting, full glam makeup, tripod-perfect framing."
                    }
                  ],
                  "portrait_url": null,
                  "reference_urls": [],
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-01T09:12:04.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "characterId",
            "in": "path",
            "required": true,
            "description": "The character's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "patch": {
        "operationId": "characters_update",
        "summary": "Update a character",
        "description": "Changing the name or seed also updates every shot this character is cast into, so the board stays consistent.",
        "tags": [
          "characters"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "characters:write"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "cMayaR001",
                  "object": "character",
                  "name": "Maya R.",
                  "handle": "@mayarivs",
                  "age": "27",
                  "locked": true,
                  "seed": 48211,
                  "look": "Warm-toned combination skin, dark wavy hair up in a towel, cream ribbed tank, no makeup.",
                  "voice": "Fast, dry, unpolished — talks like a voice note.",
                  "fields": [
                    {
                      "k": "Appearance",
                      "v": "Late 20s, warm mid-tone skin, freckles across the nose…"
                    },
                    {
                      "k": "Never",
                      "v": "Studio lighting, full glam makeup, tripod-perfect framing."
                    }
                  ],
                  "portrait_url": null,
                  "reference_urls": [],
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-01T09:12:04.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "characterId",
            "in": "path",
            "required": true,
            "description": "The character's id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Display name."
                  },
                  "handle": {
                    "type": "string",
                    "description": "Social handle."
                  },
                  "age": {
                    "type": "string",
                    "description": "Apparent age."
                  },
                  "locked": {
                    "type": "boolean",
                    "description": "Lock or unlock the appearance."
                  },
                  "seed": {
                    "type": "number",
                    "description": "Seed carried into every shot."
                  },
                  "look": {
                    "type": "string",
                    "description": "Card summary."
                  },
                  "voice": {
                    "type": "string",
                    "description": "How they speak."
                  },
                  "fields": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "k": {
                          "type": "string"
                        },
                        "v": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "k",
                        "v"
                      ]
                    },
                    "description": "Character sheet rows."
                  }
                }
              },
              "example": {
                "locked": true,
                "seed": 48211
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "characters_delete",
        "summary": "Delete a character",
        "description": "Shots already cast keep the name and seed they were given; they simply stop pointing at a saved sheet.",
        "tags": [
          "characters"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "characters:write"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "deleted",
                  "id": "cMayaR001",
                  "deleted": true
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "characterId",
            "in": "path",
            "required": true,
            "description": "The character's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/products": {
      "get": {
        "operationId": "products_list",
        "summary": "List products",
        "description": "Saved products on the account, shared across every project.",
        "tags": [
          "products"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "products:read"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "list",
                  "data": [
                    {
                      "id": "pSerum001",
                      "object": "product",
                      "name": "Niacinamide Serum",
                      "brand": "Lumen",
                      "variant": "30ml · 5% niacinamide",
                      "description": "Frosted glass bottle with a matte white dropper cap and a small sans-serif label.",
                      "rules": "Label faces the lens whenever the bottle is in frame. Never show it lying down.",
                      "packshot_url": null,
                      "reference_urls": [],
                      "created_at": "2026-08-01T09:12:04.000Z",
                      "updated_at": "2026-08-01T09:12:04.000Z"
                    }
                  ],
                  "has_more": false
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "products_create",
        "summary": "Create a product",
        "description": "Packshots are uploaded from the workspace; everything else can be written here.",
        "tags": [
          "products"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "products:write"
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "pSerum001",
                  "object": "product",
                  "name": "Niacinamide Serum",
                  "brand": "Lumen",
                  "variant": "30ml · 5% niacinamide",
                  "description": "Frosted glass bottle with a matte white dropper cap and a small sans-serif label.",
                  "rules": "Label faces the lens whenever the bottle is in frame. Never show it lying down.",
                  "packshot_url": null,
                  "reference_urls": [],
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-01T09:12:04.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Product name."
                  },
                  "brand": {
                    "type": "string",
                    "description": "Brand it belongs to."
                  },
                  "variant": {
                    "type": "string",
                    "description": "Size, strength or SKU line."
                  },
                  "description": {
                    "type": "string",
                    "description": "What it looks like in frame — material, cap, label."
                  },
                  "rules": {
                    "type": "string",
                    "description": "How it must always be shown. Read this before writing a prompt."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "SPF 50 Fluid",
                "brand": "Lumen",
                "variant": "50ml · broad spectrum",
                "rules": "Cap stays on unless the shot is the application beat."
              }
            }
          }
        }
      }
    },
    "/api/v1/products/{productId}": {
      "get": {
        "operationId": "products_get",
        "summary": "Retrieve a product",
        "description": "One product and its rules.",
        "tags": [
          "products"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "products:read"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "pSerum001",
                  "object": "product",
                  "name": "Niacinamide Serum",
                  "brand": "Lumen",
                  "variant": "30ml · 5% niacinamide",
                  "description": "Frosted glass bottle with a matte white dropper cap and a small sans-serif label.",
                  "rules": "Label faces the lens whenever the bottle is in frame. Never show it lying down.",
                  "packshot_url": null,
                  "reference_urls": [],
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-01T09:12:04.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "productId",
            "in": "path",
            "required": true,
            "description": "The product's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "patch": {
        "operationId": "products_update",
        "summary": "Update a product",
        "description": "Renaming also updates the label on every shot featuring it, so boards stay consistent.",
        "tags": [
          "products"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "products:write"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "pSerum001",
                  "object": "product",
                  "name": "Niacinamide Serum",
                  "brand": "Lumen",
                  "variant": "30ml · 5% niacinamide",
                  "description": "Frosted glass bottle with a matte white dropper cap and a small sans-serif label.",
                  "rules": "Label faces the lens whenever the bottle is in frame. Never show it lying down.",
                  "packshot_url": null,
                  "reference_urls": [],
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-01T09:12:04.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "productId",
            "in": "path",
            "required": true,
            "description": "The product's id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Product name."
                  },
                  "brand": {
                    "type": "string",
                    "description": "Brand it belongs to."
                  },
                  "variant": {
                    "type": "string",
                    "description": "Size, strength or SKU line."
                  },
                  "description": {
                    "type": "string",
                    "description": "What it looks like in frame — material, cap, label."
                  },
                  "rules": {
                    "type": "string",
                    "description": "How it must always be shown. Read this before writing a prompt."
                  }
                }
              },
              "example": {
                "variant": "50ml · broad spectrum, fragrance free"
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "products_delete",
        "summary": "Delete a product",
        "description": "Shots keep the name they were given; they simply stop pointing at a saved product.",
        "tags": [
          "products"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "products:write"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "deleted",
                  "id": "pSerum001",
                  "deleted": true
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "productId",
            "in": "path",
            "required": true,
            "description": "The product's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/audio": {
      "get": {
        "operationId": "audio_list",
        "summary": "List audio tracks",
        "description": "Every saved voiceover line, music bed and effect.",
        "tags": [
          "audio"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "audio:read"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "list",
                  "data": [
                    {
                      "id": "aVoProof01",
                      "object": "audio",
                      "name": "Proof line",
                      "kind": "Voiceover",
                      "script": "“It’s this one — 5% niacinamide.”",
                      "voice": "Same read, a beat slower on the number.",
                      "duration_s": 4,
                      "notes": "Land “5%” on the label close-up.",
                      "created_at": "2026-08-01T09:12:04.000Z",
                      "updated_at": "2026-08-01T09:12:04.000Z"
                    }
                  ],
                  "has_more": false
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "audio_create",
        "summary": "Create an audio track",
        "description": "Tracks are metadata today — script, read direction and length. Lay one over a shot with `audio_ids`.",
        "tags": [
          "audio"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "audio:write"
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "aVoProof01",
                  "object": "audio",
                  "name": "Proof line",
                  "kind": "Voiceover",
                  "script": "“It’s this one — 5% niacinamide.”",
                  "voice": "Same read, a beat slower on the number.",
                  "duration_s": 4,
                  "notes": "Land “5%” on the label close-up.",
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-01T09:12:04.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Track name."
                  },
                  "kind": {
                    "type": "string",
                    "description": "`Voiceover`, `Music`, `SFX` or `Ambience`."
                  },
                  "script": {
                    "type": "string",
                    "description": "The spoken line, for voiceover."
                  },
                  "voice": {
                    "type": "string",
                    "description": "How it should sound — pace, timbre, energy."
                  },
                  "duration_s": {
                    "type": "number",
                    "description": "Length in seconds, or `null` if it doesn't matter yet."
                  },
                  "notes": {
                    "type": "string",
                    "description": "Mixing or timing direction."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "CTA line",
                "kind": "Voiceover",
                "script": "“Link’s in my bio. Go.”",
                "duration_s": 3
              }
            }
          }
        }
      }
    },
    "/api/v1/audio/{audioId}": {
      "get": {
        "operationId": "audio_get",
        "summary": "Retrieve an audio track",
        "description": "One track and its direction.",
        "tags": [
          "audio"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "audio:read"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "aVoProof01",
                  "object": "audio",
                  "name": "Proof line",
                  "kind": "Voiceover",
                  "script": "“It’s this one — 5% niacinamide.”",
                  "voice": "Same read, a beat slower on the number.",
                  "duration_s": 4,
                  "notes": "Land “5%” on the label close-up.",
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-01T09:12:04.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "audioId",
            "in": "path",
            "required": true,
            "description": "The track's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "patch": {
        "operationId": "audio_update",
        "summary": "Update an audio track",
        "description": "Only the fields you send change. Send `duration_s: null` to clear the length.",
        "tags": [
          "audio"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "audio:write"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "aVoProof01",
                  "object": "audio",
                  "name": "Proof line",
                  "kind": "Voiceover",
                  "script": "“It’s this one — 5% niacinamide.”",
                  "voice": "Same read, a beat slower on the number.",
                  "duration_s": 4,
                  "notes": "Land “5%” on the label close-up.",
                  "created_at": "2026-08-01T09:12:04.000Z",
                  "updated_at": "2026-08-01T09:12:04.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "audioId",
            "in": "path",
            "required": true,
            "description": "The track's id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Track name."
                  },
                  "kind": {
                    "type": "string",
                    "description": "`Voiceover`, `Music`, `SFX` or `Ambience`."
                  },
                  "script": {
                    "type": "string",
                    "description": "The spoken line, for voiceover."
                  },
                  "voice": {
                    "type": "string",
                    "description": "How it should sound — pace, timbre, energy."
                  },
                  "duration_s": {
                    "type": "number",
                    "description": "Length in seconds, or `null` if it doesn't matter yet."
                  },
                  "notes": {
                    "type": "string",
                    "description": "Mixing or timing direction."
                  }
                }
              },
              "example": {
                "script": "“Link’s in my bio.”",
                "duration_s": 2.5
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "audio_delete",
        "summary": "Delete an audio track",
        "description": "Removes it from the library and from any shot carrying it.",
        "tags": [
          "audio"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "audio:write"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "deleted",
                  "id": "aVoProof01",
                  "deleted": true
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "audioId",
            "in": "path",
            "required": true,
            "description": "The track's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/projects/{projectId}/shots/{shotId}/generate": {
      "post": {
        "operationId": "generations_shot",
        "summary": "Generate one variation",
        "description": "Starts a take for one shot. Returns `202` immediately with a job in `running` — video models take minutes. Poll until it settles, then the shot has a new current variation and status `Generated`.",
        "tags": [
          "generations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "generations:write"
        ],
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "gJ40aPz1",
                  "object": "generation",
                  "project_id": "1JcQm2xUaB",
                  "shot_ids": [
                    "s7YbQ1pW0k"
                  ],
                  "kind": "variation",
                  "status": "running",
                  "provider": "openrouter",
                  "request": {
                    "model": "ugc-video-01",
                    "character": "mayar-lock",
                    "start_image": "ref-02-product.jpg",
                    "product": "niacinamide-serum",
                    "audio": [
                      {
                        "kind": "Voiceover",
                        "name": "Proof line",
                        "script": "“It’s this one…”"
                      }
                    ],
                    "aspect_ratio": "9:16",
                    "resolution": "1080x1920",
                    "duration_s": 4,
                    "motion_strength": 0.45,
                    "seed": 48211,
                    "prompt": "The same creator holds a frosted glass serum bottle…"
                  },
                  "warnings": [
                    "ByteDance: Seedance 2.0 Fast does not support 1080p; this shot was generated at 720p."
                  ],
                  "shots": [
                    {
                      "shot_id": "s7YbQ1pW0k",
                      "status": "running",
                      "video_url": null,
                      "error": null
                    }
                  ],
                  "error": null,
                  "created_at": "2026-08-10T11:41:01.000Z",
                  "completed_at": null
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "shotId",
            "in": "path",
            "required": true,
            "description": "The shot's id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "model": {
                    "type": "string",
                    "description": "Model id. Defaults to `ugc-video-01`."
                  },
                  "motion_strength": {
                    "type": "number",
                    "description": "0–1. Defaults to 0.45."
                  },
                  "seed": {
                    "type": "number",
                    "description": "Overrides the character's locked seed for this run."
                  }
                }
              },
              "example": {
                "motion_strength": 0.6
              }
            }
          }
        }
      }
    },
    "/api/v1/projects/{projectId}/generations": {
      "post": {
        "operationId": "generations_draft",
        "summary": "Generate a draft",
        "description": "Starts every shot still in `Draft` or `Ready`, or exactly the shots you name. Returns `202` with one job covering all of them; `shots[]` reports each one separately.",
        "tags": [
          "generations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "generations:write"
        ],
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "gJ40aPz1",
                  "object": "generation",
                  "project_id": "1JcQm2xUaB",
                  "shot_ids": [
                    "s7YbQ1pW0k",
                    "s8ZcR2qX1m"
                  ],
                  "kind": "draft",
                  "status": "running",
                  "provider": "openrouter",
                  "request": {
                    "model": "ugc-video-01",
                    "character": "mayar-lock",
                    "start_image": "ref-02-product.jpg",
                    "product": "niacinamide-serum",
                    "audio": [
                      {
                        "kind": "Voiceover",
                        "name": "Proof line",
                        "script": "“It’s this one…”"
                      }
                    ],
                    "aspect_ratio": "9:16",
                    "resolution": "1080x1920",
                    "duration_s": 4,
                    "motion_strength": 0.45,
                    "seed": 48211,
                    "prompt": "The same creator holds a frosted glass serum bottle…"
                  },
                  "warnings": [
                    "ByteDance: Seedance 2.0 Fast does not support 1080p; this shot was generated at 720p."
                  ],
                  "shots": [
                    {
                      "shot_id": "s7YbQ1pW0k",
                      "status": "running",
                      "video_url": null,
                      "error": null
                    }
                  ],
                  "error": null,
                  "created_at": "2026-08-10T11:41:01.000Z",
                  "completed_at": null
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "shot_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Force specific shots instead of the pending ones."
                  },
                  "model": {
                    "type": "string",
                    "description": "Model id. Defaults to `ugc-video-01`."
                  },
                  "motion_strength": {
                    "type": "number",
                    "description": "0–1. Defaults to 0.45."
                  }
                }
              },
              "example": {
                "model": "ugc-video-01",
                "motion_strength": 0.45
              }
            }
          }
        }
      },
      "get": {
        "operationId": "generations_list",
        "summary": "List generation jobs",
        "description": "The 50 most recent jobs for the project, newest first.",
        "tags": [
          "generations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "generations:read"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "list",
                  "data": [
                    {
                      "id": "gJ40aPz1",
                      "object": "generation",
                      "project_id": "1JcQm2xUaB",
                      "shot_ids": [
                        "s7YbQ1pW0k"
                      ],
                      "kind": "variation",
                      "status": "running",
                      "provider": "openrouter",
                      "request": {
                        "model": "ugc-video-01",
                        "character": "mayar-lock",
                        "start_image": "ref-02-product.jpg",
                        "product": "niacinamide-serum",
                        "audio": [
                          {
                            "kind": "Voiceover",
                            "name": "Proof line",
                            "script": "“It’s this one…”"
                          }
                        ],
                        "aspect_ratio": "9:16",
                        "resolution": "1080x1920",
                        "duration_s": 4,
                        "motion_strength": 0.45,
                        "seed": 48211,
                        "prompt": "The same creator holds a frosted glass serum bottle…"
                      },
                      "warnings": [
                        "ByteDance: Seedance 2.0 Fast does not support 1080p; this shot was generated at 720p."
                      ],
                      "shots": [
                        {
                          "shot_id": "s7YbQ1pW0k",
                          "status": "running",
                          "video_url": null,
                          "error": null
                        }
                      ],
                      "error": null,
                      "created_at": "2026-08-10T11:41:01.000Z",
                      "completed_at": null
                    }
                  ],
                  "has_more": false
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/projects/{projectId}/generations/{generationId}/poll": {
      "post": {
        "operationId": "generations_poll",
        "summary": "Advance a running job",
        "description": "Asks the provider whether the work is done and moves the job forward, writing variations for whatever finished. Nothing blocks on the server, so call this every few seconds until `status` is no longer `running`. Safe to call on a settled job.",
        "tags": [
          "generations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "generations:write"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "gJ40aPz1",
                  "object": "generation",
                  "project_id": "1JcQm2xUaB",
                  "shot_ids": [
                    "s7YbQ1pW0k"
                  ],
                  "kind": "variation",
                  "status": "succeeded",
                  "provider": "openrouter",
                  "request": {
                    "model": "ugc-video-01",
                    "character": "mayar-lock",
                    "start_image": "ref-02-product.jpg",
                    "product": "niacinamide-serum",
                    "audio": [
                      {
                        "kind": "Voiceover",
                        "name": "Proof line",
                        "script": "“It’s this one…”"
                      }
                    ],
                    "aspect_ratio": "9:16",
                    "resolution": "1080x1920",
                    "duration_s": 4,
                    "motion_strength": 0.45,
                    "seed": 48211,
                    "prompt": "The same creator holds a frosted glass serum bottle…"
                  },
                  "warnings": [
                    "ByteDance: Seedance 2.0 Fast does not support 1080p; this shot was generated at 720p."
                  ],
                  "shots": [
                    {
                      "shot_id": "s7YbQ1pW0k",
                      "status": "succeeded",
                      "video_url": "https://firebasestorage.googleapis.com/v0/b/your-bucket/o/…take-1760000000000.mp4?alt=media&token=…",
                      "error": null
                    }
                  ],
                  "error": null,
                  "created_at": "2026-08-10T11:41:01.000Z",
                  "completed_at": "2026-08-10T11:44:12.000Z"
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "generationId",
            "in": "path",
            "required": true,
            "description": "The job's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/projects/{projectId}/generations/{generationId}": {
      "get": {
        "operationId": "generations_get",
        "summary": "Retrieve a generation job",
        "description": "The job's status, the exact request body that was sent, and any provider error.",
        "tags": [
          "generations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "generations:read"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "gJ40aPz1",
                  "object": "generation",
                  "project_id": "1JcQm2xUaB",
                  "shot_ids": [
                    "s7YbQ1pW0k"
                  ],
                  "kind": "variation",
                  "status": "running",
                  "provider": "openrouter",
                  "request": {
                    "model": "ugc-video-01",
                    "character": "mayar-lock",
                    "start_image": "ref-02-product.jpg",
                    "product": "niacinamide-serum",
                    "audio": [
                      {
                        "kind": "Voiceover",
                        "name": "Proof line",
                        "script": "“It’s this one…”"
                      }
                    ],
                    "aspect_ratio": "9:16",
                    "resolution": "1080x1920",
                    "duration_s": 4,
                    "motion_strength": 0.45,
                    "seed": 48211,
                    "prompt": "The same creator holds a frosted glass serum bottle…"
                  },
                  "warnings": [
                    "ByteDance: Seedance 2.0 Fast does not support 1080p; this shot was generated at 720p."
                  ],
                  "shots": [
                    {
                      "shot_id": "s7YbQ1pW0k",
                      "status": "running",
                      "video_url": null,
                      "error": null
                    }
                  ],
                  "error": null,
                  "created_at": "2026-08-10T11:41:01.000Z",
                  "completed_at": null
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "The project's id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "generationId",
            "in": "path",
            "required": true,
            "description": "The job's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/settings/generation": {
      "get": {
        "operationId": "settings_generation_get",
        "summary": "Read the video model setup",
        "description": "Every provider the account can use, which one is active, and whether a key is installed for it. The key itself is never returned — `key_hint` is a mask. `native_aspect_ratios` and `duration_note` tell you what a provider takes natively; anything else is mapped to the closest and reported in the job's `warnings`.",
        "tags": [
          "settings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "generations:read"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "generation_settings",
                  "active": "openrouter",
                  "providers": [
                    {
                      "id": "mock",
                      "label": "Mock (no model)",
                      "blurb": "Writes real jobs and variations without calling anything. The default, and useful for building a board out before spending money.",
                      "needs_key": false,
                      "key_set": true,
                      "key_hint": "",
                      "model": "mock",
                      "resolution": "720p",
                      "models": [
                        {
                          "id": "mock",
                          "label": "Mock"
                        }
                      ],
                      "resolutions": [
                        {
                          "value": "720p",
                          "label": "720p"
                        }
                      ],
                      "native_aspect_ratios": [
                        "9:16",
                        "4:5",
                        "1:1",
                        "16:9"
                      ],
                      "duration_note": "Any duration.",
                      "billing_note": "Free — nothing is generated."
                    },
                    {
                      "id": "google-veo",
                      "label": "Google Veo",
                      "blurb": "The model behind Google Flow, called with your own Gemini API key. Generates its own audio, so voiceover scripts are worth sending.",
                      "needs_key": true,
                      "key_set": false,
                      "key_hint": "",
                      "model": "veo-3.1-generate-preview",
                      "resolution": "720p",
                      "models": [
                        {
                          "id": "veo-3.1-generate-preview",
                          "label": "Veo 3.1 — highest quality"
                        },
                        {
                          "id": "veo-3.1-fast",
                          "label": "Veo 3.1 Fast — quicker, cheaper"
                        },
                        {
                          "id": "veo-3.1-lite",
                          "label": "Veo 3.1 Lite — cheapest"
                        }
                      ],
                      "resolutions": [
                        {
                          "value": "720p",
                          "label": "720p"
                        },
                        {
                          "value": "1080p",
                          "label": "1080p"
                        }
                      ],
                      "native_aspect_ratios": [
                        "9:16",
                        "16:9"
                      ],
                      "duration_note": "4, 6 or 8 second clips.",
                      "billing_note": "Billed per second to the key's Google Cloud project. A Google AI Pro or Ultra subscription covers the Gemini app and Flow, not the API — this is a separate charge, and Flow credits cannot be spent here."
                    },
                    {
                      "id": "fal-seedance",
                      "label": "Seedance (via fal)",
                      "blurb": "ByteDance Seedance on fal's queue API. Takes frames as URLs and covers more ratios and durations than Veo, so less of the board gets squared up.",
                      "needs_key": true,
                      "key_set": false,
                      "key_hint": "",
                      "model": "pro",
                      "resolution": "720p",
                      "models": [
                        {
                          "id": "pro",
                          "label": "Seedance 1 Pro — highest quality"
                        },
                        {
                          "id": "lite",
                          "label": "Seedance 1 Lite — quicker, cheaper"
                        }
                      ],
                      "resolutions": [
                        {
                          "value": "480p",
                          "label": "480p"
                        },
                        {
                          "value": "720p",
                          "label": "720p"
                        },
                        {
                          "value": "1080p",
                          "label": "1080p"
                        }
                      ],
                      "native_aspect_ratios": [
                        "9:16",
                        "1:1",
                        "16:9"
                      ],
                      "duration_note": "Whole seconds, 2 to 12.",
                      "billing_note": "Metered pay-as-you-go on your fal account, billed per second of video. There is no subscription tier that covers it."
                    },
                    {
                      "id": "openrouter",
                      "label": "OpenRouter Video",
                      "blurb": "One OpenRouter key for its video catalogue. The picker starts with Seedance; agents may use any current /videos/models id, with capabilities discovered at run time.",
                      "needs_key": true,
                      "key_set": true,
                      "key_hint": "sk-or-…9f0e",
                      "model": "bytedance/seedance-2.0-fast",
                      "resolution": "720p",
                      "models": [
                        {
                          "id": "bytedance/seedance-2.0",
                          "label": "Seedance 2.0 — highest quality"
                        },
                        {
                          "id": "bytedance/seedance-2.0-fast",
                          "label": "Seedance 2.0 Fast — quicker, cheaper"
                        },
                        {
                          "id": "bytedance/seedance-2.0-mini",
                          "label": "Seedance 2.0 Mini — economical"
                        },
                        {
                          "id": "bytedance/seedance-2.5",
                          "label": "Seedance 2.5 — long-form"
                        },
                        {
                          "id": "bytedance/seedance-1-5-pro",
                          "label": "Seedance 1.5 Pro — audio-visual"
                        }
                      ],
                      "resolutions": [
                        {
                          "value": "480p",
                          "label": "480p"
                        },
                        {
                          "value": "720p",
                          "label": "720p"
                        },
                        {
                          "value": "1080p",
                          "label": "1080p"
                        },
                        {
                          "value": "4K",
                          "label": "4K"
                        }
                      ],
                      "native_aspect_ratios": [
                        "9:16",
                        "1:1",
                        "16:9"
                      ],
                      "duration_note": "Discovered from the selected model at run time (Seedance presets span 4–30 seconds).",
                      "billing_note": "Metered against your OpenRouter credits. Price and supported inputs vary by model; Storyboard checks the live model catalogue before each run and reports every parameter substitution."
                    }
                  ]
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "settings_generation_put",
        "summary": "Choose the active model",
        "description": "Makes a provider active and sets its model and resolution. Send `api_key` the first time: it is checked against the provider before anything is stored, and a bad key comes back as `invalid_request` rather than being saved. Omit `api_key` once one is installed. A provider that needs a key cannot be made active without one. OpenRouter accepts any current `provider/model` id from its video-model catalogue; the returned presets are recommended starting points.",
        "tags": [
          "settings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "generations:write"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "generation_settings",
                  "active": "openrouter",
                  "providers": [
                    {
                      "id": "mock",
                      "label": "Mock (no model)",
                      "blurb": "Writes real jobs and variations without calling anything. The default, and useful for building a board out before spending money.",
                      "needs_key": false,
                      "key_set": true,
                      "key_hint": "",
                      "model": "mock",
                      "resolution": "720p",
                      "models": [
                        {
                          "id": "mock",
                          "label": "Mock"
                        }
                      ],
                      "resolutions": [
                        {
                          "value": "720p",
                          "label": "720p"
                        }
                      ],
                      "native_aspect_ratios": [
                        "9:16",
                        "4:5",
                        "1:1",
                        "16:9"
                      ],
                      "duration_note": "Any duration.",
                      "billing_note": "Free — nothing is generated."
                    },
                    {
                      "id": "google-veo",
                      "label": "Google Veo",
                      "blurb": "The model behind Google Flow, called with your own Gemini API key. Generates its own audio, so voiceover scripts are worth sending.",
                      "needs_key": true,
                      "key_set": false,
                      "key_hint": "",
                      "model": "veo-3.1-generate-preview",
                      "resolution": "720p",
                      "models": [
                        {
                          "id": "veo-3.1-generate-preview",
                          "label": "Veo 3.1 — highest quality"
                        },
                        {
                          "id": "veo-3.1-fast",
                          "label": "Veo 3.1 Fast — quicker, cheaper"
                        },
                        {
                          "id": "veo-3.1-lite",
                          "label": "Veo 3.1 Lite — cheapest"
                        }
                      ],
                      "resolutions": [
                        {
                          "value": "720p",
                          "label": "720p"
                        },
                        {
                          "value": "1080p",
                          "label": "1080p"
                        }
                      ],
                      "native_aspect_ratios": [
                        "9:16",
                        "16:9"
                      ],
                      "duration_note": "4, 6 or 8 second clips.",
                      "billing_note": "Billed per second to the key's Google Cloud project. A Google AI Pro or Ultra subscription covers the Gemini app and Flow, not the API — this is a separate charge, and Flow credits cannot be spent here."
                    },
                    {
                      "id": "fal-seedance",
                      "label": "Seedance (via fal)",
                      "blurb": "ByteDance Seedance on fal's queue API. Takes frames as URLs and covers more ratios and durations than Veo, so less of the board gets squared up.",
                      "needs_key": true,
                      "key_set": false,
                      "key_hint": "",
                      "model": "pro",
                      "resolution": "720p",
                      "models": [
                        {
                          "id": "pro",
                          "label": "Seedance 1 Pro — highest quality"
                        },
                        {
                          "id": "lite",
                          "label": "Seedance 1 Lite — quicker, cheaper"
                        }
                      ],
                      "resolutions": [
                        {
                          "value": "480p",
                          "label": "480p"
                        },
                        {
                          "value": "720p",
                          "label": "720p"
                        },
                        {
                          "value": "1080p",
                          "label": "1080p"
                        }
                      ],
                      "native_aspect_ratios": [
                        "9:16",
                        "1:1",
                        "16:9"
                      ],
                      "duration_note": "Whole seconds, 2 to 12.",
                      "billing_note": "Metered pay-as-you-go on your fal account, billed per second of video. There is no subscription tier that covers it."
                    },
                    {
                      "id": "openrouter",
                      "label": "OpenRouter Video",
                      "blurb": "One OpenRouter key for its video catalogue. The picker starts with Seedance; agents may use any current /videos/models id, with capabilities discovered at run time.",
                      "needs_key": true,
                      "key_set": true,
                      "key_hint": "sk-or-…9f0e",
                      "model": "bytedance/seedance-2.0-fast",
                      "resolution": "720p",
                      "models": [
                        {
                          "id": "bytedance/seedance-2.0",
                          "label": "Seedance 2.0 — highest quality"
                        },
                        {
                          "id": "bytedance/seedance-2.0-fast",
                          "label": "Seedance 2.0 Fast — quicker, cheaper"
                        },
                        {
                          "id": "bytedance/seedance-2.0-mini",
                          "label": "Seedance 2.0 Mini — economical"
                        },
                        {
                          "id": "bytedance/seedance-2.5",
                          "label": "Seedance 2.5 — long-form"
                        },
                        {
                          "id": "bytedance/seedance-1-5-pro",
                          "label": "Seedance 1.5 Pro — audio-visual"
                        }
                      ],
                      "resolutions": [
                        {
                          "value": "480p",
                          "label": "480p"
                        },
                        {
                          "value": "720p",
                          "label": "720p"
                        },
                        {
                          "value": "1080p",
                          "label": "1080p"
                        },
                        {
                          "value": "4K",
                          "label": "4K"
                        }
                      ],
                      "native_aspect_ratios": [
                        "9:16",
                        "1:1",
                        "16:9"
                      ],
                      "duration_note": "Discovered from the selected model at run time (Seedance presets span 4–30 seconds).",
                      "billing_note": "Metered against your OpenRouter credits. Price and supported inputs vary by model; Storyboard checks the live model catalogue before each run and reports every parameter substitution."
                    }
                  ]
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "provider": {
                    "type": "string",
                    "description": "`mock`, `google-veo`, `fal-seedance` or `openrouter`."
                  },
                  "model": {
                    "type": "string",
                    "description": "One of the provider's `models[].id`. OpenRouter also accepts any current video `provider/model` id. Defaults to the provider's own default."
                  },
                  "resolution": {
                    "type": "string",
                    "description": "One of the provider's `resolutions[].value`. Defaults to 720p."
                  },
                  "api_key": {
                    "type": "string",
                    "description": "Only needed until a key is installed. Verified before it is stored, and stored server-side only."
                  }
                },
                "required": [
                  "provider"
                ]
              },
              "example": {
                "provider": "openrouter",
                "model": "bytedance/seedance-2.0-fast",
                "resolution": "720p",
                "api_key": "sk-or-v1-…"
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "settings_generation_delete",
        "summary": "Forget a provider key",
        "description": "Deletes the stored key for one provider and drops the account back to `mock`, so nothing generates until another key is installed. Any job still running against the deleted key's provider will fail on its next poll. Merely switching the active provider does not interrupt jobs already in flight.",
        "tags": [
          "settings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "generations:write"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "generation_settings",
                  "active": "mock",
                  "providers": [
                    {
                      "id": "mock",
                      "label": "Mock (no model)",
                      "blurb": "Writes real jobs and variations without calling anything. The default, and useful for building a board out before spending money.",
                      "needs_key": false,
                      "key_set": true,
                      "key_hint": "",
                      "model": "mock",
                      "resolution": "720p",
                      "models": [
                        {
                          "id": "mock",
                          "label": "Mock"
                        }
                      ],
                      "resolutions": [
                        {
                          "value": "720p",
                          "label": "720p"
                        }
                      ],
                      "native_aspect_ratios": [
                        "9:16",
                        "4:5",
                        "1:1",
                        "16:9"
                      ],
                      "duration_note": "Any duration.",
                      "billing_note": "Free — nothing is generated."
                    },
                    {
                      "id": "google-veo",
                      "label": "Google Veo",
                      "blurb": "The model behind Google Flow, called with your own Gemini API key. Generates its own audio, so voiceover scripts are worth sending.",
                      "needs_key": true,
                      "key_set": false,
                      "key_hint": "",
                      "model": "veo-3.1-generate-preview",
                      "resolution": "720p",
                      "models": [
                        {
                          "id": "veo-3.1-generate-preview",
                          "label": "Veo 3.1 — highest quality"
                        },
                        {
                          "id": "veo-3.1-fast",
                          "label": "Veo 3.1 Fast — quicker, cheaper"
                        },
                        {
                          "id": "veo-3.1-lite",
                          "label": "Veo 3.1 Lite — cheapest"
                        }
                      ],
                      "resolutions": [
                        {
                          "value": "720p",
                          "label": "720p"
                        },
                        {
                          "value": "1080p",
                          "label": "1080p"
                        }
                      ],
                      "native_aspect_ratios": [
                        "9:16",
                        "16:9"
                      ],
                      "duration_note": "4, 6 or 8 second clips.",
                      "billing_note": "Billed per second to the key's Google Cloud project. A Google AI Pro or Ultra subscription covers the Gemini app and Flow, not the API — this is a separate charge, and Flow credits cannot be spent here."
                    },
                    {
                      "id": "fal-seedance",
                      "label": "Seedance (via fal)",
                      "blurb": "ByteDance Seedance on fal's queue API. Takes frames as URLs and covers more ratios and durations than Veo, so less of the board gets squared up.",
                      "needs_key": true,
                      "key_set": false,
                      "key_hint": "",
                      "model": "pro",
                      "resolution": "720p",
                      "models": [
                        {
                          "id": "pro",
                          "label": "Seedance 1 Pro — highest quality"
                        },
                        {
                          "id": "lite",
                          "label": "Seedance 1 Lite — quicker, cheaper"
                        }
                      ],
                      "resolutions": [
                        {
                          "value": "480p",
                          "label": "480p"
                        },
                        {
                          "value": "720p",
                          "label": "720p"
                        },
                        {
                          "value": "1080p",
                          "label": "1080p"
                        }
                      ],
                      "native_aspect_ratios": [
                        "9:16",
                        "1:1",
                        "16:9"
                      ],
                      "duration_note": "Whole seconds, 2 to 12.",
                      "billing_note": "Metered pay-as-you-go on your fal account, billed per second of video. There is no subscription tier that covers it."
                    },
                    {
                      "id": "openrouter",
                      "label": "OpenRouter Video",
                      "blurb": "One OpenRouter key for its video catalogue. The picker starts with Seedance; agents may use any current /videos/models id, with capabilities discovered at run time.",
                      "needs_key": true,
                      "key_set": true,
                      "key_hint": "sk-or-…9f0e",
                      "model": "bytedance/seedance-2.0-fast",
                      "resolution": "720p",
                      "models": [
                        {
                          "id": "bytedance/seedance-2.0",
                          "label": "Seedance 2.0 — highest quality"
                        },
                        {
                          "id": "bytedance/seedance-2.0-fast",
                          "label": "Seedance 2.0 Fast — quicker, cheaper"
                        },
                        {
                          "id": "bytedance/seedance-2.0-mini",
                          "label": "Seedance 2.0 Mini — economical"
                        },
                        {
                          "id": "bytedance/seedance-2.5",
                          "label": "Seedance 2.5 — long-form"
                        },
                        {
                          "id": "bytedance/seedance-1-5-pro",
                          "label": "Seedance 1.5 Pro — audio-visual"
                        }
                      ],
                      "resolutions": [
                        {
                          "value": "480p",
                          "label": "480p"
                        },
                        {
                          "value": "720p",
                          "label": "720p"
                        },
                        {
                          "value": "1080p",
                          "label": "1080p"
                        },
                        {
                          "value": "4K",
                          "label": "4K"
                        }
                      ],
                      "native_aspect_ratios": [
                        "9:16",
                        "1:1",
                        "16:9"
                      ],
                      "duration_note": "Discovered from the selected model at run time (Seedance presets span 4–30 seconds).",
                      "billing_note": "Metered against your OpenRouter credits. Price and supported inputs vary by model; Storyboard checks the live model catalogue before each run and reports every parameter substitution."
                    }
                  ]
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "provider",
            "in": "query",
            "required": true,
            "description": "The provider whose key to forget, e.g. `openrouter`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/settings/generation/models": {
      "get": {
        "operationId": "settings_generation_models",
        "summary": "Discover video models",
        "description": "Returns model capabilities without exposing the stored provider key. For OpenRouter this is the live `/videos/models` catalogue, including supported durations, resolutions, aspect ratios, frames, audio, seeds and pricing. Other providers return their static Storyboard catalogue. Use this before selecting a non-preset OpenRouter model.",
        "tags": [
          "settings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "generations:read"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "object": "list",
                  "provider": "openrouter",
                  "live": true,
                  "data": [
                    {
                      "id": "bytedance/seedance-2.0-fast",
                      "name": "ByteDance: Seedance 2.0 Fast",
                      "supported_resolutions": [
                        "480p",
                        "720p"
                      ],
                      "supported_aspect_ratios": [
                        "1:1",
                        "3:4",
                        "9:16",
                        "4:3",
                        "16:9"
                      ],
                      "supported_durations": [
                        4,
                        5,
                        6,
                        7,
                        8,
                        9,
                        10,
                        11,
                        12,
                        13,
                        14,
                        15
                      ],
                      "supported_frame_images": [
                        "first_frame",
                        "last_frame"
                      ],
                      "generate_audio": true,
                      "seed": true,
                      "pricing_skus": {
                        "video_tokens": "0.0000042"
                      }
                    }
                  ]
                }
              }
            }
          },
          "default": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "provider",
            "in": "query",
            "required": true,
            "description": "Provider id from GET /api/v1/settings/generation, e.g. `openrouter`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Integration token, e.g. `sb_live_…`. Scopes: projects:read (Read projects), projects:write (Write projects), characters:read (Read characters), characters:write (Write characters), products:read (Read products), products:write (Write products), audio:read (Read audio), audio:write (Write audio), generations:read (Read generations), generations:write (Run generations)."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unauthorized",
                  "forbidden",
                  "not_found",
                  "invalid_request",
                  "provider_error",
                  "server_not_configured",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string"
              },
              "details": {}
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "error"
        ]
      }
    }
  }
}