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.

5 min readSume
All posts

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.

Sume job webhook delivery (Sume webhooks docs, read 2026-10-05)
ItemValue
Eventsjob.completed, job.failed, job.canceled (terminal only)
AttemptsUp to 10 total
SpacingFixed 30 s by default, not exponential
Timeout per attempt10 s
URLPublic HTTPS only
SignatureHMAC 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

All Developers posts

Written by Sume