OpenRouter video webhook idempotency key vs Sume job_id
OpenRouter's video webhook sends X-OpenRouter-Idempotency-Key as job_id-status. Sume says use job_id alone. How to dedupe both, with a runnable verifier.

OpenRouter's video webhooks carry an X-OpenRouter-Idempotency-Key header whose value is <job_id>-<status>, so a completed and a failed event for the same job have different keys. Sume sends no such header: its docs tell you to use job_id as the idempotency key on your side, and each job reaches one terminal event, so one key per job is enough. If you are porting a receiver, change the dedupe key and keep the rest.
OpenRouter's side is from its video generation guide, read on 2026-10-03, which also lists the events video.generation.completed, failed, cancelled, and expired and an optional X-OpenRouter-Signature HMAC-SHA256. Sume's side is from its webhooks page.
Why would the status be part of the key?
OpenRouter has four event types per job, and a job can in principle be reported more than once as its state changes, so a key made of job and status identifies one transition. A receiver that dedupes on job id alone would drop a later event for the same job.
Why is job_id enough on Sume?
Sume sends terminal job events only: job.completed, job.failed, and job.canceled. There are no progress or partial deliveries. A job reaches one terminal state, so the same job_id arriving again is a retry or a redeliver of the same event.
Retries happen until up to 10 attempts are used, with a fixed delay (30 seconds by default) and a 10-second timeout per attempt. Redeliver, from the dashboard or POST /v1/jobs/{job_id}/webhook/redeliver, re-sends the real terminal event with a fresh timestamp and signature, so you will see the same job_id again by design.
| Item | OpenRouter | Sume |
|---|---|---|
| Idempotency header | X-OpenRouter-Idempotency-Key, job_id-status | None; use job_id from the body |
| Events | video.generation.completed, failed, cancelled, expired | job.completed, job.failed, job.canceled |
| Signature header | X-OpenRouter-Signature (optional), HMAC-SHA256 | x-sume-webhook-signature, sume-v1=hex |
| Timestamp header | Not listed on the page we fetched | x-sume-webhook-timestamp |
How do you verify and dedupe a Sume delivery?
Verify first, then dedupe. Sume signs <timestamp>.<raw_body> with HMAC SHA-256, and the header may carry two sume-v1= entries during a secret rotation, so accept any match. Refuse an empty secret instead of treating it as a valid key. This function does both, with an in-memory set standing in for your database:
import hashlib, hmac, json, time
SEEN: set[str] = set()
def accept(secret: str, ts: str, sig_header: str, raw: bytes, tol: int = 300):
if not secret:
raise ValueError("empty signing secret")
if abs(time.time() - int(ts)) > tol:
return None
mac = hmac.new(secret.encode(), f"{ts}.".encode() + raw, hashlib.sha256)
want = "sume-v1=" + mac.hexdigest()
ok = False
for part in sig_header.split(","):
ok |= hmac.compare_digest(part.strip(), want)
if not ok:
return None
event = json.loads(raw)
if event["job_id"] in SEEN:
return None
SEEN.add(event["job_id"])
return eventWhat breaks when you port an OpenRouter receiver?
Two things. If your code reads X-OpenRouter-Idempotency-Key, it will find nothing on Sume. And if you key on job_id-status, you can still do it on Sume by building the string from job_id and the event field, but it adds nothing because one job has one terminal event.
Store the event durably before you return a 2xx, as the Sume docs advise, and keep status polling in place for deliveries that never arrive.
What does Sume not do?
It does not send a dedupe header and it does not send non-terminal events, so you cannot use webhooks for progress. For progress, poll the job. Send test deliveries to confirm your verifier before you point it at paid jobs.
How do you test your receiver before real jobs arrive?
Sume has two tools for this. Send test posts a dummy signed webhook.test payload to a URL you type; it carries no job_id, so your dedupe code must not crash when it is missing. Redeliver re-sends a real job's terminal event with a fresh timestamp and signature, which is the right way to prove your dedupe works, since the second delivery must be acknowledged but not processed twice.
Write both cases as tests: one delivery with no job_id gets a 2xx and is ignored, and the same job_id twice produces one side effect.
What belongs in the database?
Persist the event before you return success. Sume's docs say to return any 2xx after durably storing the event, and that non-2xx responses and network errors are retried up to ten attempts. Use job_id as a unique key in the table, insert first, and treat a unique-violation as 'already seen'. An in-memory set, like the one above, loses state on restart, which is the moment retries are most likely.
Keep status polling alive next to the webhook. A failed delivery does not mean a failed job.
Sources
Related posts
More in Comparisons
- OpenRouter video unsigned_urls need an API key: Sume too
OpenRouter's unsigned_urls require your API key in the Authorization header, and so does Sume's content endpoint. Why a browser video tag fails and a safe fix.
- Pocket TTS voice cloning: a wav in, and what Sume does instead
Pocket TTS clones from a wav file you pass to --voice, with consent rules in its model card. Sume's API takes voice ids, not audio. Here is the difference.
- Replicate MCP discovery via server.json vs Sume's MCP URL
Replicate publishes /.well-known/mcp/server.json for the official MCP Registry. Sume documents one hosted MCP URL and OAuth metadata. How each client connects.
- Replicate allow_fallback_model: Nano Banana Pro vs Sume
Replicate can fall back from Nano Banana Pro to Seedream 5.0 lite and bills the fallback. Sume's allow_fallbacks is accepted but has no effect. What to do.
Written by Sume