# Splice Agent API: full reference > The complete guide to calling Splice (film.fun's AI filmmaking studio) directly over HTTP with an agent key. The short version is https://splice.film.fun/llms.txt. The machine-readable route catalog is https://splice.film.fun/api/agents/manifest.json. It needs no key, and CI checks it against the code, so treat it as the authority on which routes exist. Base URL: `https://splice.film.fun`. Every path below is relative to it. JSON in, JSON out, unless a section says otherwise. --- ## 1. Authentication and keys - **Create a key.** Sign in at https://splice.film.fun, then open **Dashboard → API keys** (`/dashboard/api-keys`). The key looks like `sk_…` and is shown once. You can revoke it on the same page. - **Send it on every call** as `Authorization: Bearer sk_…` or as `x-api-key: sk_…`. - **A key acts as the user who made it.** It shares their productions, credits and billing, and everything it creates appears in their dashboard. New accounts start with 100 free credits. - **Default production.** A key can have a default production, set when you create it. Calls that leave out `production_id` then use it. If the key has none, they use the account's default, which `GET /api/agents/session` creates the first time it's called. - **Scopes.** Keys made in the dashboard have every scope. The manifest's `scopes` lists the scope each route needs. A key without the scope gets `403 {"error":{"code":"forbidden","message":"Key does not have scope '…'","details":{"required_scope":…}}}`. - Never put a key in client-side code or a URL. Keys are hashed at rest, so a lost key can't be recovered: make a new one. ## 2. Productions: the two ids A production is a project container that holds the generated and uploaded media (its **items**). It has two ids: | id | Where you see it | Use it for | |----|------------------|-----------| | `creation_id` (UUID) | `session.creation_id`, `productions[].id`, and `production_id` in every generate response | **Everything.** Prefer this one. | | `ff_production_id` | `session.production_id`, `creation.ff_production_id` | Accepted on nested `/productions/{id}/…` routes and in body `production_id`, but not on `GET/PATCH/DELETE /productions/{id}` itself | Rule of thumb: store `creation_id` and pass it everywhere. A generate call echoes back the `production_id` (a creation id) that the item actually went to. **Poll that one.** If you left `production_id` out, the item went to the default production, which may not be the one you expected. ## 3. Quickstart: create, generate, poll ```bash export SPLICE_API_KEY=sk_... B=https://splice.film.fun/api/agents; H="Authorization: Bearer $SPLICE_API_KEY" curl -s -X POST "$B/productions" -H "$H" -H 'Content-Type: application/json' \ -d '{"title":"Rainy Boat","aspect_ratio":"9:16"}' # 201 {"creation":{"id":"",…},"production_id":"","success":true} curl -s "$B/tools/generator-tools/image/models" -H "$H" # choose a modelId curl -s -X POST "$B/generate/image" -H "$H" -H 'Content-Type: application/json' \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"production_id":"","prompt":"Paper boat on a rain-soaked street, neon reflections","aspectRatio":"9:16"}' # 201 {"production_item_id":"","job_id":"","production_id":""} curl -s "$B/productions//items/" -H "$H" # {"id":"","status":"processing",…} then later # {"id":"","status":"completed","media_url":"https://….png","type":"image",…} ``` Then feed the result into the next step. Pass `media_url` (or `item:` on the routes that accept item refs, see §7), for example as the first frame of a video: ```bash curl -s -X POST "$B/generate/video" -H "$H" -H 'Content-Type: application/json' -H "Idempotency-Key: $(uuidgen)" \ -d '{"production_id":"","prompt":"The boat drifts toward the drain, slow push-in","modelId":"minimaxH3MaxTurbo", "parameters":{"firstFrameImage":"","duration":6,"aspectRatio":"9:16"}}' ``` ## 4. Polling results (no webhooks) `GET /api/agents/productions/{id}/items/{itemId}` returns: ```json { "id": "…", "status": "pending|processing|completed|failed", "media_url": "https://…" | null, "type": "image|video|audio|…", "name": "…", "created_at": "…", "job_id": "…", "job_status": "…", "error_message": "…"?, "cancelled": true?, "answer": "…"? } ``` - **Done:** `status: "completed"`. Read `media_url`. A vision-language result has no media; read `answer`. - **Failed:** `status: "failed"`. Read `error_message`. A pending item whose job failed, timed out or was cancelled is reported as `failed`. - **Rhythm:** poll every 3–10 s with backoff. A 404 in the first seconds after dispatch can be ignored; keep polling. Images usually finish in seconds, while video and long renders take minutes. - **Cancel:** `POST /productions/{id}/items/{itemId}/cancel` exists, but it returns `501 cancel_unavailable` while `features.cancel.enabled` is false in the manifest. - `GET /productions/{id}/items?type=&status=&name=&tag=` lists the newest 50 items (`{items:[…]}`). - `media_url` values are public storage URLs. Download or re-host anything you need to keep. ## 5. Idempotency: retry without paying twice Send `Idempotency-Key: ` on generate, edit and other paid POSTs (routes marked `idempotent: true` in the manifest). - Same key and same body within 24 h: you get the original response back, with the `Idempotent-Replayed: true` header, and no new charge. - The first request is still running: `409 {"error":{"code":"idempotency_in_progress",…}}` with `Retry-After: 5`. - Same key with a different body: `409 conflict`. - The first call's outcome is unknown and it may have been charged: `409 idempotency_outcome_unknown`. Look it up, or use a new key to generate again. - `GET /api/agents/idempotency/{key}` returns `{status: pending|completed|failed, item_id, job_id, production_id, response_status, response}`. Recovery after a crash starts here. - A `402` (not enough credits) charges nothing, so after you top up, retry with the same key. ## 6. Choosing models - `GET /api/agents/tools/generator-tools` lists every generator tool with its enabled models. - `GET /api/agents/tools/generator-tools/{generatorToolId}/models` returns `{data:[{id, name, icon, is_default, tool_costs, generator_metadata}]}`. - `id` is the `modelId` to send. - `tool_costs` is the price in credits. Some models price per second or per resolution; the charge uses the same pricing. - `generator_metadata` lists the capabilities: aspect ratios, max duration, first/last-frame support, seed and so on. - `generatorToolId` values: `image`, `video`, `video-v2` (the "Video 2" list: reference-media models such as `minimaxH3MaxReference`, plus the shot-sequence models; it runs through `/generate/video`), `video-animate`, `video-image-audio`, `voice`, `voice-changer`, `music`, `sfx`, `auto-sfx`, `lipsync`, `vision-language`, `character-sheet`, `location-sheet`, `prop-sheet`, `set-sheet`, `shot-framing`, `caption-burner`, `image-edit`, `media-merge`, `timeline-export`, `transcription`, `composition-render`. - If you leave `modelId` out, the route uses the default. **Always discover models live; models are added and retired often.** Examples at the time of writing: - video: `minimaxH3MaxTurbo` (the default), `minimaxH3`, `seedance25`, `ltx23Fast`, `googleGeminiOmni11` (no `duration`: the model picks up to ~10 s, billed as 10 s) - image: `googleNanaBanana2` (Nano Banana — the original model despite the id), `googleNanoBanana20` (Nano Banana 2), `googleNanoBanana21` (Nano Banana 2.1), `ideogram45` - image-edit: `googleNanoBanana20`, `googleNanoBanana21`, `ideogram45PreciseEdit` (optional `maskUrl`: black = edit) - **Known limits:** - Seedance 2.5 (`seedance25`) prompts are cut to 2000 characters. Anything longer is truncated, not rejected. - Reference-image video models (`seedance25`, `minimaxH3`) take `referenceImages: [url…]` for identity. Image-to-video models take `firstFrameImage` (and `lastFrameImage` where supported). - Upload bodies are capped at 4.5 MB. ## 7. Generate and edit routes All are `POST` with a JSON body, return `201 {production_item_id, job_id?, production_id}`, and accept `production_id` (optional), `modelId` (optional), `parameters` (optional) and an `Idempotency-Key` header. **`parameters` is forwarded to the model as-is and is the safe place for model settings** such as `duration`, `resolution`, `aspectRatio`, `firstFrameImage`, `referenceImages`, `seed` and `generate_audio`. `/generate/video` in particular **ignores top-level keys other than `prompt`, `modelId` and `include_universe`**: put everything else in `parameters`. | Route | Required | Notes | |---|---|---| | `/generate/image` | `prompt` | Also takes top-level `aspectRatio`, `width`, `height`, `seed`, `negativePrompt`, `referenceImages[]`, `styleImage` | | `/generate/video` | `prompt` | Text-to-video, or image-to-video via `parameters.firstFrameImage` / `referenceImages` | | `/generate/video-animate` | `input_url` (URL), `prompt` | Animates a still | | `/generate/video-image-audio` | `image_url`, `prompt` | Talking photo: image + audio → video | | `/generate/voice` | `text` | TTS; voice id etc. in `parameters` | | `/generate/voice-changer` | `voice_id`, `audio_url` | | | `/generate/music` | `prompt` and/or `lyrics` | | | `/generate/sfx` | `prompt` | `duration`, optional `video_url` | | `/generate/auto-sfx` | `video_url` | Sound design for an existing video | | `/generate/lipsync` | `video_url`, `audio_url` | | | `/generate/vision-language` | `video_url`, `prompt` | Text answer: poll the item and read `answer` | | `/generate/character-sheet` (`location-`, `prop-`, `set-sheet`) | `characterDescription` (or `locationDescription` / `propDescription` / `setDescription`), `styleDirectives` | Optional structured `sheet`; set-sheet also takes `location_id` + `set_id` | | `/generate/shot-framing` | `prompt` | Composes a frame from entities; `@slug` expansion (§9) | | `/generate/composition-render` | `compositionHtml` | Motion (HTML + GSAP) → MP4 (§10) | | `/generate/timeline-export` | `timeline_json` (string) | Renders a timeline spec to video | | `/edit/clip` | `video_url` | `start_ms`/`end_ms` or `start_time`/`end_time` (s) | | `/edit/image` | `input_url`, `prompt` | Inpaint, outpaint or variation | | `/edit/image-multiple`, `/edit/image-draw` | `input_image_urls[]`, `prompt` | | | `/edit/image-upscale` | `input_url` | | | `/edit/image-color-grade` | `image_url` | | | `/edit/media-merge` | `input_urls[]` | Concatenate videos in order. Output shape: `parameters.outputAspectRatio` (e.g. `16:9`) or `width`/`height`; omitted, it follows the production's aspect ratio, else the first input's shape. Every input is cropped to that shape. | | `/edit/caption-burner` | `video_url` | Burn captions | | `/edit/video-reframe` | `video_url` | `aspect_ratio` 9:16 (default) / 4:5 / 1:1 / 16:9; follows the subject. `model_id: "lumaReframeVideo"` fills the new frame with AI instead | | `/transcribe` | `audio_url` (JSON) or multipart `file` (≤4.5 MB) | `task`, `language`, `timestamp: chunk\|word` | - **`include_universe: true`** (on image, video, vision-language, the four sheets and shot-framing) puts the production's Universe Bible (lore, style guide) in front of the prompt. - **Item refs.** A URL field can take `item:` instead of a URL, and Splice swaps in that item's `media_url`. This works on `/transcribe`, `/generate/shot-framing`, `sfx`, `auto-sfx`, `lipsync`, `voice-changer`, `video-image-audio`, the four sheet routes, `composition-render` and `timeline-export`, plus `/edit/clip`, `caption-burner`, `image-color-grade`, `image-draw`, `image-multiple` and `video-reframe`. - It does **not** work on `/generate/{image,video,video-animate,voice,music,vision-language}` or `/edit/{image,image-upscale,media-merge}`. Pass the `media_url` there instead. - An unknown ref returns `400 {"error":"Unknown production item ref(s): …","unresolved_items":[…]}`. ## 8. Media upload `POST /api/agents/productions/{id}/media` takes `multipart/form-data` with the file in a `file` field. It's capped at **4.5 MB** (a platform limit). The optional fields are `name`, `description`, `type` (`image|video|audio|pdf|text`), `tags` (a JSON array) and `metadata` (a JSON object). It returns `201 {"media_item":{"id","name","type","media_url","url","file_size","created_at"}}`, and the `id` works as an `item:` ref. Any other content type gets `415`. For larger files, host the file yourself and pass its URL to the generator. ```bash curl -s -X POST "$B/productions//media" -H "$H" -F file=@hero.png -F name=hero ``` ## 9. Entities, @slugs and shot framing - **Library (client-wide).** `GET|POST /api/agents/{characters|locations|props}`, plus `GET|PUT /api/agents/characters/{id}`. The query params are `page`, `limit`, `search` and `status`; locations also take `type`, props also take `category`. Sets live inline on `location.sets[]`. - **In a production.** `GET|POST /api/agents/productions/{id}/{characters|locations|props}` lists entities or imports a library entity. `GET /productions/{id}/universe-bundle` returns the bible and every entity in one call. `GET|POST /productions/{id}/universe` reads or writes the Universe Bible. - **Slugs.** `GET|POST|DELETE /productions/{id}/entity-slugs` manages them. - The POST body is `{slug, entity_type: character|location|prop|location_set, entity_id}`; slugs match `^[a-z0-9_-]+$`. - A slug that is already taken returns 409. - **Shot framing.** `POST /generate/shot-framing {prompt:"@sarah reads a letter in @office_day", aspectRatio, cameraAngle, …}`. - Each `@slug` is replaced with the entity's name, and its default image is routed into the right reference slot. - Explicit `characterImageUrls[]`, `locationImageUrl`, `setImageUrl`, `propImageUrls[]` and `additionalImageUrls[]` are merged in. - An unknown slug returns `400 {"error":"Unknown entity slug(s): @foo","unknown_slugs":["foo"]}`. - The response adds `resolved_slugs[]` and `rewritten_prompt`. ## 10. Motion compositions (HTML + GSAP → MP4) - `GET|POST /api/agents/compositions` lists or creates compositions. Create takes `{title, description?, payload}`, where `payload` is the composition HTML (max about 5 MB). - `GET|PATCH|DELETE /compositions/{id}` reads, updates or deletes one. `POST /compositions/{id}/duplicate` copies it. - `GET /compositions/{id}/revisions` shows the history, and `POST /compositions/{id}/revisions/revert {revision_id}` rolls back to a revision. - `POST /generate/composition-render {compositionHtml, durationSeconds?, width?, height?, fps?, assets?[{url,name?}], outputFormat?: mp4|webm|png, previewSeconds?, previewStartSeconds?}` renders the composition. - It returns an item to poll like any other generation. - `previewSeconds` renders a short range at the lower preview price. - Heavy, image-dense HD compositions can time out; keep assets light or render a range. ## 11. Templates There are two kinds, and they are easy to confuse. - **Generation templates.** This is the public library at https://splice.film.fun/templates: ready-made prompts with model and settings, with categories such as Camera moves, Anime and Product & ads. - Browse without a key: `GET /api/public/generation-templates?category=&media_kind=image|video&q=&new=true&sort=&featured=true&limit=(≤24)&offset=` returns `{generation_templates:[{id, name, category, media_kind, generator_tool_id, tool_id, payload, preview_url, …}]}`. - `GET /api/public/generation-templates/categories` returns `{categories:[{category, count, newest_at}]}`. - One template: `GET /api/public/generation-templates/{slug}` (a UUID works too) returns `{generation_template:{id, slug, name, prompt, payload, preview_url, …}}`, or 404 for an unknown or unpublished template. Each template has a stable `slug`; its shareable page is `https://www.film.fun/templates/{slug}` (also served at `https://splice.film.fun/templates/{slug}`). - **To use one over the API,** POST to `/api/agents/generate/{generator_tool_id}` with `prompt` = `payload.prompt`, `modelId` = `payload.modelId`, and the rest of `payload` inside `parameters`. Swap in your own reference or first-frame image URL; the template's own images are only examples. - `splice templates use ` does all of this for you. In the browser, "Use" / "Remix" opens `/dashboard/templates/remix/{id}`. - **Video templates.** These are scaffolds scoped to your account for a pipeline stage: script, storyboard or shotlist. - `GET|POST /api/agents/video-templates?kind=script|storyboard|shotlist`. Create one from an existing row with `{kind, name, source_id}` or seed it with `{kind, name, payload}`. - `POST /api/agents/video-templates/{templateId}/apply {production_id}` creates the row in a production. ## 12. Planning and pipeline routes (idea → film) All of these are under `/api/agents/productions/{id}/`: - `draft-pipeline {pitch, title?}`: writes a 60-second script, then a storyboard and a shotlist from it. It takes about a minute, so use a long timeout. - `scripts`, `storyboards`, `shotlists` (GET/POST, plus `/{id}` GET/PUT), `takes`, `moodboards`, `shotboards` (shown as "Assembly" in the UI), `tasks`, `transcriptions` and `clips` (transcript-driven trims; `publish-video` renders them). - `stages` returns counts for each stage. `readiness` returns `suggested_next_steps[]`. - `shot-sheet?shotlist_id=` is a GET/PATCH aggregator. Under it: - `shot-sheet/stills` and `shot-sheet/stills/all` make the stills. - `shot-sheet/seedance` and `shot-sheet/seedance/all` make the video, voicing each shot's dialogue. - `/all` accepts `dry_run` and `max_credits`, reuses unchanged takes at 0 credits, and returns `pending` when there's more to do: call it again. - `shot-sheet/from-script?script_id=` and `shot-sheet/cost-preview` also live here. - `quote`: an itemised price before a multi-step job, and it spends nothing. It takes `{shotlist_id, …}` or `{steps:[…]}`. - `variants {shotlist_id, axis: hook_line|cast|language|aspect|music, variants[]}`: A/B arms that each change one thing. - `reference-analysis`: reads a reference video for its structure (never its footage) and returns a timed shot plan. It requires `rights: {attested: true, statement_version}`. - `share-links`: review and present links that people can open (`public_url`). `GET /share-links/{linkId}` includes comments and decisions. - `studio-handoff?tool_slug=`: gives dashboard URLs for a person to continue in the browser. `studio/projects` (and `/{projectId}/export`) cover timeline projects and render export. - `GET /api/agents/series` and `GET /api/agents/productions?series_id=` group productions into series (`series_id` and `episode_number` are set via PATCH). - `POST /api/agents/shotlists/{shotlistId}/promote {production_id}` turns a shotlist into a shotboard. - `POST /api/agents/skill/brand-dna/extract` extracts a palette, type and voice from a URL or text. **Flows** (the node-graph canvas at `/dashboard/{creation_id}/workflow`) have no agent API yet. Build and run them in the dashboard. ## 13. Credits and buying - `GET /api/agents/balance` returns `{success, balance:{total, available, allocated_to_productions, productions[]}}`. - A generation that can't be paid for returns **402**: `{"error":"insufficient_credits","message":"…","required":50,"balance":10,"shortfall":40,"hint":"…"}`. Nothing is charged. - Top up in the dashboard (card), or programmatically on Solana (USDC, SOL or AUDD): `POST /api/agents/credits/purchase {pack_id: starter|creator|pro|studio, payment_token: USDC|SOL|AUDD}` (scope `credits:write`). Without an `X-PAYMENT` header it returns **402** with `payment_required`: `transaction_id`, `receiver_address`, `amount`, `amount_atomic`, `token_mint`, and `solana_pay {url, reference, status_url}`. Then either: - **Solana Pay (easiest):** pay `solana_pay.url` from any Solana wallet. It is a standard Solana Pay transfer request (`solana:?amount=…&spl-token=…&reference=…`), so a person can also scan it as a QR code. Poll `GET /api/agents/credits/purchase/{transaction_id}` every few seconds until `{"status":"completed","credits_applied":…}`. The server finds the payment by its `reference` and checks the token, receiver, amount and reference; you don't report a signature. - **x402 (you sign the transfer):** send the transfer, then retry the same body with `X-PAYMENT: {"transaction_id","signature","payer_address"}` to get `200 {credits_applied}`. - Both are idempotent: credits apply once per purchase, and one on-chain payment can only ever pay for one purchase. Check `GET /api/agents/balance` afterwards. - `GET /api/agents/credits/packs` (scope `balance:read`) lists the packs with their price now in each token: `{packs:[{id, name, credits, price_usd, credits_per_usd, quotes:{USDC|SOL|AUDD:{amount, amount_atomic, decimals, mint}}}]}`. SOL is priced at the live SOL/USD price and AUDD at the live AUD/USD rate; USDC is 1:1. When no live SOL price is available, SOL is left out of `quotes` and a SOL purchase returns **503** (pay with USDC or AUDD, or retry). The purchase's 402 is the binding quote: verification enforces that amount even if the price moves before you pay. - **An agent-held wallet (CLI).** `splice wallet create` writes a standard Solana keypair to `~/.config/splice/wallet.json` (owner-only permissions; never overwritten). Fund it once with USDC (or SOL) plus about 0.01 SOL for fees. Then `splice credits buy [--token USDC|SOL|AUDD]` starts the purchase, checks the caps and the wallet balance, signs and sends the Solana Pay transfer (with the purchase reference), waits for confirmation and polls until the credits land. `--dry-run` shows the plan without paying. Caps: `SPLICE_WALLET_MAX_USD` per purchase (default 50) and `SPLICE_WALLET_DAILY_USD` per rolling 24 h (default 100), tracked in `spend-log.json`. `splice wallet balance` shows SOL, USDC and AUDD. - Top-up loop for an agent: read the balance → if `available` is below what the next job needs, buy the smallest pack that covers it → hand `solana_pay.url` to the wallet (or the person) → poll the status → continue. | Pack | Credits | USD | |------|---------|-----| | starter | 1,000 | $10 | | creator | 5,000 | $45 | | pro | 11,500 | $99 | | studio | 27,000 | $225 | - `POST /api/agents/credits/transfer {production_id, amount, direction: to_production|from_production}` moves credits between your account and one production's own budget. `production_id` here is the `ff_production_id`. ## 14. Errors Every error from `/api/agents/*` has one envelope (since API version 2026-10-01): ```json {"error": {"code": "insufficient_credits", "message": "Not enough credits.", "details": {}}} ``` - `code` is stable snake_case: match on it, not on `message`. Validation failures are `{"error":{"code":"invalid_request","message":"Validation failed","details":[zod issues]}}`. - Some errors carry extra fields beside `error` (`required`, `balance`, `shortfall`, `quote`, `payment_required`, `hint`); they stay at the top level. - Older clients that read a flat `error` string should read `error.message` (and `error.code` for codes such as `insufficient_credits`). - A `401` carries `WWW-Authenticate: Bearer resource_metadata="https://splice.film.fun/.well-known/oauth-protected-resource/api/agents"`. - Every response carries `Splice-Api-Version`. Versioning and deprecation policy: https://splice.film.fun/developers#versioning-and-deprecation | Status | Meaning | What to do | |---|---|---| | 400 | Validation failed, invalid JSON, an unknown `@slug` or `item:` ref | Fix the body; `details` names the field | | 401 | Missing or invalid key | Check `SPLICE_API_KEY` | | 402 | `insufficient_credits` | Top up, then retry with the same Idempotency-Key | | 403 | `forbidden`: the key lacks the scope | Use a key with the scope | | 404 | The production or item doesn't exist or isn't yours (this includes using the wrong production id for an item) | Poll the `production_id` the generate call returned | | 409 | An idempotency conflict, or `quote_exceeds_max_credits` | See §5 | | 415 | The upload wasn't multipart | Send `multipart/form-data` | | 501 | The feature is switched off (`cancel_unavailable`, `quote_unavailable`, …) | Check `features` in the manifest | | 502 / 503 | The upstream generation service failed or is unavailable | Retry with backoff and the same Idempotency-Key | **Limits.** - Requests are rate-limited per client IP: 100 reads and 30 writes a minute on `/api/agents/*` (120 a minute per credential on `/api/mcp`). Responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`; over the limit you get `429 {"error":{"code":"rate_limited",…}}` with `Retry-After`. Generation concurrency is also capped per account: keep a handful of generations in flight rather than hundreds. - List routes page with `?limit=` and `?offset=` and return `pagination: {limit, offset, returned, total?, has_more, next_offset}`. - Uploads and multipart transcription are capped at 4.5 MB. - Items list the newest 50. ## 15. CLI `splice` is a single-file, zero-dependency Node 18+ client for everything above. ```bash curl -fsSL https://splice.film.fun/cli/splice.mjs -o splice.mjs && chmod +x splice.mjs export SPLICE_API_KEY=sk_... # optional: SPLICE_API_BASE_URL (default https://splice.film.fun) ./splice.mjs session ./splice.mjs models video --human ./splice.mjs generate image --prompt "Paper boat in neon rain" --param aspectRatio=9:16 --wait ./splice.mjs generate video --prompt "…" --model seedance25 --param duration=5 --param 'referenceImages=["https://…"]' --wait ./splice.mjs templates --category "Camera moves" --human ./splice.mjs templates use --param 'firstFrameImage=https://…' --wait ./splice.mjs upload hero.png ./splice.mjs api POST productions//quote --body '{"shotlist_id":"…"}' ``` - **Output:** JSON on stdout when piped (or with `--json`); readable text on a terminal (or with `--human`). Errors go to stderr, as `{"error":{message, status, code, hint, body, exit_code}}` in JSON mode. - **Idempotency:** every `generate`, `edit` and `templates use` sends an Idempotency-Key and prints it. Rerun with `--idempotency-key ` to retry safely. - **Exit codes:** 0 ok, 1 network/unexpected, 2 usage, 3 auth (401/403), 4 insufficient credits, 5 not found, 6 request rejected (400/409/415), 7 server error, 8 generation failed, 9 `--wait` timed out. ### MCP server Splice runs a remote MCP server at `https://splice.film.fun/api/mcp`: Streamable HTTP, stateless (no session, no standalone SSE stream; GET answers 405). It speaks the 2026-07-28 protocol and falls back to 2025-era Streamable HTTP for older clients. - **Auth:** `Authorization: Bearer sk_…`, the same agent key. A missing or invalid key gets `401` with `WWW-Authenticate: Bearer error="invalid_token", resource_metadata="https://splice.film.fun/.well-known/oauth-protected-resource"`. - **Tools:** `list_models` (prices per model), `get_balance`, `list_productions`, `generate_image`, `generate_video`, `generate_voice`, `generate_music`, `edit_image`, `upscale_image`, `upscale_video`, `get_job`, `list_compositions`, `get_composition`, `render_composition`, `list_characters`. Each one calls the matching agent route in-process with your key, so scopes, production ownership, restricted models, credits and billing are exactly those of the HTTP API. - **Flow:** pick a production (`list_productions`), check prices (`list_models`), generate (returns `{job_id, production_id, status: "pending"}`), then poll `get_job(job_id, production_id)` until `completed` (read `media_url`) or `failed`. Pass `idempotency_key` on generate calls so a retry never charges twice. - **OAuth (claude.ai and Claude Desktop custom connectors):** add `https://splice.film.fun/api/mcp` as a custom connector with no key; the client discovers `/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server`, registers itself (`POST /oauth/register`, public clients only), and sends you to `/oauth/authorize` to sign in to Splice and approve. Authorization code + PKCE S256; access tokens last 1 hour and refresh tokens 30 days (rotated on every use). Connected apps are listed, and can be revoked, under Keys & integrations. - **Scopes:** read tools need `splice:read`, tools that spend credits need `splice:generate`. Keys made in the dashboard have both. A token without a tool's scope gets `403` with `error="insufficient_scope"`. ```bash claude mcp add --transport http splice https://splice.film.fun/api/mcp --header "Authorization: Bearer sk_..." npx @modelcontextprotocol/inspector --cli https://splice.film.fun/api/mcp --transport http --header "Authorization: Bearer sk_..." --method tools/list ``` Cursor (`~/.cursor/mcp.json`): ```json { "mcpServers": { "splice": { "url": "https://splice.film.fun/api/mcp", "headers": { "Authorization": "Bearer sk_..." } } } } ``` ## 16. Related products - **Studio** (https://studio.film.fun/llms.txt) publishes series and episodes. Its v1 API uses a separate key (`ff_sk_…`). **Publishing to Studio from Splice.** You don't need a Studio key in your agent to publish there. The person who owns the agent key connects Studio once in Splice (Keys & integrations, `/dashboard/api-keys#studio`: one click, or paste an `ff_sk_…` key); Splice stores that key encrypted and calls Studio for you. - `POST /api/agents/productions/{id}/publish-to-studio` (scope `studio:write`) takes `{episode: {item_id, title?, description?, episode_number?, thumbnail_url?, duration_seconds?}, beat?: {item_id, title?, description?, thumbnail_url?, duration_seconds?}, project?: {studio_project_id} | {create: {title?, description?, cover_image?}}, release?: false, publish_project?: false}`. - Items must be completed videos in this production. The episode is numbered automatically when you leave `episode_number` out. - A beat is a short clip (Studio shows it as a "Clip"). - Thumbnail and duration are filled for you. Generated videos carry neither, so Splice uses the generation's source image (first frame or reference), or for a merge the first input clip's image, then the Studio project's cover. The duration is read from the MP4 itself, else the requested duration. To set them yourself, pass `thumbnail_url` (https) or `duration_seconds` (whole seconds) on the episode or beat. - Without `project`, it goes to the production's linked Studio project, or a new draft project that then becomes the link. - It returns `{project: {id, url, manage_url, status, created}, episode: {id, url, status, episode_number}, beat?, share_kit?, warnings, link}`. Studio stores the video URL as given; it doesn't copy the file. - Re-publishing the same item updates the same Studio row. It fills a thumbnail or duration that row doesn't have yet and keeps ones it has, unless you pass an override. `Idempotency-Key` works as for generations, and nothing is charged. - Errors: - 409 `studio_not_connected`: the owner hasn't connected Studio. - 409 `studio_disabled`: Studio is turned off for this production. - 422 `studio_not_ready`, with `missing[]`, when `publish_project` is set but the project lacks a title, description or cover. - 409 `studio_key_invalid`: the stored Studio key was revoked; reconnect in Splice. - `GET|PUT /api/agents/productions/{id}/studio-link` reads or sets the production's Studio setting: `{enabled?, studio_project_id?: uuid | null, create?: {…}}`. PUT needs `studio:write`. - **ReelKit** (https://reelkit.fun) is an optional agent layer on top of this API: planning, approvals, spend caps, a content calendar and social posting. Everything in this file works without it. ## 17. Endpoint reference (all routes) Every agent route, generated from `/api/agents/manifest.json` (fetch it for the machine-readable form, with `features`, `scopes` and `error_codes`). `[id]` is the Splice production id; other bracketed segments are the named resource ids. A route listing several methods shows its write scope; reads need the matching `:read` scope. ### balance - `GET /api/agents/balance` (scope `balance:read`): Credit summary ### characters - `GET POST /api/agents/characters` (scope `entities:read | entities:write`; 201 on success): Client library — characters. POST creates; production-scoped route links. - `GET PUT /api/agents/characters/[characterId]` (scope `entities:read | entities:write`): Fetch / update one client-library character. PUT sets fields like sheet_url / avatar_url (e.g. attach a generated portrait or character sheet as the reference image @slug routes into shot-framing). ### compositions - `GET POST /api/agents/compositions` (scope `compositions:read | compositions:write`): List (GET) or create (POST) Motion (Hyperframes) compositions. Create accepts title/description/payload (serialized HTML); defaults the production binding to the key default. Pair with composition-render to render the payload to MP4. - `GET PATCH DELETE /api/agents/compositions/[compositionId]` (scope `compositions:read | compositions:write`): Fetch (GET, full payload), update (PATCH: title/description/payload/thumbnail/status) or soft-delete (DELETE) one composition. Owner-gated → 404 when unknown/unowned. - `POST /api/agents/compositions/[compositionId]/duplicate` (scope `compositions:write`; 201 on success): Clone a composition (title gets " (copy)"). - `GET /api/agents/compositions/[compositionId]/revisions` (scope `compositions:read`): Newest-first version history for one composition, without payloads (a payload can be 50KB+). Every payload change via PATCH or create appends a revision. Use with revert to run an edit/critique loop that can walk back. - `POST /api/agents/compositions/[compositionId]/revisions/revert` (scope `compositions:write`): Restore a composition to an earlier revision by revision_id. Appends a revert revision rather than rewinding, so the abandoned branch stays readable and the revert is itself revertible. Owner-gated → 404 when unknown/unowned, or when the revision belongs to another composition. ### credits - `GET /api/agents/credits/packs` (scope `balance:read`): Credit packs with their price now in each Solana token: { packs: [{ id, name, credits, price_usd, credits_per_usd, quotes: { USDC|SOL|AUDD: { amount, amount_atomic, decimals, mint } } }], tokens }. The purchase 402 is the binding quote. - `POST /api/agents/credits/purchase` (scope `credits:write`): Buy a credit pack with Solana (USDC, SOL or AUDD). Body { pack_id: starter|creator|pro|studio, payment_token? }. Without X-PAYMENT it returns 402 payment_required with transaction_id, receiver_address, amount, token_mint and solana_pay { url, reference, status_url }: pay the Solana Pay url from any wallet (or show it as a QR code) and poll status_url, or send the transfer yourself and retry with X-PAYMENT { transaction_id, signature, payer_address }. See features.credits. - `GET /api/agents/credits/purchase/[transactionId]` (scope `credits:write`): Status of a credit purchase. For Solana Pay it finds the payment on chain by the purchase reference, verifies token, receiver, amount and reference, and applies the credits. Returns { status: pending|completed, credits_applied?, signature?, pack }. Safe to poll. - `POST /api/agents/credits/transfer` (scope `credits:write`): Transfer credits ### edit - `POST /api/agents/edit/caption-burner` (scope `edit:media`; Idempotency-Key; 201 on success): Burn captions onto video - `POST /api/agents/edit/clip` (scope `edit:media`; Idempotency-Key; 201 on success): Clip a video to a time range (ms or seconds). Backed by filmFunClipMedia. - `POST /api/agents/edit/image` (scope `edit:media`; Idempotency-Key; 201 on success): Edit image (inpaint / outpaint / variation) - `POST /api/agents/edit/image-color-grade` (scope `edit:media`; Idempotency-Key; 201 on success): Color grade - `POST /api/agents/edit/image-draw` (scope `edit:media`; Idempotency-Key; 201 on success): Draw on image - `POST /api/agents/edit/image-multiple` (scope `edit:media`; Idempotency-Key; 201 on success): Batch image edits - `POST /api/agents/edit/image-upscale` (scope `edit:media`; Idempotency-Key; 201 on success): Upscale image - `POST /api/agents/edit/media-merge` (scope `edit:media`; Idempotency-Key; 201 on success): Concatenate videos (input_urls[], in order). parameters.outputAspectRatio (e.g. "16:9") or width/height set the output shape; every input is cropped to it. Omitted: the production's aspect ratio, else the first input video's own shape (the model on its own would default to 9:16). - `POST /api/agents/edit/video-extend` (scope `edit:media`; Idempotency-Key; 201 on success): Continue a video with new footage from a prompt (MiniMax H3 Max extend, minimaxH3MaxExtend). video_url (1.6–60 s, ≤50 MB) + prompt (what happens next). duration 5–15 s of new footage (default 5), resolution 480P/768P/1080P/2K (default 768P), aspect_ratio auto|21:9|16:9|4:3|1:1|3:4|9:16, output extended (source + new, default) | continuation (new only). Billed per second of new footage at the resolution rate, plus a reference-input charge for the length of the source video (an unmeasurable source is billed as 60 s). - `POST /api/agents/edit/video-reframe` (scope `edit:media`; Idempotency-Key; 201 on success): Reframe a video to 9:16 / 4:5 / 1:1 / 16:9. Defaults to the subject-following crop (filmFunVideoReframe): the crop follows the speaker, the audio is kept, and the item gets metadata.subject_track (track_url + summary) so captions can sit above the head. Pass model_id lumaReframeVideo to expand the frame with AI fill instead. - `POST /api/agents/edit/video-upscale` (scope `edit:media`; Idempotency-Key; 201 on success): Upscale a video to a higher resolution. Models: topazlabsVideoUpscaler (default; target_resolution 720p|1080p|4k, target_fps 15-60), bflFluxVideoUpscale (upscale_factor 1.5-3, creativity precise|creative) or crystalVideoUpscaler (scale_factor). Billed per second of video. ### generate - `POST /api/agents/generate/auto-sfx` (scope `generate:audio`; Idempotency-Key; 201 on success): Auto-SFX for video - `POST /api/agents/generate/character-sheet` (scope `generate:sheet`; Idempotency-Key; 201 on success): Character reference sheet - `POST /api/agents/generate/composition-render` (scope `generate:composition`; Idempotency-Key; 201 on success): Render a Hyperframes HTML composition to MP4 (HTML + assets -> MP4 via headless Chrome + FFmpeg). - `POST /api/agents/generate/image` (scope `generate:image`; Idempotency-Key; 201 on success): Text-to-image generation - `POST /api/agents/generate/lipsync` (scope `generate:audio`; Idempotency-Key; 201 on success): Lip sync audio to a face. Send video_url (face video) for most models, or image_url (face image) for image + audio models such as minimaxH3MaxLipSync (5–14.8 s of audio; billed per second of audio at the chosen resolution: 480P/768P/1080P/2K). - `POST /api/agents/generate/location-sheet` (scope `generate:sheet`; Idempotency-Key; 201 on success): Location reference sheet - `POST /api/agents/generate/music` (scope `generate:audio`; Idempotency-Key; 201 on success): Music generation - `POST /api/agents/generate/prop-sheet` (scope `generate:sheet`; Idempotency-Key; 201 on success): Prop reference sheet - `POST /api/agents/generate/set-sheet` (scope `generate:sheet`; Idempotency-Key; 201 on success): Set reference sheet - `POST /api/agents/generate/sfx` (scope `generate:audio`; Idempotency-Key; 201 on success): Sound effects - `POST /api/agents/generate/shot-framing` (scope `generate:shot-framing`; Idempotency-Key; 201 on success): Compose a shot from entities + prompt (@slug + item:) - `POST /api/agents/generate/timeline-export` (scope `generate:other`; Idempotency-Key; 201 on success): Render timeline to video - `POST /api/agents/generate/video` (scope `generate:video`; Idempotency-Key; 201 on success): Text/image-to-video generation - `POST /api/agents/generate/video-animate` (scope `generate:video`; Idempotency-Key; 201 on success): Animate still image to video - `POST /api/agents/generate/video-image-audio` (scope `generate:video`; Idempotency-Key; 201 on success): Compose video from image + audio - `POST /api/agents/generate/vision-language` (scope `generate:other`; Idempotency-Key; 201 on success): Vision + language reasoning - `POST /api/agents/generate/voice` (scope `generate:audio`; Idempotency-Key; 201 on success): Text-to-speech - `POST /api/agents/generate/voice-changer` (scope `generate:audio`; Idempotency-Key; 201 on success): Voice conversion ### idempotency - `GET /api/agents/idempotency/[key]`: Look up a generate/edit request by its Idempotency-Key (the caller's own keys only; another account's key is a 404). Returns {status: pending|completed|failed, stale?, retryable?, outcome?, item_id, job_id, production_id, response_status, response}. 404 when unknown or expired. Any valid key; no extra scope. ### locations - `GET POST /api/agents/locations` (scope `entities:read | entities:write`; 201 on success): Client library — locations. POST creates; production-scoped route links. ### manifest.json - `GET /api/agents/manifest.json`: This manifest ### productions - `GET POST /api/agents/productions` (scope `productions:read`): List / create productions. GET supports optional ?series_id= filter — when present, returns only productions in that series ordered by episode_number ASC. Each row carries series_id + episode_number for client-side grouping. - `GET PATCH DELETE /api/agents/productions/[id]` (scope `productions:read`): Single production — [id] = creation_id. PATCH accepts: title, description, status, video_url, cover_image, thumbnail, duration_seconds, aspect_ratio, content_rating, genre, tags, metadata, series_id (text), episode_number (int); other keys are ignored (ff_production_id is server-owned). The last two are episodic series ordering — see /api/agents/series for discovery. ### productions/[id]/characters - `GET POST /api/agents/productions/[id]/characters` (scope `entities:read`): Production-scoped characters (import + list) ### productions/[id]/clips - `GET POST /api/agents/productions/[id]/clips` (scope `productions:write`): List clips or create a draft. POST validates against ClipWriteSchema. - `GET PATCH DELETE /api/agents/productions/[id]/clips/[clipId]` (scope `productions:write`): Read / partial-update / soft-delete a clip draft. - `POST /api/agents/productions/[id]/clips/[clipId]/publish-transcription` (scope `productions:write`): Apply segments + corrections to source transcription, retime cues, write a new transcription. Idempotent. - `POST /api/agents/productions/[id]/clips/[clipId]/publish-video` (scope `productions:write`): Render a new video/audio file via the cog-clip-export model. Synchronous; pins output_item_id on the clip. ### productions/[id]/compositions - `GET /api/agents/productions/[id]/compositions` (scope `compositions:read`): The key owner's Motion compositions bound to this production or unbound (owner/editor access), newest first, without payloads. ?limit (≤200), ?offset. - `GET PATCH /api/agents/productions/[id]/compositions/[compositionId]` (scope `compositions:read | compositions:write`): GET: { composition, version, summary: { stage, layers[] }, html } (?include_html=false drops html; ETag = version). PATCH: { ops: [{ op, ...args }] } — the agent chat's Motion tools (update_clip, add_clip, remove_clip, reorder_clips, set_stage, set_clip_animations, replace_html, …; aliases update_layer/add_layer/remove_layer/reorder_layers/update_stage/set_html) applied atomically through the SDK parse → edit → serialize pipeline — or { payload: html } (validated, needs version). version / If-Match → 409 version_conflict when stale; 422 op_failed / invalid_payload; dry_run previews. Free (no credits). See features.composition_edit. ### productions/[id]/draft-pipeline - `POST /api/agents/productions/[id]/draft-pipeline` (scope `productions:write`): One pitch drafts the PLAN half of the pipeline: generates a 60-second script, creates it, then chains the stage bridges (script→storyboard beats→shotlist shots). Body: {pitch, title?}. Returns {script, storyboard, shotlist, counts}. ~1 minute of sequential LLM work — use a generous timeout. The one-call opening move for autonomous productions. ### productions/[id]/entity-slugs - `GET POST DELETE /api/agents/productions/[id]/entity-slugs` (scope `entities:write`): @slug CRUD for production entities ### productions/[id]/items - `GET /api/agents/productions/[id]/items` (scope `productions:read`): List production items - `GET /api/agents/productions/[id]/items/[itemId]` (scope `productions:read`): Get one item by id, fetched directly (any item in the production, not just the newest 50): id, status, media_url, job_id, job_status, cancelled?, error_message?, answer? (vision-language text output). A pending item whose job failed, timed out or was cancelled reports status "failed". - `POST /api/agents/productions/[id]/items/[itemId]/cancel` (scope `productions:write`): Cancel a pending/processing generation. Forge marks the job canceled and refunds it; returns {cancelled, refunded, refund_amount, job_status}. 409 not_cancellable when the item is not a generation or already finished. 501 cancel_unavailable while features.cancel.enabled is false. ### productions/[id]/locations - `GET POST /api/agents/productions/[id]/locations` (scope `entities:read`): Production-scoped locations ### productions/[id]/media - `POST /api/agents/productions/[id]/media` (scope `productions:write`): Upload media: multipart/form-data with the file in "file" (max 4.5 MB); optional name, type, tags (JSON array), metadata (JSON object). Returns {media_item: {id, media_url, …}}. Other content types get 415 — pass hosted media to a generator by URL instead. ### productions/[id]/moodboards - `GET POST /api/agents/productions/[id]/moodboards` (scope `productions:write`): List or create moodboards (visual reference boards). POST body: {title, description?, canvas?} — `canvas` is the structured board content (positioned cards, items, text). GET → productions:read; POST → productions:write. - `GET PATCH DELETE /api/agents/productions/[id]/moodboards/[moodboardId]` (scope `productions:write`): Read / update / delete a moodboard. PATCH accepts partial body {title?, description?, canvas?}. ### productions/[id]/props - `GET POST /api/agents/productions/[id]/props` (scope `entities:read`): Production-scoped props ### productions/[id]/publish-to-studio - `POST /api/agents/productions/[id]/publish-to-studio` (scope `studio:write`; Idempotency-Key): Publish this production's generated video to film.fun Studio (studio.film.fun, the episodic platform) with the Studio key the agent key's owner connected in Splice (Keys & integrations). Body {project?: {studio_project_id} | {create: {title?, description?, cover_image?}} (default: the production's linked Studio project, else a new draft project), episode: {item_id, title?, description?, episode_number?, thumbnail_url?, duration_seconds?} (auto next number), beat?: {item_id, title?, description?, thumbnail_url?, duration_seconds?} (a short clip), release?: bool (default false = draft), publish_project?: bool}. Items must be completed videos. thumbnail_url (https) and duration_seconds (whole seconds >= 1) are optional overrides; otherwise the thumbnail is the item's metadata, else the generation's source image, else a merge's first input clip, else the Studio project cover, and the duration is the metadata, else the MP4's real length, else the requested duration. Idempotent: rows are matched by production + item, so a re-publish updates them and fills a thumbnail or duration the Studio row lacks (an override replaces it). Returns {project:{id,url,manage_url,status,created}, episode:{id,url,status,episode_number}, beat?, share_kit?, warnings, link}. 409 studio_not_connected / studio_disabled; 422 studio_not_ready with missing[] when the project can't be published; 409 studio_key_invalid when the stored Studio key was revoked (reconnect in Splice). ### productions/[id]/quote - `POST /api/agents/productions/[id]/quote` (scope `productions:read`): Itemised quote before a multi-step job; spends nothing. Body { shotlist_id, shot_ids?, model_id?, resolution?, duration_seconds?, regenerate?, reuse? } prices what /shot-sheet/seedance/all would run (dialogue voice lines in each speaker's voice, the video, lip-sync where the model takes no reference audio); or { steps: [{ key, generator, tool_id, params }] } with "$ref:" for another step's output. Returns { quote: { lines[{ key, status generate|reused|in_progress|not_needed, full_credits, credits, input_hash, reuse_item_id }], shots[], total_credits, saved_credits, balance, sufficient } }. Unchanged steps reuse their takes (0 credits). Priced by Forge with the charge's own pricing. See features.quote. ### productions/[id]/readiness - `GET /api/agents/productions/[id]/readiness` (scope `productions:read`): Decision-helper that wraps /stages with a suggested_next_steps array ordered by the canonical pipeline (universe → moodboard → script → storyboard → shotlist → shoot → edit). Each suggestion carries a recommended_action mapped to a SpliceMediaClient method so agents can dispatch directly. ### productions/[id]/reference-analysis - `POST /api/agents/productions/[id]/reference-analysis` (scope `generate:other`; Idempotency-Key): Read a reference video for its STRUCTURE and get back a timed shot plan — pacing, shot lengths, beats and framing, never the reference's footage, audio or artwork. Body { media_item_id | video_url, depth? quick|standard|deep, language?, time_range?, brief?, aspect_ratio?, rights: { attested, statement_version }, recast?: [{ subject_role, entity_id, entity_name, entity_slug?, kind }], create_shotlist?, shotlist_title?, dry_run?, max_credits?, reuse? }. `rights` is required and recorded before anything runs (400 reference_rights_required without it); a real person's face or voice still needs that character's own consent record. Three paid steps run in order — transcribe, analyse (flat 3 credits), plan — each quoted with the charge's own pricing right before it runs and reused at 0 credits when an identical read exists. Returns { analysis_id, status, summary, analysis, plan, casting_call, uncast, shotlist_id, credits_spent, steps, stopped }. See features.remix. ### productions/[id]/scripts - `GET POST /api/agents/productions/[id]/scripts` (scope `productions:write`; 201 on success): List or create persisted scripts for a production. GET forwards limit/offset/status/sort_direction. POST body: {title, format?} — format one of fountain/plaintext/markdown/fdx; returns 201. Used by agents that decompose narratives into shot sheets or shotlists. GET → productions:read; POST → productions:write. - `GET /api/agents/productions/[id]/scripts/[scriptId]` (scope `productions:read`): Read a single script — full body (logline, synopsis, scenes, dialogue) + metadata. ### productions/[id]/share-links - `GET POST /api/agents/productions/[id]/share-links` (scope `share:write`): List / create share links (frame.io-style review or present). POST returns public_url. - `GET PATCH DELETE /api/agents/productions/[id]/share-links/[linkId]` (scope `share:read`): Read share link with comments + decisions (split-approval polling target). Update or delete. ### productions/[id]/shot-sheet - `GET PATCH /api/agents/productions/[id]/shot-sheet` (scope `productions:write`): Shot Sheet aggregator. GET ?shotlist_id=X returns shotlist + universe-bundle for rendering. PATCH ?shotlist_id=X with body {shotsheet:{...}} partial-merges fields into shotlist.metadata.shotsheet (color palette, mood keywords, audio/cinematography notes). Sister to /shot-framing. - `GET /api/agents/productions/[id]/shot-sheet/cost-preview` (scope `productions:read`): Count-only cost preview for the bulk shot-sheet generators. GET ?shotlist_id=X returns shot_count + bulk_stills_targets + bulk_seedance_targets + i2v/t2v split — no pricing, just the *targets* that bulk calls would fire. Combine with /api/agents/tools/generator-tools/{id}/models to estimate spend before pulling the trigger. - `GET /api/agents/productions/[id]/shot-sheet/from-script` (scope `productions:read`): One-shot composer for "decompose this script" agent loops. GET ?script_id=X returns the full script + universe-bundle + a structured prompt template + scaffolding hints (suggested titles, available entity ids). The agent does the LLM decomposition (it IS an LLM); then calls create_shotlist with the result. - `POST /api/agents/productions/[id]/shot-sheet/seedance` (scope `generate:video`): Send one shot in a Shot Sheet to Seedance 2 video generation. POST ?shotlist_id=X&shot_id=Y composes the video body from scene identity refs (character sheets), environment (location), motion priors (entity gallery videos), audio (voice samples + reference tracks). Defaults to seedance2Fast: with voice samples the take is voiced (the still becomes a reference image); without them the still is the i2v first frame; with no still, text-to-video. first_frame_lock keeps the still as the first frame. The response reports input_mode. Closes the ShotFramer → ShotSheet → VideoGenerator loop. - `POST /api/agents/productions/[id]/shot-sheet/seedance/all` (scope `generate:video`): Bulk: generate video for every shot in a Shot Sheet (or shot_ids), one shot at a time. POST ?shotlist_id=X. A shot's dialogue [{ character_id | speaker, line }] is said in each speaking character's voice: rendered in that voice, then passed as reference audio (seedance2Fast, seedance25, minimaxH3) or lip-synced (other models); durations follow the lines' syllable estimate. Unchanged steps reuse their takes (0 credits); regenerate: [shot ids | step keys] forces new ones. dry_run: true returns the itemised quote only; max_credits refuses a higher quote (409 quote_exceeds_max_credits). Returns shots[{ shot_id, production_item_id, prompt, input_mode, voice_path, first_frame_image, reused, status, error? }], successes, failures, reused, pending (call again to continue), dispatched_credits, stopped, quote. - `POST /api/agents/productions/[id]/shot-sheet/stills` (scope `generate:shot-framing`): Generate the still frame for one shot in a Shot Sheet. POST ?shotlist_id=X&shot_id=Y composes a shot-framing body from the scene context (characters/location refs + shot description as prompt), fires the generator, and stamps asset_item_id onto the shot. Returns production_item_id for polling. - `POST /api/agents/productions/[id]/shot-sheet/stills/all` (scope `generate:shot-framing`): Bulk: generate stills for every shot in a Shot Sheet in parallel. POST ?shotlist_id=X. Per-shot failures isolate; successes are persisted in a single shotlist PUT. Returns array of { shot_id, production_item_id, error? }. ### productions/[id]/shotboards - `GET POST /api/agents/productions/[id]/shotboards` (scope `productions:read`): Shotboards (shown as "Assembly" in the UI) - `GET PATCH DELETE /api/agents/productions/[id]/shotboards/[shotboardId]` (scope `productions:write`): Read / update / delete a shotboard. ### productions/[id]/shotlists - `GET POST /api/agents/productions/[id]/shotlists` (scope `productions:write`): List or create shot lists for a production. GET filters: limit/offset/status/script_id/storyboard_id/sort_direction. POST body: {title, description?, script_id?, storyboard_id?, source_type_default?, scenes?, status?, metadata?} — Forge normalises ids/order/source_type/status server-side. - `GET PUT /api/agents/productions/[id]/shotlists/[shotlistId]` (scope `productions:write`): Read or replace fields on a shot list. PUT accepts partial body — only fields sent are updated. Updating scenes is the path agents use after decomposing a script into shot rows. ### productions/[id]/stages - `GET /api/agents/productions/[id]/stages` (scope `productions:read`): Aggregated production stage state — counts across moodboards/scripts/storyboards/shotlists/takes/studio + universe bible flag. Designed for polling. ### productions/[id]/storyboards - `GET POST /api/agents/productions/[id]/storyboards` (scope `productions:write`): List or create storyboards for a production. Storyboards are the visual planning layer between script and shotlist (each storyboard = sequence of beats, each beat = ordered reference frames). GET filters: limit/offset/status/script_id/sort_direction. POST body: {title, description?, script_id?, aspect_ratio?, beats?, status?, metadata?} — Forge normalises beat ids/order server-side. - `GET PUT /api/agents/productions/[id]/storyboards/[storyboardId]` (scope `productions:write`): Read or replace fields on a storyboard. PUT accepts partial body — only fields sent are updated. ### productions/[id]/studio - `GET POST /api/agents/productions/[id]/studio/projects` (scope `studio:read`): Studio projects - `GET PATCH DELETE /api/agents/productions/[id]/studio/projects/[projectId]` (scope `studio:write`): Read / update / delete a Studio project linked to this production. - `POST GET /api/agents/productions/[id]/studio/projects/[projectId]/export` (scope `studio:write`): Export / render status ### productions/[id]/studio-handoff - `GET /api/agents/productions/[id]/studio-handoff` (scope `studio:read`): Browser handoff URL ### productions/[id]/studio-link - `GET PUT /api/agents/productions/[id]/studio-link` (scope `studio:write`): The production's film.fun Studio setting: {link: {enabled, studio_project_id, studio_project_url, studio_project_title, linked_at, linked_by}}. GET needs studio:read. PUT (studio:write) {enabled?: bool, studio_project_id?: uuid | null (null unlinks), create?: {title?, description?, cover_image?}} links one of the owner's Studio projects (read with their connected Studio key), creates one, unlinks, or turns Studio off for the production. ### productions/[id]/takes - `GET POST /api/agents/productions/[id]/takes` (scope `productions:write`): List or create takes for the shoot stage. One shotlist shot can have many takes (live-action / composition / stock captures). GET filters: limit/offset/status/shotlist_id/shot_id/shoot_label/sort_direction. POST body requires {shotlist_id, shot_id} + optional {shoot_label, take_number, status, asset_url, asset_item_id, thumbnail_url, duration_s, captured_at, notes, metadata}. - `GET PUT DELETE /api/agents/productions/[id]/takes/[takeId]` (scope `productions:write`): Read, update, or archive a single take. Status flow: planned → rolling → captured → circle | no_good → archived. ### productions/[id]/tasks - `GET POST /api/agents/productions/[id]/tasks` (scope `productions:read`): Tasks - `GET PATCH DELETE /api/agents/productions/[id]/tasks/[taskId]` (scope `productions:write`): Read / update / delete a single task. - `POST /api/agents/productions/[id]/tasks/[taskId]/notes` (scope `productions:write`): Append to a task notes thread (use parent task GET for the thread itself). ### productions/[id]/transcriptions - `GET POST /api/agents/productions/[id]/transcriptions` (scope `productions:write`): List or create/upsert a transcription. Filter list by parent_item_id and language. - `GET PATCH DELETE /api/agents/productions/[id]/transcriptions/[transcriptionId]` (scope `productions:write`): Read / partial-update / soft-delete a transcription. ### productions/[id]/universe - `GET POST /api/agents/productions/[id]/universe` (scope `productions:write`): Universe Bible — lore / themes / era / factions / style guide that wraps the production's entities ### productions/[id]/universe-bundle - `GET /api/agents/productions/[id]/universe-bundle` (scope `entities:read`): One-shot read: Universe Bible + all production characters/locations/sets/props ### productions/[id]/variants - `POST /api/agents/productions/[id]/variants` (scope `generate:video`; Idempotency-Key): Make variants of one shotlist that each change exactly ONE thing. Body { shotlist_id, axis: hook_line|cast|language|aspect|music, variants: [{ key, label?, value }], include_base?, model_id?, resolution?, aspect_ratio?, duration_seconds?, regenerate?, reuse?, dry_run?, max_credits? }. Every arm is planned by the same planner a batch uses, so a shot the axis didn't touch keeps its input hash and reuses the existing take at 0 credits — only what changed is paid for. The quote reports, per arm, how many shots are generated and how many are reused. dry_run returns the quote only; max_credits refuses a higher one (409 quote_exceeds_max_credits). Shots run one at a time against the account-wide limit; leftovers come back as pending, and calling again reuses what finished. Returns { batch_id, axis, variants[{ key, label, shots, credits, dispatched, reused, pending }], quote, dispatched_credits, pending, stopped }. See features.variants. ### props - `GET POST /api/agents/props` (scope `entities:read | entities:write`; 201 on success): Client library — props. POST creates; production-scoped route links. ### series - `GET /api/agents/series` (scope `productions:read`): Discovery: distinct series_id values across the agent's editable productions, with episode_count + max_episode_number + latest_created_at per series. Sorted by latest_created_at descending. No standalone series table in Splice — this is a GROUP BY over splice_productions; richer series metadata (title, cover, etc.) lives in Studio. ### session - `GET /api/agents/session` (scope `session:read`): Default session + creation/production id ### shotlists - `POST /api/agents/shotlists/[shotlistId]/promote` (scope `productions:write`; 201 on success): Promote a shotlist to a fresh shotboard. Snapshots scenes[].shots[] into the shotboard's flat shots[] (preserving order) and wires parent_shotlist_id so lineage breadcrumbs render. Body: {production_id, name?, description?, aspect_ratio?}. ### skill - `POST /api/agents/skill/brand-dna/extract` (scope `tools:read`; Idempotency-Key; 201 on success): Extract brand DNA (palette / typography / voice / values) from a URL or pasted text. ### tools - `GET /api/agents/tools/generator-tools` (scope `tools:read`): List every generator tool with its enabled models embedded (?include=models, default). Mapping endpoint that ReelKit and other agent-side registries cache to discover what models are enabled and which is the default per generator. - `GET /api/agents/tools/generator-tools/[generatorToolId]/models` (scope `tools:read`): List models + capabilities for a generator - `GET /api/agents/tools/generator-tools/image/models` (scope `tools:read`): List image-generation models + capabilities (alias for generator-tools/[id]/models with id=image) ### transcribe - `POST /api/agents/transcribe` (scope `transcribe:write`; Idempotency-Key; 201 on success): Audio/video -> text (JSON audio_url OR multipart file <=4.5MB) ### video-templates - `GET POST /api/agents/video-templates` (scope `productions:read`): List the agent client's video templates (filter by ?kind=script|storyboard|shotlist), or create one — extract from a row via {kind, name, source_id} OR raw seed via {kind, name, payload}. Source enrichment auto-snapshots the production's universe_bible into source.universe_bible when present. - `POST /api/agents/video-templates/[templateId]/apply` (scope `productions:write`; 201 on success): Apply a video template to a target production, creating a new scripts/storyboards/shotlists row seeded from the template's payload. Returns {applied: {kind, id}}. --- Maintainers: this file is `public/llms.txt` / `public/llms-full.txt` in the Splice repo. When you add or change an `/api/agents/*` route, update `app/api/agents/manifest.json/route.ts` (CI enforces this), regenerate section 17 (`UPDATE_LLMS_REFERENCE=1 pnpm vitest run __tests__/agents/llms-reference.test.ts`), the prose above where it helps, and the CLI (`public/cli/splice.mjs`) if the change affects it.