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.

5 min readSume
All posts

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.

Webhook signing, read 2026-10-02
DetailReplicateSume
Signature headerwebhook-signaturex-sume-webhook-signature
Timestamp headerwebhook-timestamp (seconds)x-sume-webhook-timestamp
Message id headerwebhook-idNone in the signature; use job_id as the dedupe key
Signed stringwebhook_id.timestamp.bodytimestamp.raw_body
Secret formatwhsec_ plus base64Workspace-derived secret, env name SUME_COM_WEBHOOK_SIGNING_SECRET
Multiple signaturesSpace-delimited listComma-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 on job_id instead.
  • 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

All Comparisons posts

Written by Sume