Replicate's default webhook secret versus Sume's signing secret
Replicate's secret is at /v1/webhooks/default/secret and it signs id.timestamp.body. Sume's is at /v1/webhooks/signing-secret and signs timestamp.raw_body.

Both services sign webhooks with HMAC-SHA256, but the signed strings and the secret endpoints differ, so a verifier for one will not pass the other. Replicate signs ${id}.${timestamp}.${body} with the key that follows whsec_, and you fetch the secret from GET https://api.replicate.com/v1/webhooks/default/secret. Sume signs <timestamp>.<raw_body>, prefixes the hex with sume-v1=, and you fetch the secret from GET /v1/webhooks/signing-secret with an account:read key, or reveal it on the dashboard Webhooks tab.
Side by side
| Item | Replicate | Sume |
|---|---|---|
| Headers | webhook-id, webhook-timestamp, webhook-signature | x-sume-webhook-timestamp, x-sume-webhook-signature, x-sume-webhook-secret-fingerprint |
| Signed string | ${id}.${timestamp}.${body} | <timestamp>.<raw_body> |
| Key | Secret after the whsec_ prefix | The workspace signing secret |
| Secret endpoint | GET /v1/webhooks/default/secret | GET /v1/webhooks/signing-secret (account:read) |
| Signature format | As the header gives it | sume-v1=<hex>, several entries during rotation |
| Replay window | Not covered here | 300 seconds recommended |
Two Sume details that bite
First, rotation: during a secret rotation the signature header carries one sume-v1= entry for each live secret, newest first, comma-separated. Accept the delivery if any entry matches. Second, always verify the raw body bytes before you parse JSON; re-serialized JSON will not match. If a signature fails, compare x-sume-webhook-secret-fingerprint with the fingerprint next to the secret in the dashboard, which avoids sending the secret itself.
Job webhooks and run webhooks share one secret and one signature scheme, so one verifier covers both.
A Sume verifier in Python
This refuses an empty secret, checks the 300-second tolerance, and compares every entry in constant time. The demo signs a body itself so you can run it.
import asyncio, hashlib, hmac, time
def verify(raw: bytes, ts: str, header: str, secret: str, tol: int = 300) -> bool:
if not secret:
raise ValueError("empty signing secret")
try:
t = int(ts)
except ValueError:
return False
if abs(time.time() - t) > tol:
return False
digest = hmac.new(secret.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
want = f"sume-v1={digest}"
ok = False
for entry in header.split(","):
ok |= hmac.compare_digest(entry.strip(), want)
return ok
async def main():
body, secret, ts = b'{"event":"job.completed"}', "test-secret", str(int(time.time()))
sig = hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
print(verify(body, ts, f"sume-v1={sig}", secret))
asyncio.run(main())Migrating a receiver
- Branch on the header names, not on the URL, if one endpoint receives both vendors.
- Keep each secret under its own name. Sume's docs use
SUME_COM_WEBHOOK_SIGNING_SECRET. - Send a test delivery with
POST /v1/webhooks/test-deliveriesbefore you go live.
Testing a receiver
Sume has a test endpoint, POST /v1/webhooks/test-deliveries, so you can see a signed delivery arrive before a real job finishes. For a real job that you missed, POST /v1/jobs/{job_id}/webhook/redeliver with jobs:write sends it again, and a redelivery does not consume one of the attempts. The delivery policy is up to 10 attempts, a fixed 30 second delay, a 10 second timeout, public HTTPS only, and redirects are not followed.
Replicate's docs say inputs and outputs are deleted after one hour. Treat that as a reason to store results as soon as you verify the delivery, whichever vendor sends it.
Sources
Related posts
More in Comparisons
- Retouch 200 product photos: Image API, Agent Completion or Format?
For 200 identical retouches use POST /v1/images: fixed model, price per image, plan queue limits. Use an Agent Completion only if each photo needs judgement.
- Rime Mist v3 vs Coda vs Sume TTS: cost for 1M characters
Rime lists Mist v3 at $0.03 and Coda at $0.05 per 1K characters. Sume TTS 1.0 is $0.0475. Here is the cost of 1M characters and a 20,000-character call.
- Runway: no RPM limit but a daily cap; Sume answers 429 two ways
Runway documents no per-minute limit but a rolling 24-hour generation cap. Sume returns 429 queue_full or rate_limited, with ratelimit headers.
- Runway dev MCP login needs Claude Code 2.1.186+; Sume: mcp login
Runway's dev MCP logs in with OAuth in the terminal on Claude Code 2.1.186+, --no-browser from 2.1.191. Sume uses the same two commands at mcp.sume.com.
Written by Sume