Seedance 2.5 30-second job: set a 20-minute client deadline in Python
A 30-second Seedance 2.5 job outlives any HTTP timeout. Submit async to Sume, poll with next_poll_after_seconds, and set the deadline in your client.

Set the long timeout in your own client, not on the HTTP request. Submit the Seedance 2.5 job with mode: "async", then poll GET /v1/jobs/{id}/status until terminal is true, with a deadline of about 20 minutes that your code enforces.
Sume's docs state the reason plainly: wait_timeout_seconds is clamped to 0-30 and limits how long the request blocks, not how long the job can take. Video jobs usually do not finish inside that window, so the loop below lives in your process.
Why 20 minutes
The TypeScript SDK's waitForJob defaults to 20 minutes for video and avatar-video jobs, and the docs call that reasonable for video. It is a ceiling, not an estimate. Most clips finish sooner, but a queued job on a busy workspace waits for a concurrency slot before it even starts.
A deadline that expires does not cancel anything. The job continues and still bills, so store the job id and read it later, or cancel it before generation starts.
| Limit | Value | Where it applies |
|---|---|---|
| Seedance 2.5 clip length | 4-30 seconds | Request validation |
| sync/subscribe wait | 0-30 seconds, clamped | HTTP request on submit |
| Poll cadence | next_poll_after_seconds when present | Your loop |
| SDK waitForJob default | 20 minutes | Client side |
The loop
Read data.next_poll_after_seconds from the status response and use it as the sleep when it is not null. The fallback is a doubling backoff capped at 30 seconds.
import os, time, requests
BASE = "https://api.sume.com"
H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
r = requests.post(f"{BASE}/v1/video-router/generate", timeout=30,
headers={**H, "Idempotency-Key": "mug-orbit-001"},
json={"model": "seedance-2.5", "prompt": "Slow orbit around a mug",
"duration": 30, "resolution": "720p", "mode": "async"})
r.raise_for_status()
job_id = r.json()["data"]["request_id"]
deadline, delay = time.time() + 20 * 60, 2
while time.time() < deadline:
s = requests.get(f"{BASE}/v1/jobs/{job_id}/status", headers=H, timeout=30)
s.raise_for_status()
d = s.json()["data"]
if d["terminal"]:
break
time.sleep(d["next_poll_after_seconds"] or delay)
delay = min(delay * 2, 30)
else:
raise SystemExit(f"still running, keep {job_id} and look again later")
print(d["sume_status"], d["result_ready"])After the loop ends
sume_status is completed, failed, or canceled. Only a completed job has a result; for the others GET /v1/jobs/{id}/result answers 409 job_not_completed, so read the failure from GET /v1/jobs/{id} instead.
If you use TypeScript, waitForJob from @sume-com/sdk is this same loop with the 20-minute default.
Sources
Related posts
More in Developers
- Seedance 2.5 API in Python: a 30-second clip with an audio reference
Submit a 30-second Seedance 2.5 job on Sume with requests: an image and an audio reference in input_references, polling, download, and limits to check.
- Seedance 2.5 estimate vs usage.cost: reconcile a job in Node
Compute the Seedance 2.5 price from width, height and seconds, submit one job to Sume, and compare it with usage.cost on the poll. A runnable Node 18 script.
- Seedance 2.5 reference limits: 30 images on the vendor page, 9 on Sume
ByteDance lists up to 30 images, 10 video and 10 audio references for Seedance 2.5. Sume's Video Router validates fewer: 9, 3, 3, and 12 in total. Read both.
- Semantic Kernel .NET defaults to gpt-image-1: it fails Oct 23
The .NET OpenAI connector in Semantic Kernel defaults to gpt-image-1, which OpenAI removes Oct 23, 2026. What breaks, and a C# call to Sume that does not.
Written by Sume