HMAC signing
Every outbound webhook carries an X-Webhook-Signature header. The receiver recomputes the HMAC over the timestamp + body with the shared secret and compares in constant time. Stripe and Svix use the same shape; if you've seen one you've seen this one.
Header format
X-Webhook-Signature: t=1717000000,v1=cafebabe...t— Unix timestamp the signature was generated at.v1— hex-encoded HMAC-SHA256 over the string"{t}.{raw_body}".
Verifying
The signature verifier is a 30-line helper. Every SDK ships one — use the helper rather than rolling your own; constant-time comparison and timestamp-window checks are easy to get wrong.
import { verifySignature } from "@philiprehberger/webhook-relay-client";
verifySignature(
secret, // whsec_...
rawBody, // exact bytes received
request.headers.get("x-webhook-signature"),
300, // tolerance seconds (default)
);use PhilipRehberger\WebhookRelayClient\Signer;
Signer::verify(
$secret,
file_get_contents('php://input'),
$_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '',
);from philiprehberger_webhook_relay_client import verify_signature
verify_signature(secret, request.body, request.headers.get("X-Webhook-Signature"))webhookrelay.VerifySignature(
secret,
string(body),
r.Header.Get("X-Webhook-Signature"),
0, // default tolerance: 5 minutes
)Two failure modes that bite
Re-encoding the body
The signature is computed over the bytes that hit the wire. If your framework JSON-parses the body and you re-serialize it before verifying, key order or whitespace can change and the signature will fail. Always verify against the raw request body.
Skipping the timestamp window
A signature on a body is replayable forever without a timestamp check. The SDK helpers reject signatures older than 5 minutes by default — keep that on. If you need a longer window, raise it explicitly; don't turn it off.
Rotating secrets
Calling POST /v1/subscriptions/{id}/rotate-secretgenerates a new secret and keeps the old one valid for 48 hours. During the window, every outbound delivery is signed with the new secret. Update your receiver to accept both during the grace, then drop the old one.