Replicate webhook signature vs Sume: webhook-id or sume-v1?
Replicate signs id.timestamp.body with a whsec_ key; Sume signs timestamp.body as sume-v1. Compare headers, secrets and a Python verifier for the Sume side.

How do Replicate and Sume sign webhooks differently?
Both use HMAC with SHA-256, but they sign different strings and send different headers. Replicate signs webhook_id.timestamp.body and sends three headers; Sume signs timestamp.raw_body and sends a timestamp header plus one sume-v1= signature header.
If you are porting a receiver from Replicate to Sume, the verification skeleton survives (raw body, timestamp check, constant-time compare), but the signed string, the secret format and the header names all change. A verifier written for one will reject every delivery from the other.
What does Replicate send and sign?
Per Replicate's verification page, each webhook carries webhook-id (a unique message identifier), webhook-timestamp (seconds since epoch) and webhook-signature (a base64-encoded list of signatures, space delimited). The signed content is ${webhook_id}.${webhook_timestamp}.${body}.
The signing key comes from GET https://api.replicate.com/v1/webhooks/default/secret, which returns a key formatted as whsec_ followed by the base64 portion used for the HMAC. Replicate advises caching the key locally and comparing the timestamp against your own clock to prevent replay, without stating an exact tolerance on that page.
What does Sume send and sign?
Sume signs the raw JSON body with HMAC SHA-256 over <timestamp>.<raw_body> and sends x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex>. The default replay window in the SDK is 300 seconds. See Webhooks and Verifying webhooks.
The secret is derived per workspace. Read it on the dashboard Webhooks tab or from GET /v1/webhooks/signing-secret with a key carrying account:read. Every delivery also carries x-sume-webhook-secret-fingerprint, so you can compare fingerprints in a support ticket without pasting the secret.
During a rotation, Sume signs with both secrets for 24 hours and sends the signatures comma-separated, newest first. Replicate documents a space-delimited list in its own header; Sume uses a comma list inside one. Do not reuse a split routine.
Side by side
Everything below is from the two vendors' own pages.
| Detail | Replicate | Sume |
|---|---|---|
| Signature header | webhook-signature | x-sume-webhook-signature |
| Timestamp header | webhook-timestamp (seconds) | x-sume-webhook-timestamp |
| Message id header | webhook-id | None in the signature; use job_id as the dedupe key |
| Signed string | webhook_id.timestamp.body | timestamp.raw_body |
| Secret format | whsec_ plus base64 | Workspace-derived secret, env name SUME_COM_WEBHOOK_SIGNING_SECRET |
| Multiple signatures | Space-delimited list | Comma-separated sume-v1= entries during a 24-hour rotation window |
How do I verify a Sume delivery in Python?
This verifier refuses an empty secret, checks the replay window first, and compares every entry in the header so a rotation window works. Pass the raw bytes of the request body, never a re-serialized object.
import hashlib, hmac, time
def verify(raw: bytes, timestamp: str, header: str, secret: str,
tolerance: int = 300) -> bool:
if not secret:
return False
try:
ts = int(timestamp)
except ValueError:
return False
if abs(time.time() - ts) > tolerance:
return False
mac = hmac.new(secret.encode(), f"{ts}.".encode() + raw, hashlib.sha256)
expected = "sume-v1=" + mac.hexdigest()
ok = False
for entry in header.split(","):
if hmac.compare_digest(entry.strip(), expected):
ok = True
return ok
if __name__ == "__main__":
body = b'{"event":"job.completed"}'
ts = str(int(time.time()))
sig = "sume-v1=" + hmac.new(b"s3cret", ts.encode() + b"." + body, hashlib.sha256).hexdigest()
print(verify(body, ts, sig, "s3cret"), verify(body, ts, sig, ""))What else changes when you port a receiver?
Reply fast with a 2xx after storing the event. Sume retries non-2xx and network errors up to 10 attempts total, 30 seconds apart by default, with a 10 second timeout per attempt, so a slow handler burns the budget. Sume sends only terminal job events (job.completed, job.failed, job.canceled), so there is nothing to filter. Keep status polling as a backup for deliveries that never arrive.
- Drop any code that reads
webhook-id; dedupe onjob_idinstead. - Stop splitting the signature header on spaces; split on commas and look for
sume-v1=. - Use the dashboard fingerprint to confirm you hold the right secret before debugging the body.
Sources
Related posts
More in Comparisons
- Resemble AI vs Sume: voice, detection and watermarking vs video
Resemble AI covers TTS, speech-to-speech, deepfake detection and watermarking. Sume makes media but ships no detection API. What each covers.
- Respeecher Space at $2 an hour vs Sume async text to speech
Respeecher Space is a real-time TTS API for voice agents at $2 an hour. Sume TTS is async and per character. Which fits narration, and which fits live voice.
- Rev AI Reverb $0.20 per hour vs Sume STT $0.60 per hour
Rev AI lists Reverb at $0.20 an hour, a 15-second minimum, and Whisper Large at $0.005 a minute. Sume STT is $0.01 a minute, reserving one minute by default.
- Runway lists 18 video model ids: which nine does Sume have?
Of the 18 video ids on Runway's models page, nine match a Sume id: four Seedance rows, MiniMax H3 and Max, Wan 3.0, Grok Imagine, Gemini Omni Flash 1.1.
Written by Sume