Storyboard
Integration APIv1
Agent guideOpenAPIOpen workspace

Storyboard integration API

Storyboard is built to be driven by agents. Hand one an integration token and it can build a board, write every panel, cast a locked creator and run generations — the same operations the workspace UI performs, over HTTP.

A short primer you can paste into an agent's context.

Getting started

1 · Mint a token

In the workspace, open Integration in the left rail and generate a token. Choose Full access for a trusted agent or Scoped access for least privilege. It is shown once — the server only ever stores a SHA-256 hash of it.

2 · Authenticate

Every request carries the token as a bearer credential. The account that owns the token is the account you act on — there is no way to reach anyone else's boards.

curl "https://your-app/api/v1/me" \
  -H "Authorization: Bearer sb_live_…"

3 · Scopes

Full-access tokens receive every current and future API capability. Scoped tokens carry only the permissions you tick when minting them. A call outside a token's scopes returns 403 forbidden and names what was missing.

projects:read
List boards, shots and their variation history.
projects:write
Create, edit, reorder and delete projects and shots, and upload panel frames.
characters:read
List saved creators and their character sheets.
characters:write
Create, edit and delete saved creators.
products:read
List saved products and their packshots.
products:write
Create, edit and delete saved products.
audio:read
List saved voiceover, music and effects tracks.
audio:write
Create, edit and delete saved audio tracks.
generations:read
Inspect generation jobs and their outcomes, and see which video model is active.
generations:write
Queue draft and variation runs, and change the active video model — including installing and forgetting provider API keys.

4 · Errors

Failures come back as { error: { code, message, details? } }. Branch on code, not on the message text.

unauthorized401
Missing, malformed, unknown, revoked or expired token.
forbidden403
The token is valid but lacks a required scope.
not_found404
No such project, shot, character or job on this account.
invalid_request422
The body failed validation. `details` says what was expected.
provider_error502
The video provider rejected or failed the run.
server_not_configured503
Server-side Firebase credentials are missing.
internal_error500
Something unexpected. Safe to retry once.

Projects

A project is one board — one piece of creative and its shot sequence.

GET/api/v1/projects

List projects

Every board on the account, in board order.

Scopesprojects:read
Request
curl -X GET "https://your-app/api/v1/projects" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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
}
POST/api/v1/projects

Create a project

Starts an empty board. Add shots to it next.

Scopesprojects:write
Body
namestringreq
Project name.
brandstring
Brand shown beside the name.
statusstring
`Draft`, `In review` or `Approved`. Defaults to `Draft`.
Request
curl -X POST "https://your-app/api/v1/projects" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"SPF 50 — batch B","brand":"Lumen"}'
Response · 201
{
  "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"
}
GET/api/v1/projects/{projectId}

Retrieve a project

One board, including its shot count.

Scopesprojects:read
Path parameters
projectId
The project's id.
Request
curl -X GET "https://your-app/api/v1/projects/{projectId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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"
}
PATCH/api/v1/projects/{projectId}

Update a project

Only the fields you send change.

Scopesprojects:write
Path parameters
projectId
The project's id.
Body
namestring
Project name.
brandstring
Brand label.
statusstring
`Draft`, `In review` or `Approved`.
ordernumber
Position in the project list.
Request
curl -X PATCH "https://your-app/api/v1/projects/{projectId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"Approved"}'
Response · 200
{
  "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"
}
DELETE/api/v1/projects/{projectId}

Delete a project

Removes the board, its shots and their variation history. Not reversible.

Scopesprojects:write
Path parameters
projectId
The project's id.
Request
curl -X DELETE "https://your-app/api/v1/projects/{projectId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "object": "deleted",
  "id": "1JcQm2xUaB",
  "deleted": true
}

Shots

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.

GET/api/v1/projects/{projectId}/shots

List shots

Panels in sequence order, left to right.

Scopesprojects:read
Path parameters
projectId
The project's id.
Request
curl -X GET "https://your-app/api/v1/projects/{projectId}/shots" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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
}
POST/api/v1/projects/{projectId}/shots

