A discount on premium plans when you sign upGet your discount
eroq

blog/developers·Sep 24, 2026·6 min·by the eroq team

Cinema projects through the films API — script, storyboard, render, media, export

The scripted side of Cinema. What a v2 film project looks like on /v1/films, how the render route reads the script, how media is re-signed, how a cut is saved with the export route and published.


Everything the Cinema workspace does goes through /v1/films, and the studio is a client of it like any other. This guide is for the developer who wants to build or automate films: the project format the routes keep opaque, the render call that reads the script, the media map, the export route, and the caps. It assumes you have read the storyboard-rendering guide for the v1 basics; this one is what v2 adds.

The project

A film row has a title, a cover_url, a published_creation_id and a data document. data is the whole project — the routes validate its size (400 KB) and normalize it on read, and otherwise store what you send. The v2 shape:

{
  "v": 2,
  "look": {
    "model": "seedance-2-0-mini",
    "aspect": "16:9",
    "resolution": "720p",
    "audio": true,
    "seed": 4242,
    "filmType": "noir", "era": "1960s", "tempo": "tense",
    "cameraType": "35mm film", "lens": "anamorphic", "aperture": "f/1.4",
    "palette": "neon noir", "lighting": "practicals",
    "negative": ""
  },
  "notes": "Rooftop night. Rain machine on.",
  "sequences": [
    { "id": "q1", "heading": "EXT. JAZZ CLUB — NIGHT", "cast": ["char:9f3…"], "look": { "lighting": "practicals" } }
  ],
  "scenes": [
    {
      "id": "s1", "sequence": "q1", "heading": "",
      "prompt": "A detective in a rain-soaked trench coat waits under a flickering neon sign, slow push-in.",
      "shot": "push-in", "seconds": 5, "cast": ["char:9f3…"], "takes": 1,
      "dialogue": [{ "id": "l1", "speaker": "char:9f3…", "manner": "whispering", "line": "It's still lit." }],
      "options": [{ "libraryId": "cr_…", "contentType": "video/mp4", "seconds": 5 }],
      "pick": 0
    }
  ],
  "script": [
    { "id": "b1", "type": "scene", "sceneId": "s1" }
  ],
  "edit": { "auto": true, "clips": [], "audio": [], "titles": [], "fadeOut": 0 }
}

Since 13 October the studio shows this as the Story: each entry of scenes is a shot, and sequences are the Story's scenes (one place and time in heading, a cast and an optional look that overrides the film's light, palette, lens, aperture, camera or tempo), each shot pointing at its own with sequence. A shot's lines of dialogue live on the shot (dialogue: speaker as char:<id> or a free name, manner, line); notes on the film, a scene or a shot never reach an engine, and a shot's link (continue or angle) says how it follows the one before. Four rules the normalizer enforces so you do not have to: script keeps one scene block per shot and sets their order (old Notion-style text blocks are folded into notes), a shot's legacy heading moves to its scene, an old look.chain becomes links and is cleared, and every libraryId in options is a creation id — media is never stored in the project, only pointed at. A v1 draft (look + scenes with prompt/shot/seconds/cast/takes) normalizes into this shape on read, so old integrations keep working.

Caps: 24 shots, 30 seconds per shot, 8 options per shot, 30 lines of dialogue per shot, 80 clips, 40 audio clips, 120 titles, 4,000 characters per prompt or note.

Create, read, update

POST /v1/films
{ "title": "Rooftop night", "data": { … } }

GET /v1/films              → cards: scenes, rendered, seconds, aspect, cover, preview
GET /v1/films/{id}         → the row + data + media
PATCH /v1/films/{id}       → title and/or data (whole document)
DELETE /v1/films/{id}      → the draft; renders stay in the library

GET /v1/films/{id} returns a media map next to the project: every take, clip and sound the project points at, keyed creation:<id> or upload:<id>, with a fresh signed URL, content type, length and thumbnail. Your storage is private, so URLs expire; the map is how a client gets working links without owning a key per file. To refresh a subset — before a signed URL lapses, or when a new file joins the bin — POST /v1/films/media with { "keys": ["creation:…", "upload:…"] } (1 to 300) returns the same shape for those keys. Unknown or foreign keys are simply absent.

