OpenRouter signs timestamp,body; Sume signs timestamp.body
The two webhook signatures differ by one separator and a header shape. Side-by-side table and a tested Python verifier for both, refusing empty secrets.

OpenRouter signs {timestamp},{raw_body} with a comma and sends X-OpenRouter-Signature: t=...,v1=.... Sume signs {timestamp}.{raw_body} with a dot and sends the timestamp in x-sume-webhook-timestamp and the signature in x-sume-webhook-signature: sume-v1=.... The same HMAC-SHA256 primitive sits under both, so a port to Sume fails silently if you keep the comma.
Differences that break a ported verifier
Both vendors say to verify the exact bytes received. The fields below come from OpenRouter's video guide and Sume's Webhooks page.
| Item | OpenRouter | Sume |
|---|---|---|
| Signature header | X-OpenRouter-Signature | x-sume-webhook-signature |
| Header shape | t=<timestamp>,v1=<hex> | sume-v1=<hex>, comma-separated during a rotation |
| Timestamp location | Inside the signature header | Separate x-sume-webhook-timestamp header |
| Signed string | {timestamp},{raw_body} | {timestamp}.{raw_body} |
| Replay window in the docs | 5 minutes in the sample code | 5 minutes suggested, 300 s default in the SDK |
| Secret source | Workspace settings | Dashboard Webhooks tab or GET /v1/webhooks/signing-secret |
One Python verifier for both
The code builds both headers from a test secret and checks that each verifier accepts its own scheme and rejects the other. Both functions return False for an empty secret. I ran it locally.
import hashlib
import hmac
def _hex(secret: str, signed: str) -> str:
return hmac.new(secret.encode(), signed.encode(), hashlib.sha256).hexdigest()
def verify_sume(secret: str, ts: str, header: str, body: str) -> bool:
if not secret:
return False
want = "sume-v1=" + _hex(secret, f"{ts}.{body}") # dot
return any(hmac.compare_digest(p.strip(), want) for p in header.split(","))
def verify_openrouter(secret: str, header: str, body: str) -> bool:
if not secret:
return False
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
if "t" not in parts or "v1" not in parts:
return False
want = _hex(secret, f"{parts['t']},{body}") # comma
return hmac.compare_digest(parts["v1"], want)
body = '{"id":"abc123"}'
sume_header = "sume-v1=" + _hex("s3cret", f"1780000000.{body}")
or_header = "t=1780000000,v1=" + _hex("s3cret", f"1780000000,{body}")
print(verify_sume("s3cret", "1780000000", sume_header, body)) # True
print(verify_openrouter("s3cret", or_header, body)) # True
print(verify_sume("s3cret", "1780000000", or_header, body)) # FalseRotation changes only the Sume side
For 24 hours after a rotation Sume signs each delivery with both secrets and puts two sume-v1= entries in the header, newest first. The verifier above already splits on commas and accepts any match. A verifier that compares the header for equality fails every delivery in that window, as the SDK page warns.
If a Sume signature does not verify, compare x-sume-webhook-secret-fingerprint with the fingerprint next to the secret in the dashboard. Neither side has to send the secret.
Why the separator matters
A verifier that builds timestamp + '.' + body will reject every OpenRouter delivery, and one that builds timestamp + ',' + body will reject every Sume delivery. Both fail closed, which is safe, but it looks like a broken secret. If both fail on one route, check the separator first.
Always compare with a constant-time function, and refuse an empty secret before computing anything. On the Sume side, the header may hold several comma-separated entries during the 24 hour rotation window, newest first, so accept a match on any entry.
Sources
Related posts
More in Comparisons
- OpenRouter's workspace default callback URL vs Sume's callback_url
OpenRouter has a workspace default for video callbacks. Sume's docs describe callback_url or webhook_url on each request. A Python submit wrapper.
- PII redaction on 40 hours of calls: AssemblyAI $14.40 vs Sume STT
AssemblyAI lists PII audio and text redaction as add-ons. Priced for 40 hours of calls beside Sume STT at $0.01 a minute, which has no redaction option.
- Pika plans: every tier costs 1.11 cents a credit, 0.89 on annual
Starter, Creator and Fancy price credits identically on pika.art: $10 for 900, $35 for 3,150, $95 for 8,550. What that means against Sume's per-second pricing.
- POST /v1/videos vs POST /v1/video-router/generate on Sume
Sume's docs recommend POST /v1/videos for new integrations; Video Router stays available and unchanged. Same catalog, same jobs, different request shape.
Written by Sume