Create a shot

Appends a panel to the end of the sequence. Anything you leave out gets a sensible default, so a bare `{}` is a valid body.

Scopesprojects:write
Path parameters
projectId
The project's id.
Body
titlestring
Short name shown on the panel.
beatstring
Story beat and timing, e.g. `Proof · 0:03–0:07`.
statusstring
`Draft`, `Ready`, `Generated` or `Approved`.
duration_snumber
Shot length in seconds. The board's slider covers 1–10; the API accepts 0.5–60.
aspect_ratiostring
`9:16`, `4:5`, `1:1` or `16:9`. Drives the output resolution.
dialoguestring
Spoken line or voiceover.
actionstring
What happens with the product.
notesstring
Direction for whoever reviews the shot.
promptstring
The prompt sent to the video model.
annotationstring | null
Handwritten margin note on the panel. `null` removes it.
reference_namestring
Label for the start frame.
end_frame_enabledboolean
Frame the shot start-to-end. Setting it to `false` also clears the end frame.
character_idstring | null
Cast a saved character. The character's name and locked seed are copied onto the shot; `null` uncasts it.
product_idstring | null
Feature a saved product. Its packshot and slug ride along in the generation payload; `null` removes it.
audio_idsstring[]
The tracks laid over this shot, in order. Replaces the whole list; duplicates are collapsed.
Request
curl -X POST "https://your-app/api/v1/projects/{projectId}/shots" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"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"}'
Response · 201
{
  "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"
}
GET/api/v1/projects/{projectId}/shots/{shotId}

Retrieve a shot

One panel with every field the workspace shows.

Scopesprojects:read
Path parameters
projectId
The project's id.
shotId
The shot's id.
Request
curl -X GET "https://your-app/api/v1/projects/{projectId}/shots/{shotId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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"
}
PATCH/api/v1/projects/{projectId}/shots/{shotId}

Update a shot

Only the fields you send change. Setting `character_id` recasts the shot and copies the character's locked seed onto it.

Scopesprojects:write
Path parameters
projectId
The project's id.
shotId
The shot's id.
Body
titlestring
Short name shown on the panel.
beatstring
Story beat and timing, e.g. `Proof · 0:03–0:07`.
statusstring
`Draft`, `Ready`, `Generated` or `Approved`.
duration_snumber
Shot length in seconds. The board's slider covers 1–10; the API accepts 0.5–60.
aspect_ratiostring
`9:16`, `4:5`, `1:1` or `16:9`. Drives the output resolution.
dialoguestring
Spoken line or voiceover.
actionstring
What happens with the product.
notesstring
Direction for whoever reviews the shot.
promptstring
The prompt sent to the video model.
annotationstring | null
Handwritten margin note on the panel. `null` removes it.
reference_namestring
Label for the start frame.
end_frame_enabledboolean
Frame the shot start-to-end. Setting it to `false` also clears the end frame.
character_idstring | null
Cast a saved character. The character's name and locked seed are copied onto the shot; `null` uncasts it.
product_idstring | null
Feature a saved product. Its packshot and slug ride along in the generation payload; `null` removes it.
audio_idsstring[]
The tracks laid over this shot, in order. Replaces the whole list; duplicates are collapsed.
Request
curl -X PATCH "https://your-app/api/v1/projects/{projectId}/shots/{shotId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Tighter framing on the dropper.","status":"Ready"}'
Response · 200
{
  "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"
}
DELETE/api/v1/projects/{projectId}/shots/{shotId}

Delete a shot

Removes the panel and its variations, and updates the project's shot count.

Scopesprojects:write
Path parameters
projectId
The project's id.
shotId
The shot's id.
Request
curl -X DELETE "https://your-app/api/v1/projects/{projectId}/shots/{shotId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "object": "deleted",
  "id": "s7YbQ1pW0k",
  "deleted": true
}
POST/api/v1/projects/{projectId}/shots/reorder

Reorder the sequence

