Home › Guides › Errors & retries

Seedance request errors, what triggers them, and a retry policy that never double-bills

Updated 2026-10-02

Seedance has several ways to send a wrong request, because it supports several input modes: text, a start image, reference images, videos and audio, and editing. Most of its errors are therefore not outages but combinations the API refuses. This page sorts them into what you fix, what you retry and what you must never retry, and gives code that keeps a retry from creating a second paid job. The generic HTTP policy is also in the how-to-call guide; here the focus is on Seedance-shaped failures.

Errors you fix, not retry

These come back at creation as 400 invalid_request_error, before any job exists, so they are free and immediate. Retrying will return the same answer.

CauseWhat to do
Unknown model id (a typo in bytedance/seedance-2.5, or a variant that is not in the catalog)Check GET /v1/videos/models or the model page.
Missing prompt on a model that requires oneSend a prompt; for image-to-video it should describe motion.
start_image_url sent to a model or host that has no image inputUse a variant or host that does, per the model page.
end_image_url in the bodyRemove it. The documentation states no model accepts it yet and it is rejected with a 400, not ignored.
Start image combined with reference arrays, or either combined with input_video_urlPick one intent per request. These modes are mutually exclusive on Seedance.
Editing (input_video_url) without a promptEditing requires a non-empty prompt describing the change.
Reference arrays over their capsExceeding a cap is a 400, not a truncation. Documented caps are up to 9 images, 3 videos and 3 audio clips, 12 combined, but they depend on the model and host.

Also remember what is not an error: a wrong resolution or aspect_ratio combination returns success and the default is used. Test dimensions with ffprobe; do not look for an error that will not come.

The host-capability trap

Seedance is resold by many hosts, and the documentation notes that reference video and audio are supported on some hosts while others take images only. A request that works on one host can be refused on another, and because the router falls back across hosts, a request using a soft model/host preference may land somewhere you did not test. When you depend on a capability that only some hosts have, restrict the candidate set deliberately:

{
  "model": "bytedance/seedance-2.0",
  "prompt": "...",
  "input_references": [{"type": "image_url", "image_url": {"url": "https://..."}}],
  "provider": {"only": ["fal"], "allow_fallbacks": false}
}

Check the model page for which hosts offer the mode, and note that allow_fallbacks: false means exactly one attempt, with an immediate error on failure. The model-level fallback array is a separate setting.

Errors you retry

Never retry 401, 402 or 403. They are key, budget and policy problems: invalid_api_key, spend_cap_exceeded or insufficient_credits, and model_not_allowed.

The duplicate-job risk

Creation is billed once, in full, at the moment the job is created. That makes one failure mode expensive: a connection that drops or times out after the server accepted the job. Your client sees an exception, a naive retry decorator runs again, and you have two paid clips. The documentation does not describe an idempotency key for video creation, so build the protection yourself with a ledger written before the request:

import hashlib, json, random, sqlite3, time, requests

BASE = "https://videorouter.sh/api/v1"
H = {"Authorization": "Bearer llmr_sk_live_..."}
db = sqlite3.connect("jobs.db")
db.execute("create table if not exists jobs (key text primary key, state text, job_id text)")

def key_for(payload: dict) -> str:
    return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()

def create_once(payload: dict, attempts: int = 4) -> str:
    k = key_for(payload)
    row = db.execute("select state, job_id from jobs where key=?", (k,)).fetchone()
    if row and row[0] == "created":
        return row[1]                          # already submitted: reuse
    if row and row[0] == "unknown":
        raise RuntimeError("earlier attempt is unresolved: reconcile before re-submitting")
    db.execute("insert or ignore into jobs values (?, 'pending', null)", (k,)); db.commit()
    for n in range(attempts):
        try:
            r = requests.post(f"{BASE}/videos", headers=H, json=payload, timeout=(5, 30))
        except (requests.ConnectionError, requests.Timeout):
            db.execute("update jobs set state='unknown' where key=?", (k,)); db.commit()
            raise                              # do not loop: a job may exist
        if r.ok:
            jid = r.json()["id"]
            db.execute("update jobs set state='created', job_id=? where key=?", (jid, k)); db.commit()
            return jid
        if r.status_code in (400, 401, 402, 403):
            db.execute("delete from jobs where key=?", (k,)); db.commit()
            raise RuntimeError(r.json().get("error"))
        wait = float(r.headers.get("Retry-After") or 0) if r.status_code == 429 else 0
        time.sleep(max(wait, min(30, 2 ** n)) + random.random())
    raise RuntimeError("retries exhausted; nothing was created")

The important branches are the connection error, which marks the row unknown and stops, and the early return for rows already created, which also absorbs queue redeliveries and double-clicks. Resolve an unknown row by checking your own records and your account's usage before deciding to resubmit.

Accepted, then failed

A job can be created successfully and later report status: "failed", with the provider's message in error. That arrives as a normal 200 on the poll, so check the field, not just the HTTP status. The most common cause on image-to-video and reference jobs is an input URL the host cannot fetch, particularly presigned links that expired before the host read them. Fix the URL, then submit again; since a job that failed because every upstream host failed is not billed, this is a safe retry. If a job is merely slow, do not resubmit: keep polling, which is free.

Stalls are a separate problem with a separate tool. failover.on_timeout_sec hedges to a second host when an accepted job is late, but it bills both attempts if both finish, so never combine it with your own resubmit-on-timeout loop.

A short checklist

For field-by-field semantics see the parameters guide, and for the staged workflow that makes failures cheap see the draft pipeline. Create a key and test the failure paths with deliberately bad requests first; they cost nothing.

Frequently asked questions

Which Seedance errors should I retry?

Only 429, after the Retry-After delay, and 500, 502, 503 or 504 with backoff. 5xx from an exhausted fallback chain is documented as not billed. Never retry 400, 401, 402 or 403.

How do I avoid creating duplicate Seedance jobs on a timeout?

Do not retry a create after a dropped connection, because the job may exist and be billed. Write a ledger row before the request, store the job id after, and reconcile any row left in an unknown state.

Why does a request work on one host and 400 on another?

Capabilities such as reference video and audio vary by host. Use provider.only with allow_fallbacks false when you depend on a host-specific mode, and check the model page for which hosts support it.

Keep reading

Using Seedance is one part of the job.

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 →