The signing secret returned by
POST /webhooks/subscriptions (and by rotation) looks like whsec_.... That prefixed value is base64 — decode it before using it as the HMAC key.Verification Steps
- Read
webhook-id,webhook-timestamp, andwebhook-signaturefrom the request headers. - Build the signed content by concatenating the message ID, the timestamp, and the raw request body, separated by dots:
{webhook-id}.{webhook-timestamp}.{body}. Use the exact raw bytes/string you received — do not parse and re-serialize the JSON first, since that can change field order or whitespace and break the signature. - Base64-decode the signing secret (after stripping the
whsec_prefix) and compute the HMAC-SHA256 digest of the signed content using it as the key. - Base64-encode the digest and compare it against each
v1value inwebhook-signatureusing a constant-time comparison. The header can contain multiple space-separated signatures (for example during secret rotation); accept the delivery if any one of them matches. - Reject the request if no signature matches, or if
webhook-timestampis older than 5 minutes (or further in the future than your allowed clock skew), to prevent replay attacks. - Only after verification succeeds, parse the JSON body and process the event.
2xx status once the event has been safely accepted (even if asynchronous processing is still pending) — see Delivery Semantics for retry behaviour.