# Video generation > POST /v1/videos and polling: prompts, length, resolution, aspect ratio, audio, start and end frames, reference images and prices. Source: https://zurelay.com/docs/video Make videos with Seedance from a prompt, from a first frame (and optionally a last one), or from up to 9 reference images. Videos take a few minutes, so you start one and check back. `POST /v1/videos` | Model | ID | Price per second | Length | | --- | --- | --- | --- | | [Seedance 2.5](https://zurelay.com/models/seedance-2.5) | `seedance-2.5` | 480p $0.14 · 720p $0.305 · 1080p $0.75 | 4 to 30 s | | [Seedance 2.0](https://zurelay.com/models/seedance-2.0) | `seedance-2.0` | 480p $0.097 · 720p $0.207 | 4 to 15 s | | [Seedance 2.0 Fast](https://zurelay.com/models/seedance-2.0-fast) | `seedance-2.0-fast` | 480p $0.074 · 720p $0.159 | 4 to 15 s | | [Seedance 2.0 Mini](https://zurelay.com/models/seedance-2.0-mini) | `seedance-2.0-mini` | 480p $0.047 · 720p $0.10 | 4 to 15 s | ## Start, poll, download **Python** (`video.py`): ```python import os, time, requests API = "https://api.zurelay.com/v1" HEADERS = {"Authorization": f"Bearer {os.environ['ZURELAY_API_KEY']}"} # 1. Start the video response = requests.post(f"{API}/videos", headers=HEADERS, json={ "model": "seedance-2.0", "prompt": "A paper boat drifting down a rain-soaked street at night, neon reflections, slow dolly shot", "seconds": 5, "resolution": "720p", "aspect_ratio": "16:9", }) response.raise_for_status() # a 4xx says what to fix job = response.json() # 2. Poll until it's done (usually 2 to 6 minutes) while job["status"] in ("queued", "in_progress"): time.sleep(10) job = requests.get(f"{API}/videos/{job['id']}", headers=HEADERS).json() if job["status"] == "failed": raise SystemExit(job["error"]["message"]) # 3. Download the MP4 video = requests.get(f"{API}/videos/{job['id']}/content", headers=HEADERS) open("boat.mp4", "wb").write(video.content) ``` **Node.js** (`video.mjs`): ```javascript import { writeFile } from "node:fs/promises"; const API = "https://api.zurelay.com/v1"; const headers = { Authorization: `Bearer ${process.env.ZURELAY_API_KEY}`, "Content-Type": "application/json", }; // 1. Start the video let job = await (await fetch(`${API}/videos`, { method: "POST", headers, body: JSON.stringify({ model: "seedance-2.0", prompt: "A paper boat drifting down a rain-soaked street at night, neon reflections, slow dolly shot", seconds: 5, resolution: "720p", aspect_ratio: "16:9", }), })).json(); // 2. Poll until it's done (usually 2 to 6 minutes) while (job.status === "queued" || job.status === "in_progress") { await new Promise((resolve) => setTimeout(resolve, 10_000)); job = await (await fetch(`${API}/videos/${job.id}`, { headers })).json(); } if (job.status === "failed") throw new Error(job.error.message); // 3. Download the MP4 const video = await fetch(`${API}/videos/${job.id}/content`, { headers }); await writeFile("boat.mp4", Buffer.from(await video.arrayBuffer())); ``` **cURL**: ```bash # 1. Start the video curl https://api.zurelay.com/v1/videos \ -H "Authorization: Bearer $ZURELAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2.0", "prompt": "A paper boat drifting down a rain-soaked street at night", "seconds": 5, "resolution": "720p" }' # 2. Check on it (repeat until "status" is "completed") curl https://api.zurelay.com/v1/videos/video_abc123 -H "Authorization: Bearer $ZURELAY_API_KEY" # 3. Download it curl -L https://api.zurelay.com/v1/videos/video_abc123/content \ -H "Authorization: Bearer $ZURELAY_API_KEY" -o boat.mp4 ``` ## Parameters - `model` (string, required): A video model ID. - `prompt` (string): What happens, up to 8,000 characters. Required unless you give a first frame or reference images. - `seconds` (integer): Length, within the model’s range (see the table). Default 5. - `resolution` (string): `480p`, `720p` or `1080p`, as the model offers. Default 720p. - `aspect_ratio` (string): `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `21:9`, or `adaptive` (the first frame’s shape; the default when you give one). Otherwise the default is `16:9`. - `size` (string): Instead of resolution and ratio: pixels like `1280x720`. - `generate_audio` (boolean): Sound and speech made with the video. Default `true`. (`audio` works too.) - `seed` (integer): The same seed and settings give similar results. - `first_frame` (string): An image the video starts on: a link or a data URL. - `last_frame` (string): An image it ends on. Needs a first frame. - `reference_images` (array): Up to 9 images of characters, products, places or a style to use. Not with frames. ## Starting from images ### First and last frame Animate a still: the video opens exactly on `first_frame`. Add `last_frame` and it lands on that image, so you control both ends of the shot. Request body: ```json { "model": "seedance-2.0", "first_frame": "https://example.com/storefront-day.jpg", "last_frame": "https://example.com/storefront-night.jpg", "prompt": "Time passes from afternoon to night; lights come on inside the shop.", "seconds": 6, "resolution": "720p" } ``` ### Reference images Give up to 9 images and mention them in the prompt by order (`@Image1`, `@Image2`…): the people, products and places in them appear in the video. Request body: ```json { "model": "seedance-2.5", "reference_images": [ "https://example.com/chef.jpg", "https://example.com/kitchen.jpg", "https://example.com/dish.jpg" ], "prompt": "The chef from @Image1 plates the dish from @Image3 in the kitchen from @Image2, close-up, warm light.", "seconds": 8, "resolution": "720p", "aspect_ratio": "9:16" } ``` ### Rules for images - PNG, JPEG or WebP, as a public `https` link or a data URL, up to 10 MB. - 300 to 6000 pixels on each side, and no wider or taller than 5:2. Data URLs outside these are refused up front with a message saying why. Links are read when the video starts: one that isn’t a usable image fails the video with `invalid_image`, free of charge. - Frames or reference images, not both in one request. Reference videos and audio aren’t supported. - Requests are JSON: images go in as links or data URLs, not as multipart uploads. ## The video object GET /v1/videos/video_abc123: ```json { "id": "video_abc123", "object": "video", "model": "seedance-2.0", "status": "completed", "progress": 100, "created_at": 1790870400, "completed_at": 1790870562, "expires_at": 1793462400, "seconds": 5, "resolution": "720p", "aspect_ratio": "16:9", "size": "1280x720", "generate_audio": true, "url": "https://...signed link...", "url_expires_at": 1790956962, "cost": 1.035, "error": null } ``` | Field | Means | | --- | --- | | `status` | `queued`, `in_progress`, `completed` or `failed`. | | `progress` | An estimate from 0 to 100, from how long the model usually takes. | | `url` | The MP4, once completed. The link works for 24 hours; fetch the job again for a fresh one. | | `expires_at` | When the video is deleted: 30 days after it was made, or sooner if someone deletes it in the Library. | | `cost` | What it cost, once completed. | | `error` | For failed videos: `code` (`content_policy`, `invalid_image`, `generation_failed`, or `generation_timeout` after 90 minutes) and a message. | ## Other endpoints | Endpoint | Returns | | --- | --- | | `GET /v1/videos/{id}` | One video, as above. | | `GET /v1/videos/{id}/content` | The MP4 itself. | | `GET /v1/videos?limit=20&after=video_abc` | Your workspace’s videos, newest first, with `first_id` and `last_id` for paging. | ## Paying for video A video costs its price per second (by resolution) times its length. When you start one, its price is set aside from your balance; it’s charged when the video is done, and released if it fails. A failed video is free. > **Tip:** Draft at 480p and a short length, then make the final cut at 720p or 1080p once the prompt is right. Most of the wait is the queue, so drafts aren’t much faster, but they cost a fraction.