Rewrites the running order in one call. `shot_ids` must list every shot in the project exactly once.

Scopesprojects:write
Path parameters
projectId
The project's id.
Body
shot_idsstring[]req
Every shot id, in the order you want them.
Request
curl -X POST "https://your-app/api/v1/projects/{projectId}/shots/reorder" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"shot_ids":["s1","s3","s2","s4","s5"]}'
Response · 200
{
  "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
}
PUT/api/v1/projects/{projectId}/shots/{shotId}/frame

Upload a panel frame

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.

Scopesprojects:write
Path parameters
projectId
The project's id.
shotId
The shot's id.
position
`start` (default) or `end`.
Body

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://…" }`.

image_urlstring
Only for `application/json` requests — a public https URL the server downloads.
Request
curl -X PUT "https://your-app/api/v1/projects/{projectId}/shots/{shotId}/frame" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN" \
  -H "Content-Type: image/jpeg" \
  --data-binary @panel.jpg
Response · 200
{
  "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"
}
GET/api/v1/projects/{projectId}/shots/{shotId}/variations

List variations

Every take generated for the shot, newest first. Exactly one is marked `current`.

Scopesprojects:read
Path parameters
projectId
The project's id.
shotId
The shot's id.
Request
curl -X GET "https://your-app/api/v1/projects/{projectId}/shots/{shotId}/variations" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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
}

Characters

Saved creators. A character's seed is what keeps the same face across every shot it is cast into.

GET/api/v1/characters

List characters

Saved creators on the account. Characters are shared across every project.

Scopescharacters:read
Request
curl -X GET "https://your-app/api/v1/characters" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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
}
POST/api/v1/characters

Create a character

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.

Scopescharacters:write
Body
namestringreq
Display name.
handlestring
Social handle, e.g. `@mayarivs`.
agestring
Apparent age, free text.
lockedboolean
Whether the appearance is locked.
seednumber
Seed carried into every shot.
lookstring
One-line summary shown on the card.
voicestring
How they speak.
fields{ k, v }[]
The character sheet — appearance, wardrobe, setting, voice & tone, and what to never do.
Request
curl -X POST "https://your-app/api/v1/characters" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"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."}]}'
Response · 201
{
  "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"
}
GET/api/v1/characters/{characterId}

Retrieve a character

One saved creator and their full sheet.

Scopescharacters:read
Path parameters
characterId
The character's id.
Request
curl -X GET "https://your-app/api/v1/characters/{characterId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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"
}
PATCH/api/v1/characters/{characterId}

Update a character

Changing the name or seed also updates every shot this character is cast into, so the board stays consistent.

Scopescharacters:write
Path parameters
characterId
The character's id.
Body
namestring
Display name.
handlestring
Social handle.
agestring
Apparent age.
lockedboolean
Lock or unlock the appearance.
seednumber
Seed carried into every shot.
lookstring
Card summary.
voicestring
How they speak.
fields{ k, v }[]
Character sheet rows.
Request
curl -X PATCH "https://your-app/api/v1/characters/{characterId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"locked":true,"seed":48211}'
Response · 200
{
  "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"
}
DELETE/api/v1/characters/{characterId}

Delete a character

Shots already cast keep the name and seed they were given; they simply stop pointing at a saved sheet.

Scopescharacters:write
Path parameters
characterId
The character's id.
Request
curl -X DELETE "https://your-app/api/v1/characters/{characterId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "object": "deleted",
  "id": "cMayaR001",
  "deleted": true
}

Products

The thing being sold. A shot features one product; its packshot and name ride along in the generation payload.

GET/api/v1/products

List products

Saved products on the account, shared across every project.

Scopesproducts:read
Request
curl -X GET "https://your-app/api/v1/products" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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
}
POST/api/v1/products

Create a product

Packshots are uploaded from the workspace; everything else can be written here.

