Poll a 30-second Seedance 2.5 job in Python: backoff and a deadline

A 30-second Seedance 2.5 job is slow. Poll /v1/videos/{id} with growing delays and a deadline. A stdlib Python script submits, waits and prints the cost.

4 min readSume
All posts

Submit with POST /v1/videos, then poll GET /v1/videos/{id} with a delay that grows from a few seconds to about 20, and stop at a deadline you choose. The script below does that with the Python standard library and prints the status, the result URL and usage.

Do not poll in a tight loop. A long Seedance 2.5 job takes a while, and a one-second poll mostly returns the same pending status while adding load.

Statuses to handle

The poll response is the OpenRouter-shaped object. Sume maps its internal statuses to pending, in_progress, completed, failed and cancelled. expired exists in the enum for wire compatibility, but Sume never emits it. Treat completed, failed and cancelled as terminal.

Poll statuses on /v1/videos, read 2026-10-05
Sume statusPoll statusAction
queuedpendingkeep polling
processingin_progresskeep polling
completedcompletedread unsigned_urls and usage
failedfailedread error, do not retry blindly
canceledcancelledstop

The script

Set SUME_API_KEY first. The Idempotency-Key header makes a retry of the submit safe: the same key with the same body returns the same job, and the same key with a different body returns 409.

import json, os, time, urllib.request

KEY = os.environ["SUME_API_KEY"]

def call(method, path, body=None, idem=None):
    headers = {"Authorization": "Bearer " + KEY, "Content-Type": "application/json"}
    if idem:
        headers["Idempotency-Key"] = idem
    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request("https://api.sume.com" + path, data=data, headers=headers, method=method)
    with urllib.request.urlopen(req, timeout=60) as r:
        return json.load(r)

job = call("POST", "/v1/videos", {
    "model": "seedance-2.5", "prompt": "A drone shot over a foggy pine forest at dawn",
    "duration": 30, "resolution": "720p", "aspect_ratio": "16:9"}, "seedance-30s-001")
deadline, delay = time.time() + 1800, 3
while time.time() < deadline:
    job = call("GET", "/v1/videos/" + job["id"])
    if job["status"] in ("completed", "failed", "cancelled"):
        break
    time.sleep(delay)
    delay = min(delay * 1.5, 20)
print(job["status"], job.get("unsigned_urls"), job.get("usage"), job.get("error"))

Why a deadline

A loop with no end hides a stuck job. The script stops after 30 minutes and prints the last status, and you decide what to do next. Sume's job history keeps the record, so a later check by id is still possible.

Fetching the file

unsigned_urls point to /v1/videos/{id}/content?index=0, which redirects to the artifact. Fetching them needs your API key in the Authorization header, as covered in the unsigned URL post.

If you would rather not poll at all, pass callback_url and let Sume call you when the job ends. See what a failed job sends.

Variations

Change a few lines for other models, and keep the rest.

  • For kling-3, set duration to 15 or less.
  • For gemini-omni-flash-1.1, set duration to 10 or less.
  • For a shorter wait, use a lower resolution while you test the prompt.
  • For several jobs, run one loop per job id and share a single deadline.

Related posts

More in Developers

All Developers posts

Written by Sume