Wan 3.0 sync mode waits 30 seconds: a 30-second clip needs async
Sume's sync mode holds a request at most 30 seconds, so a 30 second Wan 3.0 clip needs async or a webhook. What the timeout means and how to avoid paying twice.

Sume's sync mode blocks the HTTP request for at most 30 seconds, so a 30 second Wan 3.0 clip should be submitted with async (or a webhook), then polled. If sync times out the job keeps running and billing, so read the status URL; never submit the same request again.
Two different thirty-second numbers
The 30 in wait_timeout_seconds and the 30 in a 30 second clip are unrelated numbers, and mixing them up is a common mistake. The first is a limit on how long Sume holds your HTTP request. The second is the duration of the video. The jobs docs say the API clamps wait_timeout_seconds to 0 to 30 and that the budget "sets a limit on how long the HTTP request blocks, not on how long the job can take".
The four modes
Each mode decides only how you learn the outcome. It does not change cost or run time.
| Mode | Request blocks? | Use for a 30 s Wan clip? |
|---|---|---|
| async (default) | No, returns 202 with status_url | Yes: poll until terminal |
| sync | Up to 30 s | No: video usually runs longer |
| subscribe | Same bounded wait as sync | No: it is an alias of sync |
| webhook | No, Sume calls you at the end | Yes: keep polling as backup |
What a timed-out sync call looks like
When the wait budget ends, the response is still a 2xx and still carries the job id. The envelope includes status_url, result_url, events_url and cancel_url, and a sync object where sync.timed_out is true. Continue with GET status_url, and obey next_poll_after_seconds when it is present. Do not submit a new paid job for the same intent.
The retry rule
If you retry the submit itself because a network error hid the response, send the same Idempotency-Key. The retry returns the original job rather than creating a second one, so you are not billed twice. A fresh key is a fresh paid job.
A polling client
Submit, then poll GET /v1/jobs/{id}/status until terminal is true, sleeping for next_poll_after_seconds when present and otherwise backing off. Fetch GET /v1/jobs/{id}/result when the job completed.
import asyncio, json, os, time, urllib.request
BASE = "https://api.sume.com"
HDR = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"], "Content-Type": "application/json"}
def call(path, body=None, key=None):
h = dict(HDR)
if key:
h["Idempotency-Key"] = key
data = json.dumps(body).encode() if body else None
req = urllib.request.Request(BASE + path, data=data, headers=h)
return json.load(urllib.request.urlopen(req))
def run():
job = call("/v1/videos", {"model": "wan-3.0", "prompt": "Harbor at dawn",
"resolution": "480p", "duration": 30}, "wan-30s-001")
delay = 5
while True:
s = call(f"/v1/jobs/{job['id']}/status")
if s.get("terminal"):
return s
time.sleep(s.get("next_poll_after_seconds") or delay)
delay = min(delay * 2, 60)
async def main():
print(await asyncio.to_thread(run))
asyncio.run(main())Short clips are not an exception
Why not sync for short clips? A 2 second 480p Wan 3.0 clip costs $0.125 and could finish inside 30 seconds; the docs still call sync the wrong tool for work that can last longer than 30 seconds, and most video work does. Write one code path (async plus polling) and use it for every length.
What a mistake costs
Watch the cost of mistakes: a 30 second clip is $1.875 at 480p, $3.75 at 720p and $7.50 at 1080p on Sume, so a duplicate submit at 1080p costs a real $7.50. The idempotency key and the status URL are what prevent it. The webhooks docs cover the push alternative, and the jobs docs cover polling.
Three ways people pay twice
Three mistakes account for most double bills with long clips. The first is treating a timed-out sync response as a failure and calling submit again with a new key. The second is a client library that retries a POST on a timeout without a stable idempotency key. The third is a cron job that submits a clip on every run without checking whether yesterday's job finished.
The fix for all three is the same: store the job id the moment you receive it, derive the idempotency key from the business intent (for example wan-ad-2026-10-05-es), and look the job up before you submit. If the job exists, poll it. If it is terminal, read the result.
Polling without hammering
Polling needs a stop condition and a back-off. Stop on completed, failed or canceled; the docs list those as terminal. Obey next_poll_after_seconds when present. Otherwise double your delay up to a ceiling such as 60 seconds. Reads have rate limits too, and the admission docs call them poll backpressure, so a tight loop that checks every second across fifty jobs invites a 429 rate_limited.
For a batch, track every job in one loop rather than one thread per job, and sleep once per round. Log the status per job so a stuck one is visible. The events_url shows the public timeline (created, queued, started, completed) when you need to see where a job is.
Sources: Jobs and results, Webhooks and the Video Router docs, for the 2 to 30 second range and the per-second rates.
Sources
Related posts
More in Developers
- Wan 3.0's watermark parameter defaults to false: and on Sume?
Alibaba's Wan 3.0 API has a watermark boolean that is off by default. What that means when you call wan-3.0 through Sume, which documents no watermark field.
- Watch a Sume Format run by phase: preparing, running, finalizing
Pass timeline: true to subscribeFormatRun or waitForRun, or call getFormatRunTimeline, to show phases during a long run. It doubles the polling rate.
- Webhook before your DB row? Insert, then submit the Sume video
A Sume job webhook can beat your own write of the job id. Insert a pending row keyed by Idempotency-Key first, then upsert by job_id when the event lands.
- Webhook events arrive out of order: Sume sends one event per job
Stripe does not guarantee event order. Sume job webhooks send terminal events only, so key on job_id, dedupe, and poll status when a callback never arrives.
Written by Sume