Calling Seedance 2.5 from curl and Python, step by step
Updated 2026-10-02
Seedance 2.5 is called through one endpoint, POST https://videorouter.sh/api/v1/videos, using the model id bytedance/seedance-2.5. Video generation is asynchronous: you create a job, poll it, and read the file URL when it completes. This page is the shortest path from an API key to a downloaded clip, plus the parts that bite in production: host pinning and error handling.
Step 1: create the job with curl
curl https://videorouter.sh/api/v1/videos \
-H "Authorization: Bearer llmr_sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"model": "bytedance/seedance-2.5",
"prompt": "a paper airplane gliding over a city at dusk, slow tracking shot",
"duration_secs": 5,
"resolution": "720p",
"aspect_ratio": "16:9"
}'
The response is immediate and carries a job, not a video: an id, a status of queued, and the provider that accepted it. The full cost is billed once at this moment, from the requested (snapped) duration. Nothing after this point charges you.
Step 2: poll until it finishes
curl https://videorouter.sh/api/v1/videos/video_abc123 \
-H "Authorization: Bearer llmr_sk_live_..."
Status moves queued, in_progress, then completed or failed. Polling is free, so poll every few seconds without worrying about cost. On completion the file URL is at data[0].url; on failure error carries the provider's message and data is null. A job that fails upstream is not billed.
The same flow in Python
import time
import requests
BASE = "https://videorouter.sh/api/v1"
HEADERS = {"Authorization": "Bearer llmr_sk_live_...", "Content-Type": "application/json"}
def generate(prompt, model="bytedance/seedance-2.5", timeout=900, **fields):
payload = {"model": model, "prompt": prompt, **fields}
for _ in range(3):
r = requests.post(f"{BASE}/videos", headers=HEADERS, json=payload)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 5)))
continue
r.raise_for_status()
job = r.json()
break
else:
raise RuntimeError("rate limited on every attempt")
deadline = time.time() + timeout
while job["status"] not in ("completed", "failed"):
if time.time() > deadline:
raise TimeoutError(f"job {job['id']} still running; do not resubmit")
time.sleep(5)
job = requests.get(f"{BASE}/videos/{job['id']}", headers=HEADERS).json()
if job["status"] == "failed":
raise RuntimeError(job["error"])
return job["data"][0]["url"]
url = generate("a paper airplane gliding over a city", duration_secs=5, resolution="720p")
Two details in that wrapper matter. It never re-submits on a timeout, because a second submission is a second charge and polling costs nothing. And it stores nothing about the file: copy the finished video to your own storage rather than treating the returned URL as permanent.
Choosing a variant
The variants share one request shape; only the model string changes. The ids to know are bytedance/seedance-2.5, bytedance/seedance-2.0, bytedance/seedance-2.0-fast and bytedance/seedance-2.0-mini. That makes a draft-then-final pipeline a config change: iterate on a lighter variant, then re-run approved prompts on 2.5. See the variant guide for the trade-offs. Some variants accept modes that others do not (start images, reference arrays), so confirm on the model pages before building on one.
Pinning a host
Seedance is sold by many hosts at different prices. With no preference, VideoRouter picks the cheapest healthy host and walks down the list if a submission is rejected. To prefer one host, suffix the id: bytedance/seedance-2.5/fal or bytedance/seedance-2.5/machgen. The host slugs are listed in the host table on each model page.
The suffix is a soft preference. If that host rejects the request, the router still falls back to others. For a hard pin with no fallback, use the provider object instead:
{
"model": "bytedance/seedance-2.5",
"prompt": "...",
"provider": {"only": ["fal"], "allow_fallbacks": false}
}
With allow_fallbacks false you get exactly one attempt and an immediate error if it fails, so you give up failover in exchange for certainty. Pin when a capability exists only on certain hosts (reference video and audio, for example) or when you have a policy reason; otherwise let it float. The live table shows how wide the price gap between hosts is right now:
| Model | Cheapest host | Priciest host | Cheapest is | Hosts |
|---|---|---|---|---|
| bytedance/seedance-2.5 (480p) | OpenSand $0.0525 / second | Fal-US $0.2646 / second | 80% lower | 9 |
| bytedance/seedance-2.0 (2160p) | MachGen $0.59 / second | Fal $1.5552 / second | 62% lower | 9 |
| bytedance/seedance-2.0-fast (480p) | Atlas Cloud $0.027 / second | Fal $0.2419 / second | 89% lower | 9 |
| bytedance/seedance-2.0-mini (480p) | OpenSand $0.0104 / second | Fal $0.0721 / second | 86% lower | 8 |
| seedance-2-mini-unrestricted (480p) | OpenSand $0.0114 / second | SandBase $0.0721 / second | 84% lower | 3 |
| seedance-2-5-unrestricted (1080p) | OpenSand $0.3482 / second | TOAPIS $0.5881 / second | 41% lower | 2 |
| seedance-2.0-fast-unrestricted (480p) | OpenSand $0.0344 / second | SandBase $0.0448 / second | 23% lower | 2 |
| seedance-2-unrestricted (2160p) | OpenSand $0.661 / second | TOAPIS $0.7966 / second | 17% lower | 2 |
Per second, before VideoRouter's 2% platform fee. For tiered models each row compares the resolution tier with the widest host-to-host gap. Built 2026-10-02 from the live catalog.
Errors you will actually see
Every error uses the OpenAI-style envelope {"error": {"message", "type", "code"}}, regardless of which host was involved.
| Status | Meaning | What to do |
|---|---|---|
400 invalid_request_error | Missing prompt, unknown model id, or an input field the model does not accept | Fix the request; retrying will not help |
401 invalid_api_key | Key missing, malformed, revoked or expired | Check the key |
| 402 | Monthly key cap reached, or prepaid balance exhausted | Top up or raise the cap |
403 model_not_allowed | Model is outside this key's allow-list | Adjust the key's allow-list |
| 429 | Per-key rate limit | Wait for the Retry-After header, then retry |
500, 502, 503, 504 upstream_error | Every candidate host failed for this request | Safe to retry; failed attempts are not billed |
Distinguish a failed creation call (above) from a job that is accepted and later reports failed. The second still arrives as a normal 200 response with status: "failed", so check the status field, not just the HTTP code. For jobs that stall after acceptance, failover.on_timeout_sec can hedge to a second host, but both attempts are billed if both finish, so use it deliberately.
Common first-call mistakes
- Trusting resolution and aspect ratio. An unsupported combination is ignored and the model default is used, never a 400. Check the dimensions of the returned file.
- Expecting synchronous output. There is no mode that returns the video in the create response.
- Sending an image to a model that rejects it.
start_image_urlreturns a 400 on models without image input; see image-to-video for the supported setup. - Forgetting the platform fee. A 2% fee applies to image and video usage on top of the host's rate.
For the full parameter list continue with Seedance 2.5 parameters explained, or create an API key and run the curl above. The quickstart has the same flow for other models.
Frequently asked questions
What is the model id for Seedance 2.5?
It is bytedance/seedance-2.5. The 2.0 variants are bytedance/seedance-2.0, -fast and -mini, and all use the same request shape.
Is the Seedance API synchronous?
No. POST /videos returns a job, and you poll GET /videos/{id} through queued and in_progress until completed or failed. Polling is free.
How do I force a specific host?
Suffix the model id, for example bytedance/seedance-2.5/fal, for a soft preference. For a hard pin with no fallback use provider: {only: [...], allow_fallbacks: false}.
Am I charged if the job fails?
Jobs that fail upstream are not billed. A successful job is billed once at creation from the requested duration, plus a 2% platform fee.
Keep reading
- Seedance 2.5 vs 2.0 vs Fast vs Mini — Choosing a Variant for API Use
- Seedance Reference-to-Video API: Images, Video, Audio Refs
- Seedance Image-to-Video API: start_image_url Prompts & Setup
- Seedance vs Kling vs Veo API: How to Choose by Workload
VideoRouter puts it next to dozens of other video and image models behind one API key, so you can compare providers, prices and fail over automatically. Compare providers on VideoRouter →