Scopesproducts:write
Body
namestringreq
Product name.
brandstring
Brand it belongs to.
variantstring
Size, strength or SKU line.
descriptionstring
What it looks like in frame — material, cap, label.
rulesstring
How it must always be shown. Read this before writing a prompt.
Request
curl -X POST "https://your-app/api/v1/products" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"SPF 50 Fluid","brand":"Lumen","variant":"50ml · broad spectrum","rules":"Cap stays on unless the shot is the application beat."}'
Response · 201
{
  "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"
}
GET/api/v1/products/{productId}

Retrieve a product

One product and its rules.

Scopesproducts:read
Path parameters
productId
The product's id.
Request
curl -X GET "https://your-app/api/v1/products/{productId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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"
}
PATCH/api/v1/products/{productId}

Update a product

Renaming also updates the label on every shot featuring it, so boards stay consistent.

Scopesproducts:write
Path parameters
productId
The product's id.
Body
namestring
Product name.
brandstring
Brand it belongs to.
variantstring
Size, strength or SKU line.
descriptionstring
What it looks like in frame — material, cap, label.
rulesstring
How it must always be shown. Read this before writing a prompt.
Request
curl -X PATCH "https://your-app/api/v1/products/{productId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"variant":"50ml · broad spectrum, fragrance free"}'
Response · 200
{
  "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"
}
DELETE/api/v1/products/{productId}

Delete a product

Shots keep the name they were given; they simply stop pointing at a saved product.

Scopesproducts:write
Path parameters
productId
The product's id.
Request
curl -X DELETE "https://your-app/api/v1/products/{productId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "object": "deleted",
  "id": "pSerum001",
  "deleted": true
}

Audio

Voiceover lines, music beds and effects. A shot can carry several, in order.

GET/api/v1/audio

List audio tracks

Every saved voiceover line, music bed and effect.

Scopesaudio:read
Request
curl -X GET "https://your-app/api/v1/audio" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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
}
POST/api/v1/audio

Create an audio track

Tracks are metadata today — script, read direction and length. Lay one over a shot with `audio_ids`.

Scopesaudio:write
Body
namestringreq
Track name.
kindstring
`Voiceover`, `Music`, `SFX` or `Ambience`.
scriptstring
The spoken line, for voiceover.
voicestring
How it should sound — pace, timbre, energy.
duration_snumber
Length in seconds, or `null` if it doesn't matter yet.
notesstring
Mixing or timing direction.
Request
curl -X POST "https://your-app/api/v1/audio" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"CTA line","kind":"Voiceover","script":"“Link’s in my bio. Go.”","duration_s":3}'
Response · 201
{
  "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"
}
GET/api/v1/audio/{audioId}

Retrieve an audio track

One track and its direction.

Scopesaudio:read
Path parameters
audioId
The track's id.
Request
curl -X GET "https://your-app/api/v1/audio/{audioId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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"
}
PATCH/api/v1/audio/{audioId}

Update an audio track

Only the fields you send change. Send `duration_s: null` to clear the length.

Scopesaudio:write
Path parameters
audioId
The track's id.
Body
namestring
Track name.
kindstring
`Voiceover`, `Music`, `SFX` or `Ambience`.
scriptstring
The spoken line, for voiceover.
voicestring
How it should sound — pace, timbre, energy.
duration_snumber
Length in seconds, or `null` if it doesn't matter yet.
notesstring
Mixing or timing direction.
Request
curl -X PATCH "https://your-app/api/v1/audio/{audioId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"script":"“Link’s in my bio.”","duration_s":2.5}'
Response · 200
{
  "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"
}
DELETE/api/v1/audio/{audioId}

Delete an audio track

Removes it from the library and from any shot carrying it.

Scopesaudio:write
Path parameters
audioId
The track's id.
Request
curl -X DELETE "https://your-app/api/v1/audio/{audioId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "object": "deleted",
  "id": "aVoProof01",
  "deleted": true
}

Generation

Queue takes against the video model. Jobs are asynchronous — start one, then poll it. The workspace's Generate button calls exactly these endpoints.

POST/api/v1/projects/{projectId}/shots/{shotId}/generate

