fal webhook ED25519 and JWKS vs Sume's HMAC-SHA256 check

fal signs webhooks with ED25519 keys fetched from a JWKS URL. Sume signs HMAC-SHA256 over timestamp.body with a workspace secret. The two checks, side by side.

5 min readSume
All posts

A fal webhook is verified with public keys: you fetch a JSON Web Key Set from https://rest.fal.ai/.well-known/jwks.json and check an ED25519 signature over a message built from four headers and a SHA-256 of the body. A Sume webhook is verified with a shared secret: you compute HMAC-SHA256 over <timestamp>.<raw_body> and compare it with x-sume-webhook-signature. A fal verifier cannot be reused for Sume, and the reverse is also true.

The fal steps are from its Webhooks page, read on 2026-10-02. The Sume steps are from Webhooks and Run webhooks.

How does fal verify a webhook?

Four headers must be present or the request is invalid: X-Fal-Webhook-Request-Id, X-Fal-Webhook-User-Id, X-Fal-Webhook-Timestamp (Unix seconds) and X-Fal-Webhook-Signature (hex). The timestamp must be within plus or minus 300 seconds. The message is the request id, user id, timestamp and the hex SHA-256 of the raw body, joined by newlines and encoded as UTF-8.

The signature is checked against each key in the JWKS, where each key's x field is a base64url ED25519 public key. If any key verifies, the request is valid. fal says the JWKS may be cached but not longer than 24 hours because keys can change.

How does Sume verify a webhook?

Sume signs the raw JSON body with HMAC SHA-256 over the timestamp, a dot, and the raw body. The headers are x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex>. The docs call five minutes a reasonable replay window. During a secret rotation the signature header carries one sume-v1= entry per live secret, newest first, separated by commas, and you accept the delivery if any entry matches.

The secret is yours: read it on the dashboard Webhooks tab or from GET /v1/webhooks/signing-secret with an API key carrying account:read. Every delivery also carries x-sume-webhook-secret-fingerprint, so you can compare fingerprints without sending the secret anywhere.

What are the differences in one table?

Verify against the raw bytes in both cases, before any JSON parse.

fal's verification steps from its Webhooks page and Sume's from the Webhooks and Run webhooks docs, read 2026-10-02.
ItemfalSume
Key typeED25519 public keys from a JWKS URLOne HMAC-SHA256 secret per workspace
Signed messageRequest id, user id, timestamp, SHA-256 of body, newline-joined<timestamp>.<raw_body>
HeadersX-Fal-Webhook-Request-Id, -User-Id, -Timestamp, -Signaturex-sume-webhook-timestamp, x-sume-webhook-signature
Replay windowPlus or minus 300 secondsFive minutes suggested
Key refreshCache JWKS up to 24 hoursRotation: several sume-v1= entries in one header

What does a Sume verifier look like in Python?

This follows the scheme in Sume's docs and refuses an empty secret. Pass the raw request bytes, not re-serialized JSON.

import hashlib
import hmac
import time


def verify_sume_webhook(raw_body: bytes, timestamp: str, signature_header: str,
                        secret: str, tolerance: int = 300) -> bool:
    if not secret:
        raise ValueError("webhook signing secret is empty")
    try:
        ts = int(timestamp)
    except ValueError:
        return False
    if abs(time.time() - ts) > tolerance:
        return False
    digest = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body,
                      hashlib.sha256).hexdigest()
    expected = f"sume-v1={digest}".encode()
    matched = False
    for entry in signature_header.split(","):
        if hmac.compare_digest(entry.strip().encode(), expected):
            matched = True
    return matched

Sources

Related posts

More in Comparisons

All Comparisons posts

Written by Sume