callback_url on Sume /v1/videos: HTTPS only, not OpenRouter events
POST /v1/videos takes callback_url. It must be HTTPS, and Sume sends its own job.completed envelope, not OpenRouter's video.generation events.

What does callback_url do on Sume's /v1/videos?
It makes Sume POST to your URL when the job reaches a terminal state. The URL must be HTTPS. The body is Sume's standard job webhook envelope with events job.completed, job.failed or job.canceled, signed with HMAC-SHA256. It is not the video.generation.* envelope that the OpenRouter video API uses, even though the rest of /v1/videos copies OpenRouter's request shape.
That difference breaks ported code. A receiver that switches on OpenRouter's event names would never see a match, and it would quietly drop every delivery. Route on Sume's names and make unknown events loud.
Sume envelope at a glance
| Event | status field | Where the data is |
|---|---|---|
| job.completed | no error | payload.artifacts, each with a url |
| job.failed | ERROR | error object |
| job.canceled | ERROR | error object |
Build the body and route the event
The first function refuses a non-HTTPS URL before it spends a request. The second raises on an event it does not know, which is the behaviour you want while migrating.
from urllib.parse import urlparse
def video_body(prompt: str, callback_url: str) -> dict:
if urlparse(callback_url).scheme != "https":
raise ValueError("callback_url must be https")
return {
"model": "wan-3.0", "prompt": prompt, "duration": 10,
"resolution": "720p", "callback_url": callback_url,
}
def route(event: dict):
kind = event["event"]
if kind == "job.completed":
return [a["url"] for a in event["payload"]["artifacts"]]
if kind in ("job.failed", "job.canceled"):
return event.get("error")
raise ValueError(f"unexpected event {kind!r}")
print(video_body("A kettle boiling at dawn", "https://example.com/hooks/sume")["callback_url"])
print(route({"event": "job.completed", "job_id": "job_1",
"payload": {"artifacts": [{"url": "https://media.sume.com/a.mp4"}]}}))
print(route({"event": "job.failed", "status": "ERROR", "error": {"code": "x"}}))Before you go live
Verify the signature on the raw body before you route, using the x-sume-webhook-timestamp and x-sume-webhook-signature headers and your workspace secret. Deduplicate on job_id, since a delivery can repeat up to ten times. Keep polling polling_url as a fallback for a delivery that never arrives.
The submit response is a 202 with id, polling_url and status, and the states are pending, in_progress, completed, failed and cancelled. Store id the moment you get it, so the callback has something to match. Your test plan is short: send a test delivery to your endpoint from the dashboard or with POST /v1/webhooks/test-deliveries, confirm your verifier accepts it, and note that the route function above raises on webhook.test, so answer test events before routing if you want them to pass quietly.
Failed and canceled deliveries are the ones teams forget. They carry status: "ERROR" and an error object instead of artifacts, so a receiver that reads payload.artifacts unconditionally will raise a KeyError on exactly the jobs that need attention. Branch on the event name first, as route does, and write the error somewhere a person will see it. Check the usage record for what a failed job was charged, and make sure the order that wanted the clip has an owner.
callback_url is the /v1/videos field for this push, and webhook_url is the field on Sume's other generation routes.
Sources
Related posts
More in Developers
- Verify a Sume TTS transcript_receipt SHA-256 yourself in Python
Recompute submitted_transcript_sha256 from your script with NFC and LF canonicalization and compare it to the transcript_receipt on a finished Sume TTS job.
- 4K vertical Short in Timeline: the 2160 cap and 1214x2160
Timeline output width and height top out at 2160 and must be even, so 2160x3840 is refused. What the largest 9:16 frame is and whether a Short needs it.
- Voice one script in six languages: Sume TTS loop and 409 guard
MAI-Voice-2.1 sells one voice across 23 languages. On Sume, set language per line, handle the 409 voice-language guard and price a six-language batch.
- Voiceover for shorts: one sentence per shot using TTS sentence slices
Ask TTS for timestamps.words and sentence segmentation and Sume returns gapless per-sentence slices, so each shot in a short gets its own audio file.
Written by Sume