Generate one variation

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`.

Scopesgenerations:write
Path parameters
projectId
The project's id.
shotId
The shot's id.
Body
modelstring
Model id. Defaults to `ugc-video-01`.
motion_strengthnumber
0–1. Defaults to 0.45.
seednumber
Overrides the character's locked seed for this run.
Request
curl -X POST "https://your-app/api/v1/projects/{projectId}/shots/{shotId}/generate" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"motion_strength":0.6}'
Response · 202
{
  "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
}
POST/api/v1/projects/{projectId}/generations

Generate a draft

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.

Scopesgenerations:write
Path parameters
projectId
The project's id.
Body
shot_idsstring[]
Force specific shots instead of the pending ones.
modelstring
Model id. Defaults to `ugc-video-01`.
motion_strengthnumber
0–1. Defaults to 0.45.
Request
curl -X POST "https://your-app/api/v1/projects/{projectId}/generations" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"model":"ugc-video-01","motion_strength":0.45}'
Response · 202
{
  "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
}
POST/api/v1/projects/{projectId}/generations/{generationId}/poll

Advance a running job

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.

Scopesgenerations:write
Path parameters
projectId
The project's id.
generationId
The job's id.
Request
curl -X POST "https://your-app/api/v1/projects/{projectId}/generations/{generationId}/poll" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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"
}
GET/api/v1/projects/{projectId}/generations

List generation jobs

The 50 most recent jobs for the project, newest first.

Scopesgenerations:read
Path parameters
projectId
The project's id.
Request
curl -X GET "https://your-app/api/v1/projects/{projectId}/generations" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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
}
GET/api/v1/projects/{projectId}/generations/{generationId}

Retrieve a generation job

The job's status, the exact request body that was sent, and any provider error.

Scopesgenerations:read
Path parameters
projectId
The project's id.
generationId
The job's id.
Request
curl -X GET "https://your-app/api/v1/projects/{projectId}/generations/{generationId}" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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
}

Settings

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.

GET/api/v1/settings/generation

Read the video model setup

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`.

Scopesgenerations:read
Request
curl -X GET "https://your-app/api/v1/settings/generation" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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."
    }
  ]
}
GET/api/v1/settings/generation/models

Discover video models

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.

Scopesgenerations:read
Path parameters
provider
Provider id from GET /api/v1/settings/generation, e.g. `openrouter`.
Request
curl -X GET "https://your-app/api/v1/settings/generation/models?provider=PROVIDER" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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"
      }
    }
  ]
}
PUT/api/v1/settings/generation

Choose the active model

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.

Scopesgenerations:write
Body
providerstringreq
`mock`, `google-veo`, `fal-seedance` or `openrouter`.
modelstring
One of the provider's `models[].id`. OpenRouter also accepts any current video `provider/model` id. Defaults to the provider's own default.
resolutionstring
One of the provider's `resolutions[].value`. Defaults to 720p.
api_keystring
Only needed until a key is installed. Verified before it is stored, and stored server-side only.
Request
curl -X PUT "https://your-app/api/v1/settings/generation" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"provider":"openrouter","model":"bytedance/seedance-2.0-fast","resolution":"720p","api_key":"sk-or-v1-…"}'
Response · 200
{
  "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."
    }
  ]
}
DELETE/api/v1/settings/generation

Forget a provider key

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.

Scopesgenerations:write
Path parameters
provider
The provider whose key to forget, e.g. `openrouter`.
Request
curl -X DELETE "https://your-app/api/v1/settings/generation?provider=PROVIDER" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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."
    }
  ]
}

Service & identity

Two calls worth making before anything else.

GET/api/v1

Service index

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.

Scopesnone — public
Request
curl -X GET "https://your-app/api/v1"
Response · 200
{
  "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"
}
GET/api/v1/me

Who am I

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.

Scopesany valid token
Request
curl -X GET "https://your-app/api/v1/me" \
  -H "Authorization: Bearer $STORYBOARD_TOKEN"
Response · 200
{
  "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"
  }
}