OpenRouter video callbacks vs Sume job webhooks: what differs
OpenRouter video callback_url reports completed, failed, cancelled, expired. Sume job webhooks send completed, failed, canceled, signed with HMAC-SHA256.

Same idea, different edges
OpenRouter's video API is asynchronous: you POST to /api/v1/videos, get a polling_url, and check back; its guide suggests polling every 30 seconds and lists the statuses pending, in_progress, completed and failed. It also supports a callback_url that is notified when a job finishes, fails, is cancelled or expires.
Sume's /v1/videos route is shaped like that API and accepts callback_url too. If you are porting a callback handler, the differences are in the event set and the security details.
Comparison (read 2026-10-05)
| Topic | OpenRouter video | Sume job webhooks |
|---|---|---|
| Events | completed, failed, cancelled, expired | job.completed, job.failed, job.canceled |
| Spelling of cancel | cancelled | canceled (job events), cancelled on /v1/videos polling |
| Signature | See the OpenRouter page | HMAC-SHA256 over timestamp.raw_body in x-sume-webhook-signature |
| Replay window | See the OpenRouter page | 5 minutes on the timestamp |
| Retries | See the OpenRouter page | 10 attempts, 30 s apart, 10 s timeout each |
A verifier that refuses an empty secret
Sume signs sume-v1= plus the hex HMAC-SHA256 of the timestamp, a dot, and the raw body, so verify before parsing JSON. The header can carry several comma-separated entries while a secret is being rotated; accept any match.
import crypto from 'node:crypto';
export function verify(rawBody, headers, secret, now = Date.now()) {
if (!secret) throw new Error('empty webhook secret');
const ts = headers['x-sume-webhook-timestamp'];
const sigs = String(headers['x-sume-webhook-signature'] || '').split(',');
if (!ts || Math.abs(now / 1000 - Number(ts)) > 300) return false;
const want = 'sume-v1=' + crypto.createHmac('sha256', secret)
.update(ts + '.' + rawBody).digest('hex');
return sigs.some((s) => {
const a = Buffer.from(s.trim()), b = Buffer.from(want);
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
}Porting notes
An expired event has no counterpart in the Sume job webhook list, so a handler that waits for it needs no replacement: poll the polling_url or status_url for anything that never terminates. Dedupe on job_id, since Sume retries until you answer 2xx.
Sources
Related posts
More in Developers
- Submit 60 Gemini Omni 4K jobs on a Pro plan: pace in waves of 24
A Pro workspace holds 24 accepted generation jobs (4 running, 20 queued). Pace 60 Omni 4K submits in waves with asyncio and retry queue_full safely.
- Perl HTTP::Tiny: submit a MiniMax H3 video job and save it
A 23-line Perl script with core modules only: POST /v1/videos for minimax-h3, poll, download. A 5-second 768p clip is $0.375; 480p is $0.3125.
- PHP cURL: save a Sume-generated image to disk with CURLOPT_FILE
Two cURL calls in plain PHP: POST to /v1/images, read data[0].url on a 200, then stream the file to disk with CURLOPT_FILE. Handles 202 and a missing key.
- PHP cURL: submit a Seedance 2.5 video job, poll it, save the MP4
PHP with only ext-curl: POST /v1/videos for seedance-2.5, poll polling_url until completed, then download content?index=0; a 5-second 480p test costs $1.34.
Written by Sume