PATCH replaces data whole. Read, modify, write; there is no partial merge, and a stale write wins over a newer one, so serialize your writers.

Render

POST /v1/films/{id}/render
{ "store": false }

One call queues every scene that has a prompt, as one async video job per take, on the look's engine — the same pipeline as POST /v1/videos/generations, each charged individually. The response maps each scene id to its job ids, or to the error that kept it from queueing; scenes fail independently, and a charged failure refunds itself. Poll the jobs like any render.

What the route composes per shot, in v2: its scene's slugline and cast, its action and its dialogue lines (Mina (whispering) says: "…"), with the speakers' character sheets and voices joining the references — the same prompt the studio sends. The look rides every scene, including audio and seed. The engine's caps clamp format, resolution, length and references before billing; a scene that asks for a feature the engine lacks is refused, not silently reduced.

When a take lands, write it into the scene's options with its libraryId and set pick; the studio does this for you, an integration does it after polling. Links between shots (link) are a client behaviour: for Continue the shot the studio grabs the previous take's last frame, uploads it and passes it as the start frame; for Same scene, new angle it passes it as a reference. On the API, send references[0] yourself on a per-shot POST /v1/videos/generations if you want the chain; the bulk render route renders the shots as written.

The edit and the export

The edit document is pure data — clips referencing { "kind": "scene", "sceneId" }, creation or upload, with in/out trims, transitions, volume and fit; audio clips on up to eight tracks with fades; titles in twelve styles; a fade-out. The studio renders it to an MP4 in the browser; the API does not render edits server-side.

What the API does is keep the result:

POST /v1/films/{id}/export?seconds=42.5&signature=<edit signature>
Content-Type: video/mp4

<raw MP4 bytes>

The body is the file, streamed. It lands in the library as a video creation — so it plays, downloads and publishes like any clip — and counts against the workspace's storage like every library file. Free. The signature is a hash of the edit at export time; the film records it, and a later change to the edit makes the saved cut stale. A re-export replaces the previous cut when that one was never published and nothing in the edit uses it.

Rate-limited with uploads; a full bucket refuses before anything is read.

Publish

POST /v1/films/{id}/cover        (multipart file, image ≤ 5 MB, free)
POST /v1/films/{id}/publish
{ "title": "Rooftop night", "segments": ["<the saved cut's URL>"], "durationSeconds": 42 }

A film with a saved cut publishes the cut as its single segment; a film without one publishes its picked takes in order. Every segment must be a video in your library. Publication goes through review (review starts at pending on GET /v1/films/{id}); DELETE /v1/films/{id}/publish takes it down and keeps the engagement.

What the API does not do

  • Render the edit. The compositor is in the studio; the API stores the result.
  • Grab last frames. Chain by sending references[0] per scene.
  • Serve raw storage URLs. Everything is signed, through GET /v1/films/{id} or POST /v1/films/media.

FAQ

Is the project format documented?

The routes treat data as opaque JSON under a size cap and normalize it on read. The shape above is what the studio writes; the fields are stable, additions are optional.

Does the render route charge for scenes that already have takes?

It renders every scene with a prompt, takes included. To re-render one scene, call POST /v1/videos/generations for that scene and write the result into its options.

Can I upload my own footage into a film?

Upload it with POST /v1/uploads and reference it from the edit as { "kind": "upload", "id": … }. The media map signs it like any take.

Why is store an option on render?

store: true persists every clip to your storage at the Store's rate (2 credits per 10 MB); the default keeps the free library copy. Cinema's takes live in the library either way.

Read the films reference for every field, and the storyboard guide for the v1 walk-through.

Tagsfilms-apicinemaapivideostoryboard

Make it in eroq

Build it on the API

An API key takes a minute. The same engines, the same prices, async jobs and webhooks.

40 free credits · no card · private mode on every account