Poll or webhook for Wan 3.0 30-second jobs: use both on Sume
A webhook tells you when a Wan 3.0 job ends; polling is the backup when delivery fails. Sume retries a webhook up to 10 times at 30 s spacing. How to wire both.

For 30 second Wan 3.0 jobs, use a webhook as the primary signal and polling as the safety net. Sume sends terminal events only (job.completed, job.failed, job.canceled), retries up to 10 attempts at a 30 second spacing, and the docs tell you to keep status polling for events that never arrive.
Why both
Neither mechanism is enough alone. A webhook can fail to deliver (your endpoint is down, a deploy is mid-flight, a firewall rule blocks it). Polling is reliable but wasteful if it is the only thing you do. Together they cost little and leave no job unknown.
Delivery behavior
The numbers in the table come from Sume's webhook docs, which describe delivery for generation jobs.
| Item | Value |
|---|---|
| Events | job.completed, job.failed, job.canceled (terminal only) |
| Attempts | Up to 10 total |
| Spacing | Fixed 30 s by default, not exponential |
| Timeout per attempt | 10 s |
| URL | Public HTTPS only |
| Signature | HMAC SHA 256 over timestamp.raw_body in x-sume-webhook-signature |
What the retry window covers
Ten attempts at 30 seconds is about five minutes of retries. A Wan 3.0 job that finishes while your server is down for longer than that gives you a failed delivery and a finished job. Polling closes that gap.
Wire the webhook
On POST /v1/videos, send callback_url (HTTPS) in the body, and Sume POSTs a signed event when the job is terminal. Verify the signature, store the event, and answer with any 2xx. Use job_id as your own idempotency key, because a retry can deliver the same event twice.
Wire the safety poll
Run a slow poll on every job you submitted. A reasonable pattern is to check each open job every few minutes, and to stop when it is terminal or when the webhook has already updated your record. The submit envelope gives status_url; the status response tells you terminal, and sometimes next_poll_after_seconds, which you should obey.
import asyncio, json, os, urllib.request
OPEN = {} # job_id -> True until a webhook or poll marks it terminal
def status(job_id):
req = urllib.request.Request(f"https://api.sume.com/v1/jobs/{job_id}/status",
headers={"Authorization": "Bearer " + os.environ["SUME_API_KEY"]})
return json.load(urllib.request.urlopen(req))
async def sweep():
for job_id in list(OPEN):
s = await asyncio.to_thread(status, job_id)
if s.get("terminal"):
OPEN.pop(job_id, None)
print(job_id, s.get("sume_status"))
async def main():
OPEN["job_123"] = True
await sweep()
asyncio.run(main())Cost of the safety net
A 30 second clip is a long job, and most of the wait will be silence. The webhook costs you nothing while you wait; the poll sweeps cost one request per open job per sweep. At 100 open jobs and a sweep every five minutes that is 100 reads every five minutes, which is a small load next to the render spend: 100 clips at 720p are $375.00.
Mistakes to avoid
Mistakes to avoid: do not treat a missing webhook as a failed job, do not resubmit the paid request when your server missed the call (read the job), and do not skip signature verification. The verification steps and a sample verifier are in the webhooks docs. Polling modes are in jobs and results.
Receiver design
Design the receiver for duplicates. Sume retries until your endpoint answers 2xx, so the same event can arrive more than once, and a slow endpoint (the per-attempt timeout is 10 seconds) can cause a retry even though you did process the event. Make the handler idempotent: look up the job id, and if the job is already marked done, return 200 and do nothing.
Store before you answer. The docs say to return a 2xx after you store the event durably. If you respond first and crash before writing, you lose the only push signal for that job, and only the poll will rescue you. Write the record, then respond.
Fetching the result
After a completed event, fetch the clip. The payload lists artifacts with a URL, and the video content endpoint is also available at GET /v1/videos/{jobId}/content?index=0. Copy the file to your own storage if you need it past the lifetime of the Sume URL; the docs we read do not give a retention period, so do not assume one.
Secret rotation and replay
One more detail from the docs: if a signing secret is rotating, the signature header carries one entry per live secret, newest first, comma separated. Accept the delivery when any sume-v1= entry matches, and compare each entry in constant time. Reject callbacks whose timestamp is outside a replay window; five minutes is the documented default.
Sources
Related posts
More in Developers
- Polling Gemini Omni jobs on Sume: statuses, backoff, Python example
Poll GET /v1/videos/{id} with backoff: pending, in_progress, completed, failed, cancelled. Python asyncio example for gemini-omni-flash-1.1 from 360p to 4K.
- Polling or webhook for 1,000 video jobs: count the requests
How many requests does a 1,000-job video batch need with polling or webhooks? A Python counter for poll schedules, plus Sume's per-plan read budgets.
- Port a reference-image video prompt to Sume with <IMAGE_REF_0> tags
How to move a prompt that leaned on input images to Sume: when to use image_url as the first frame, and when to name each reference with IMAGE_REF tags.
- Postman tests for the Sume image API: pass on 200 or on 202
Two pm.test checks for POST /v1/images: one expects 200 with data[].url, the other expects 202 with a job envelope. Know which one a slow request should hit.
Written by Sume