Poll a 30-second Seedance 2.5 job in Python with a 20-minute deadline
Submit a 30 s Seedance 2.5 video to Sume, poll GET /v1/videos/{id} with capped backoff, and stop at a deadline without losing the job. Runnable stdlib Python.

To poll a long AI video job, submit it once, keep the returned job id, and call GET /v1/videos/{id} on a growing delay until the status is completed, failed or cancelled. On Sume a 30-second Seedance 2.5 request is one POST to /v1/videos with model seedance-2.5 and duration 30, and the loop below stops after 20 minutes without cancelling the job.
Why a deadline, and why not a longer HTTP wait
Seedance 2.5 is in the Sume catalog at 4 to 30 seconds and 480p, 720p or 1080p. A clip at the top of that range is minutes of work, not seconds. Sume answers the submit with 202 and a polling_url immediately, so no HTTP request has to stay open. The wait lives in your process, and your process owns the deadline.
Hitting your own deadline is not a failure of the job. The Sume jobs guide is explicit that a client-side timeout does not cancel anything: the job keeps running and keeps billing. So the loop prints the job id and exits instead of submitting again.
A good deadline is a number you can defend. Twenty minutes is the figure the Sume TypeScript SDK uses as the default wait for generation jobs, because video and avatar-video jobs usually run for minutes. Pick your own, put it in config, and log it next to the job id so a person reading the log knows the wait was cut by policy and not by the job.
- Submit once, with an Idempotency-Key, so a retried submit returns the original job.
- Poll the polling_url from the 202 response. The shape follows the OpenRouter video guide: pending, in_progress, completed, failed.
- Stop on any terminal status. Sume spells the canceled state cancelled on this route.
- Grow the delay and cap it, so a 20-minute wait costs a few dozen reads.
Status names on the wire
Sume jobs have five internal statuses. The /v1/videos route maps them to the OpenRouter-shaped names, and the same job is readable at /v1/jobs/{id}/status in the normal Sume envelope.
| Sume job.status | /v1/videos status | Terminal |
|---|---|---|
| queued | pending | No |
| processing | in_progress | No |
| completed | completed | Yes |
| failed | failed | Yes |
| canceled | cancelled | Yes |
The script
Set SUME_API_KEY, then run it. It uses only the standard library.
import json, os, time, urllib.request, urllib.error
BASE = "https://api.sume.com"
def call(method, path, body=None, headers=None):
req = urllib.request.Request(
BASE + path, method=method,
data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": "Bearer " + os.environ["SUME_API_KEY"],
"Content-Type": "application/json", **(headers or {})})
try:
with urllib.request.urlopen(req, timeout=30) as r:
return r.status, json.loads(r.read() or b"{}")
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read() or b"{}")
body = {"model": "seedance-2.5", "duration": 30, "resolution": "1080p",
"aspect_ratio": "16:9",
"prompt": "Slow dolly through a night market in the rain, neon signs"}
code, job = call("POST", "/v1/videos", body, {"Idempotency-Key": "market-30s-001"})
assert code == 202, job
job_id, wait, deadline = job["id"], 2.0, time.time() + 20 * 60
while time.time() < deadline:
code, job = call("GET", "/v1/videos/" + job_id)
if code == 200 and job["status"] in ("completed", "failed", "cancelled"):
break
time.sleep(wait)
wait = min(wait * 1.5, 30)
else:
raise SystemExit("deadline reached, job still running: " + job_id)
print(job["status"], job.get("unsigned_urls") or job.get("error"))
What each poll costs you
A poll is a GET, so it spends the read budget, not the write budget. On the Free plan that is 4,800 reads a minute against 120 writes, according to the Sume authentication page. The loop above makes about 5 reads in its first minute and 2 a minute at the 30-second cap. Rate limits are not the thing to worry about for one job; they matter when you run hundreds, which the batch posts below cover.
If a poll returns 429, the loop keeps going because it only breaks on a 200 with a terminal status. For a hardened version, honour the retry-after header, which MDN defines as either a number of seconds or an HTTP date.
Backoff has a second benefit beyond rate limits: it keeps logs readable. A fixed one-second poll writes 1,200 identical lines for a 20-minute wait, while the 2, 3, 4.5 and up to 30 second schedule writes about 45. If you log each poll, you will want the second.
Gotchas
- Do not re-POST on timeout. A second POST without the same Idempotency-Key is a second paid job.
- Keep the job id somewhere durable. If the process dies at minute 12, GET /v1/videos/{id} still works at minute 13.
- Other vendors poll too. Google's Gemini Omni docs poll a file state for large videos, so the loop shape is portable.
- OpenAI's deprecations page lists the Videos API and Sora 2 aliases as removed on September 24, 2026. A client written to the OpenRouter-shaped wire has nothing to unlearn if you move providers.
Sources
Related posts
More in Developers
- 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.
- Poll a Seedance 2.5 job in Go: submit, wait 30 s, check status
A Go program that submits a 30-second Seedance 2.5 job to Sume, polls the polling_url every 30 seconds for up to 20 minutes, and prints the result URL.
- Poll a Seedance 2.5 job in TypeScript: deadline and failed states
A TypeScript submit-poll-download loop for a 30-second Seedance 2.5 render on Sume: the 30 s interval, a 20-minute deadline, and the three end states.
- Poll an H3 Max video job in Python: backoff, terminal, result
Submit minimax-h3-max to the Video Router, poll GET /v1/jobs/:id/status with backoff until terminal, then fetch the result. A runnable 25-line Python script.
Written by Sume