How to Access the Seedance 2.0 Video Generation API (2026)
Seedance 2.0 API quickstart: one ofox key, POST /v1/videos, poll to completed, read unsigned_urls[0]. Python + Node code, pricing from $0.07/s.
Seedance 2.0 is ByteDance’s text-to-video and image-to-video model, and the fastest way to call it is through ofox: one API key, USD billing, and a single async REST endpoint. You POST https://api.ofox.io/v1/videos with a model ID and a prompt, get back a polling_url, poll until the status is completed, then read the clip from unsigned_urls[0]. No per-vendor account, no separate SDK. The rest of this guide is the working quickstart: auth, a text-to-video call in Python and Node, the async lifecycle, image and reference inputs, and what a clip actually costs.
| Endpoint | POST https://api.ofox.io/v1/videos (async) |
| Auth | one ofox Bearer key |
| Model IDs | bytedance/seedance-2.0, -fast, -mini |
| Time to first clip | about 5 minutes |
| Get the result | poll GET /v1/videos/{id}, read unsigned_urls[0] |
What You Need
Three things, and you already have two of them if you use ofox for chat or image calls.
- An ofox API key. The base URL is
https://api.ofox.io/v1and auth is a standardAuthorization: Bearerheader. The same key you use for OpenAI-compatible chat and image requests also submits video jobs, so there is no separate account or vendor SDK to set up. - The endpoint. Everything runs through
POST /v1/videosandGET /v1/videos/{id}. That is the whole surface. - A model ID.
bytedance/seedance-2.0is the flagship: text-to-video, image-to-video, video-to-video, synced audio, clips from 4 to 15 seconds, up to 4K.bytedance/seedance-2.0-fastandbytedance/seedance-2.0-miniare the cheaper tiers and both cap at 720p.
Set the key once:
export OFOX_API_KEY="sk-..."
Your First Request: Text-to-Video
Video generation is not a blocking call the way a chat completion is. The clip takes render time, so POST /v1/videos returns 202 Accepted right away with a polling_url, and you poll that URL until the job reaches a terminal state. The mode is inferred from the fields you send: no image field means text-to-video.
Here is a complete round trip in Python. It submits the job, polls at a sane cadence, and pulls the clip from unsigned_urls[0].
import os, time, requests
BASE = "https://api.ofox.io/v1"
HEAD = {"Authorization": f"Bearer {os.environ['OFOX_API_KEY']}"}
TERMINAL = {"completed", "failed", "cancelled", "expired"}
job = requests.post(f"{BASE}/videos", headers=HEAD, json={
"model": "bytedance/seedance-2.0",
"prompt": "A red kayak cuts through morning fog on a still lake, slow dolly forward.",
"duration": 8,
"resolution": "1080p",
"aspect_ratio": "16:9",
})
job.raise_for_status() # 202 Accepted
task_url = job.json()["polling_url"]
while True:
task = requests.get(task_url, headers=HEAD).json()
if task["status"] in TERMINAL: # break on ALL four terminal states
break
time.sleep(2) # poll every 1-2s, not in a tight loop
if task["status"] == "completed":
print("clip:", task["unsigned_urls"][0]) # there is no output_url field
print("billed:", task["usage"]["video_cost"], "USD",
"for", task["usage"]["video_seconds"], "s")
else:
print("job ended as:", task["status"])
The same shape in Node with fetch:
const BASE = "https://api.ofox.io/v1";
const HEAD = {
Authorization: `Bearer ${process.env.OFOX_API_KEY}`,
"Content-Type": "application/json",
};
const TERMINAL = ["completed", "failed", "cancelled", "expired"];
const job = await fetch(`${BASE}/videos`, {
method: "POST",
headers: HEAD,
body: JSON.stringify({
model: "bytedance/seedance-2.0",
prompt: "A red kayak cuts through morning fog on a still lake, slow dolly forward.",
duration: 8,
resolution: "1080p",
aspect_ratio: "16:9",
}),
});
const { polling_url } = await job.json(); // 202 Accepted
let task;
do {
await new Promise((r) => setTimeout(r, 2000)); // poll every 1-2s
task = await (await fetch(polling_url, { headers: HEAD })).json();
} while (!TERMINAL.includes(task.status));
if (task.status === "completed") {
console.log("clip:", task.unsigned_urls[0]); // not output_url
console.log("billed:", task.usage.video_cost, "USD");
}
Two things trip up first-time callers. The first is reaching for resp["output_url"], which does not exist and returns None in Python or undefined in Node. The clip lives in unsigned_urls[0]. The second is a poll loop that only checks for completed. A job that ends in failed or expired never sets completed, so a loop that ignores the other terminal states spins forever. Break on all four.
Handling the Async Lifecycle
A Seedance job moves through a fixed set of states. Three are transient and four are terminal. There is no processing state, so do not check for one.
| Status | Phase | What to do |
|---|---|---|
pending | Accepted, not yet queued | Keep polling every 1-2s |
queued | Queued for render | Keep polling every 1-2s |
in_progress | Rendering | Keep polling every 1-2s |
completed | Done, URLs attached | Download from unsigned_urls[0] |
failed | Generation error | Read the error, retry or fall back |
cancelled | You cancelled it | Stop polling |
expired | Task aged out | Resubmit |
Poll cadence matters. GET /v1/videos/{id} no faster than once per second; roughly every one to two seconds is the sweet spot. Hammering it in a tight loop wastes quota and gets you nothing sooner, since the clip is rendering upstream regardless.
The result URLs have a shelf life, so treat completed as your cue to download. unsigned_urls expire about 24 hours after the job finishes. mirror_urls are persistent, but each signed link still carries its own TTL. In practice: when the status flips to completed, pull unsigned_urls[0] into your own bucket (S3, R2, GCS) immediately rather than storing the API URL and serving it to clients later.
For production you usually want a webhook instead of a polling thread. Pass a callback_url at creation and ofox posts an HMAC-signed payload when the job reaches a terminal state. The address must be public HTTPS; a private, loopback, or otherwise unreachable host is rejected at submit time with a 400 invalid_callback_url. Polling and webhooks are not mutually exclusive, so a common pattern is a webhook for the happy path plus a slow poll as a backstop.
Need to stop a job early? DELETE /v1/videos/{id} cancels it, and the task settles into cancelled.
Image-to-Video and Reference Images
You do not switch endpoints to animate a still or steer generation with reference frames. Same POST /v1/videos, same polling. The request mode is inferred from which fields you include.
- No image field gives text-to-video, the call above.
frame_imagesgives image-to-video. Pass one URL for a single starting frame, or a first and last frame for interpolation between them.input_referencesgives reference-guided generation, where you supply images that anchor identity, style, or a product’s look.
Image-to-video from a starting frame:
job = requests.post(f"{BASE}/videos", headers=HEAD, json={
"model": "bytedance/seedance-2.0",
"prompt": "The logo tilts up and catches a rim light, subtle rotation.",
"frame_images": ["https://your-cdn.com/first-frame.png"],
"duration": 8,
"resolution": "1080p",
"aspect_ratio": "16:9",
})
Everything downstream is identical: 202, a polling_url, the same status set, and the clip in unsigned_urls[0]. Audio is generated across all three tiers, so name the sound you want in the prompt or you inherit whatever the model infers.
Pricing: Per Resolution, Not Flat
This is the number people get wrong. The from $0.07/s next to Seedance 2.0 on the catalog is a 480p floor, not a flat rate. Price scales with the resolution you request, so the real cost of a clip is the per-second rate at your resolution times the clip length. Here is the flagship text-to-video rate card:
| Resolution | bytedance/seedance-2.0 (text-to-video) |
|---|---|
| 480p | $0.07/s |
| 720p | $0.16/s |
| 1080p | $0.34/s |
| 4K | $1.37/s |
Billing is per output second, and the completed response tells you the exact charge in usage.video_cost. So an 8-second 1080p clip is 8 x $0.34 = $2.72, and the same clip at 4K is 8 x $1.37 = $10.96. Video-to-video runs a little higher per second than text-to-video at each resolution, which the model page spells out in full.
The cheaper tiers trade resolution for cost and both cap at 720p: bytedance/seedance-2.0-fast is $0.06/s at 480p and $0.13/s at 720p, while bytedance/seedance-2.0-mini is $0.04/s at 480p and $0.08/s at 720p. If most of your output lands on a feed that downscales to 720p anyway, Mini at $0.08/s is the lowest 720p rate of the three.
Because all three tiers share the same endpoint and differ only by the model string, you can route drafts to Mini and reserve the flagship for masters. For the full head-to-head, including when Fast is worth its premium over Mini, read the Seedance 2.0 tier comparison and the Seedance 2.0 model page for the complete per-resolution matrix.
One Key, Every Video Model
The reason this quickstart is short is that ofox collapses the whole video stack into one auth and one schema. You already have the pattern: submit to /v1/videos, poll, download. Swapping models is a string change.
Generate on Seedance 2.0 with the key you already have. Start on the ofox video API: one key, USD billing, pay only for the seconds you render, no per-vendor signup.
If Seedance is not the right fit for a given clip, the same endpoint reaches the rest of the catalog. Alibaba’s Wan starts at a 2-second minimum where Seedance floors at 4 seconds; the Seedance 2.0 vs Wan comparison covers that trade. The exact request and response fields, including every optional parameter, live in the ofox video API reference.
FAQ
How do I call the Seedance 2.0 API?
Send POST https://api.ofox.io/v1/videos with a Bearer ofox key and a JSON body of model, prompt, duration, resolution, and aspect_ratio. It returns 202 with a polling_url. Poll GET /v1/videos/{id} every 1-2 seconds until status is completed, then read the clip from unsigned_urls[0].
Why is the video URL empty when I read output_url?
There is no output_url field. The clip is in unsigned_urls (an array, so use unsigned_urls[0]) or in mirror_urls. Reading resp["output_url"] returns None every time.
How long do Seedance 2.0 result URLs stay valid?
unsigned_urls expire about 24 hours after completion. mirror_urls are persistent, but each signed link carries its own TTL. Download the file as soon as the status is completed.
How much does the Seedance 2.0 API cost? Per output second by resolution, not a flat rate. Flagship text-to-video is $0.07/s at 480p, $0.16/s at 720p, $0.34/s at 1080p, and $1.37/s at 4K. An 8-second 1080p clip is $2.72.
Can Seedance 2.0 generate video from an image?
Yes. Pass frame_images for image-to-video or input_references for reference-guided generation. The mode is inferred from the fields; no image means text-to-video.
What is the cheapest Seedance 2.0 model?
bytedance/seedance-2.0-mini, at $0.04/s for 480p and $0.08/s for 720p. It caps at 720p, as does -fast. Only the flagship reaches 1080p and 4K.
Sources Checked for This Refresh
- ofox video model catalog and per-second floor pricing
- ofox Seedance 2.0 model detail, 480p to 4K per-resolution pricing
- ofox video API reference: endpoint, polling, status enum, callback_url
- ByteDance Seed, Seedance model overview
- Seedance 2.0 tier comparison: Mini, Fast, and flagship
- Seedance 2.0 vs Wan video API comparison
