# FFmpeg Compose (assembly) > Compose builds one video from a timeline you write. You give it tracks, each track holds keyframes, and each keyframe places one piece of media at a start time for a length. It joins and layers; it does not generate. Status: roasOS API v1.1. Catalog `model-catalog@2026-10-06.v5`, pricing `customer-pricing@2026-11-05.v15`. The endpoint `POST /v1/assembly/compose` is live. `POST /v1/generations` has no `assembly` type, so this endpoint is the only path to this model. ## Endpoint POST https://api.roasos.com/v1/assembly/compose Auth: Authorization: Bearer roasos_sk_… (scope api:write). Idempotency-Key required. quote_only / quote_id / webhook_url as in https://docs.roasos.com/llms.txt. ## Good at Cut a finished ad from parts you already own: a video track for the shots, an audio track for the voice-over, an image track for a logo or an end card. Every time is in milliseconds. Keyframes inside one track must not overlap, because a track plays one source at a time; tracks overlap each other freely, which is what makes a timeline. A timeline carries at most 8 tracks, 64 keyframes per track, 200 keyframes in total, and 12 distinct media URLs. The keyframe durations and the timeline end must each stay within 180,000 ms — three minutes. A longer timeline is refused with `assembly_timeline_too_long`. The whole body must be 64 KiB or smaller. The media of this operation travels inside the timeline, in `tracks[].keyframes[].url`, and not in a top-level media field. EVERY one of those URLs is checked, not the first one. Each must be an immutable object on `https://cdn.roasos.com` that your workspace owns, in the role its track type implies: a `video` track takes video, an `audio` track takes audio, an `image` track takes an image. Any other URL is refused, and the refusal names the track id and the keyframe index. The price is one credit, flat, whatever the timeline holds. ## Pricing Unit: operation. Pricing key: `compose`. | request | credits | |---|---| | `per_operation` | 1 | Credits come from the roasOS pricing table (`MODEL_COSTS`) through the catalog. No other field changes the price. ## Modes - `compose` Provider endpoints: `fal-ai/ffmpeg-api/compose`. ## Request fields | field | type | required | default | rule | |---|---|---|---|---| | `quote_id` | string | no | — | UUID | | `quote_only` | boolean | no | `false` | — | | `tracks` | object[] | yes | — | 1–8 items; each `{ id, type, keyframes }` | | `wait_timeout_seconds` | integer | no | `0` | 0–60 | | `webhook_url` | string | no | — | https URL; ≤ 4,096 characters | The schema is strict: a field that is not in this table returns `400 unsupported_option`. Field names are the roasOS role names, one name per role across every family. ## Media The media of this endpoint travels inside the timeline, at `tracks[].keyframes[].url`, and not in a top-level media field. Every URL there is checked, in the role its track type implies: a `video` track takes video, an `audio` track takes audio, an `image` track takes an image. - Refused: `avatar_id`, `elements`, `product_id`, `product_images`, `references`, `reference_images`, `image_url`, `end_image_url`, `scene_id`, `scene_image`, `video_urls`, `video_url`, `audio_urls`, `audio_url`, `voice_id`. A refused role returns `400 unsupported_option`. - Every URL must be an immutable object on `https://cdn.roasos.com/…` that your workspace owns; any other URL is `400 reference_media_not_workspace_owned`. Upload with `POST /v1/uploads` (direct POST of the bytes to `put_url`, or a `source_url` import), then use the returned CDN URL. ## Prompt None. This operation takes no prompt. Write the timeline, not the intent. Give every track a short stable `id` so an error can name it. Start every clip at an exact millisecond; rounding a float here is the difference between a cut and a frame of black. Send `quote_only: true` first. The quote runs the whole schema and every media check, so you learn about a refusal before you spend an idempotency key. ## Not exposed The catalog records no pinned or hidden provider field for this model. ## Result Result kind `video`. Always present: `url`. ```json { "data": { "id": "gen_…", "kind": "video", "model": "compose", "status": "completed", "result": {"url":"https://cdn.roasos.com/workspaces/ws_01/result"} } } ``` Files live on `https://cdn.roasos.com` and are immutable; a result URL can be a media input to the next call. Poll `GET /v1/jobs/{id}/status` → `{ id, status, updated_at }`, then `GET /v1/jobs/{id}/result` (`409 run_not_completed` until the job is `completed`, `failed` or `canceled`). Statuses: `queued → processing → completed | failed | canceled`. ## Example ```bash # 1. quote — nothing is spent curl -X POST https://api.roasos.com/v1/assembly/compose \ -H "Authorization: Bearer roasos_sk_live_…" \ -H "Idempotency-Key: 0f1c9e2a-compose-0001" \ -H "Content-Type: application/json" \ -d '{"quote_only":true,"tracks":[{"id":"example","type":"video","keyframes":[{"timestamp":0,"duration":1,"url":"https://cdn.roasos.com/workspaces/ws_01/uploads/video-1.mp4"}]}]}' # → 200 { "data": { "kind": "quote", "quote_id": "q_7Hk…", "credits": 1, "expires_at": "…", "confirmation": { "model": "compose", "example": "per_operation" } } } # 2. run — the same body and the same Idempotency-Key, quote_id instead of quote_only curl -X POST https://api.roasos.com/v1/assembly/compose \ -H "Authorization: Bearer roasos_sk_live_…" \ -H "Idempotency-Key: 0f1c9e2a-compose-0001" \ -H "Content-Type: application/json" \ -d '{"quote_id":"q_7Hk…","tracks":[{"id":"example","type":"video","keyframes":[{"timestamp":0,"duration":1,"url":"https://cdn.roasos.com/workspaces/ws_01/uploads/video-1.mp4"}]}]}' # → 201 { "data": { "id": "gen_…", "kind": "video", "model": "compose", "status": "queued", "credits_reserved": 1, "links": { "status": "/v1/jobs/gen_…/status", "result": "/v1/jobs/gen_…/result" } } } # 3. poll curl https://api.roasos.com/v1/jobs/gen_…/status -H "Authorization: Bearer roasos_sk_live_…" curl https://api.roasos.com/v1/jobs/gen_…/result -H "Authorization: Bearer roasos_sk_live_…" ``` ## Today The endpoint is `POST /v1/assembly/compose`. It is live. ## See also https://docs.roasos.com/models/assembly/compose · https://api.roasos.com/v1/catalog/assembly/compose · fal-ai/ffmpeg-api/compose