Seedance 2.5 API: Turn a Photo into a Video in Code
By Vellm ·
If you want one video, Vellm is the quick route: pick a template, add your photo and you're done. If you're building your own app and want photo-to-video inside it, you need an API. This guide walks through calling Seedance 2.5 through VizoAPI, from the first request to a downloaded MP4.
What you can make
Everything goes through one endpoint, POST /v1/generations. You put the prompt and your media in a single list, input.content, and give every image, video or audio item a role. The items you send decide the mode:
| You want to | Send in content |
|---|---|
| Make a video from a prompt | A text item |
| Bring a photo to life | A prompt and one image with role first_frame |
| Move from one picture to another | A first_frame and a last_frame image |
| Reuse people, objects or a style | A prompt and up to 30 images with role reference_image |
| Copy the motion or rhythm of a clip | Reference videos and audio, with omni_reference_task_type set to reference |
| Change or lengthen an existing clip | The source video, with omni_reference_task_type set to edit or extend |
Videos run 4 to 30 seconds at 480p, 720p or 1080p. They come with a soundtrack unless you set generate_audio to false. The Seedance 2.5 model page has a complete request body for every mode.
Before you start
Create an account, buy Credits and make a project key in the VizoAPI console. Keep the key on your server. It never belongs in a web page, a mobile app or a repository.
Your photo has to be a public https URL or a file you uploaded to your project. The model fetches it after you submit, sometimes a while later. Keep the link working until the task finishes. Otherwise the task fails with input_media_unreachable.
Check the price before you spend
Send your request body to POST /v1/estimates first. It's free: it creates no task and holds no Credits. It also checks your input the same way a real request would, so a broken body fails here instead of later.
const api = "https://api.vizoapi.com/v1";
const headers = {
Authorization: `Bearer ${process.env.VIZO_API_KEY}`,
"Content-Type": "application/json",
};
const body = {
model: "seedance-2-5",
input: {
content: [
{ type: "text", text: "The dog tilts its head, then trots toward the camera, slow push-in, soft daylight" },
{ type: "image_url", role: "first_frame", image_url: { url: "https://example.com/dog.jpg" } },
],
resolution: "720p",
ratio: "adaptive", // a first-frame video takes the photo's shape
duration: 5,
},
metadata: { client_reference: "order-1042" }, // your own order number
};
const estimate = await fetch(`${api}/estimates`, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => r.json());
console.log(estimate.estimate.credits); // "283.000"
Submit once, then wait
Send the same body to POST /v1/generations. A 202 response means the task was accepted. Save its id right away.
// Send once: switch off automatic retries for this request
const task = await fetch(`${api}/generations`, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => r.json());
const taskId = task.id;
let result;
do {
await new Promise((r) => setTimeout(r, 5000));
result = await fetch(`${api}/generations/${taskId}`, { headers }).then((r) => r.json());
} while (!result.closed_at);
if (result.status !== "succeeded") throw new Error(`task closed: ${result.status} ${result.error?.code}`);
Polling every few seconds is fine for a script. In production, register a webhook endpoint in the console and pass webhook_endpoint_id when you submit.
Download the video
Ask for a short-lived download link, then fetch that link on its own, without your API key:
import { writeFile } from "node:fs/promises";
const dl = await fetch(`${api}/assets/${result.result.asset_id}/download`, { headers }).then((r) => r.json());
const media = await fetch(dl.url);
await writeFile("result.mp4", Buffer.from(await media.arrayBuffer()));
Finished files are kept for 7 days. If a link expires, request a new one for the same asset.
Don't pay twice
Every accepted POST /v1/generations is a new task with its own charge, even when the body is identical. That's why the example sets metadata.client_reference. If your request times out, don't just send it again. Look the task up first: read it by ID if you have one, or find it with GET /v1/generations?client_reference=order-1042. Also make sure your HTTP client and any proxy don't retry the request on their own.
What it costs
You pay in Credits. The standard pack is $10 for 2,000 Credits, so one Credit is half a cent. Seedance 2.5 is billed per second of video:
| Resolution | Credits per second | About |
|---|---|---|
| 480p | 26.4 | $0.13 a second |
| 720p | 56.6 | $0.28 a second |
| 1080p | 136.4 | $0.68 a second |
That's about 40% less than fal per second, and less than many other video APIs. A 5-second photo-to-video clip at 720p holds 283 Credits, about $1.42. Requests that include a reference video use cheaper per-second tiers, but they also count the input video's seconds. A task that fails, times out or gets blocked by content moderation costs nothing. These are the prices on 11 November 2026; the model page always has the current ones.
Tips for better results
- Draft first. Set
"draft": truewith480pwhile you work on the prompt. When you like a draft, turn it into a 1080p video within 7 days without paying for another round of trial and error. - Name your references. Call them "image 1", "image 2" or "video 1", in the order you send them, and say what to take from each: "the dog from image 1".
- Write the shot, not just the subject. Cover the action, the setting, the camera move and the light.
- Say what stays the same. When you edit a clip, spell out what changes and add "keep everything else unchanged".
- Keep settings out of the prompt. Use the input fields. Flags such as
--ratio 16:9in the prompt are refused.
Rather not write code?
If you just want a trend video of yourself or your pet, you don't need an API. Browse the Vellm templates, or start from your own idea in Create.