Use this file to discover all available pages before exploring further.
Every webhook delivery carries a signature header. Verify it.
Refusing to verify is the single most common way customers ship
exploitable webhook receivers; we do not consider an unverified
endpoint a working integration.
The signing secret is the whsec_* plaintext value you received once
when creating the endpoint. Rotate via
POST /v1/webhooks/{id}/rotate-secret — the new plaintext is shown
once; the old secret keeps verifying for 24 hours so deploys can roll.
Comparison uses a constant-time equality check (crypto.timingSafeEqual or equivalent).
Use the raw bytes of the request body. Re-serialising via
JSON.stringify(JSON.parse(body)) will corrupt the signature on any
payload with non-canonical key order.
Don’t re-serialise the body. Frameworks that parse JSON before your
handler runs will hand you a Ruby Hash / Python dict / JS object whose
JSON.stringify representation does not match the bytes we signed. Use
the raw-body middleware (express.raw, Request.body(),
request.body.read).
Don’t compare with === / == / .equal?. A naive equality check leaks
signature bytes through timing. Use timingSafeEqual, hmac.compare_digest,
hmac.Equal, or Rack::Utils.secure_compare.
Don’t trust req.body.timestamp over the header t. Some CDNs rewrite or
strip request bodies. The t in the header is what we signed; verify against
that, not against the JSON body.
The previous secret keeps verifying for 24 hours, so a rolling
deploy that updates secrets across replicas does not drop deliveries.
After 24 hours, only the new secret is valid.