Webhooks are in beta. Event types and payload shapes may still
change. We’ll email orgs with active endpoints before any breaking
change.
The contract
The signature is HMAC-SHA256 over the literal string{webhook-id}.{webhook-timestamp}.{raw-body} using your whsec_…
secret as the HMAC key. We follow the canonical
Standard Webhooks spec exactly:
the prefix whsec_ is stripped and the remainder is base64-decoded to
produce the raw key bytes. The Svix standardwebhooks SDK does this
for you in one line.
Headers we send on every POST:
Receivers SHOULD:
- Reject signatures whose timestamp is more than ±5 minutes from their wall clock (replay protection).
- Compare with a constant-time byte equality (
timingSafeEqualin Node,hmac.compare_digestin Python, etc.). A==compare leaks the signature byte-by-byte. - Accept any
v1,<sig>entry as a match — during a 24h secret rotation we send two. - Dedupe on
webhook-idwith at least 5 minutes of retention.
One-line option (Node)
standardwebhooks SDK strips the prefix and base64-decodes the
remainder for you, so it’s byte-for-byte compatible with the snippets
below.