Skip to main content
Every webhook delivery carries a signature header. It’s HMAC-SHA256 of <unix-seconds>.<raw-body> keyed by your signing secret, hex-encoded.
To verify:
  1. Parse the header: t=<unix-seconds> and v1=<hex>.
  2. Reject if |now - t| > 300 (the timestamp window — protects against replay).
  3. Compute hmac_sha256(secret, "<t>.<rawBody>") and hex-encode.
  4. Compare with the v1 value using a constant-time compare.

Node

Python

Three details that bite

  • Read the raw body. If your framework parses JSON before your handler sees it, the bytes are already different (whitespace, reordered keys) and the signature won’t verify. Always grab the raw request body for verification, then parse JSON yourself.
  • Reject old timestamps. Without the 5-minute window check, a leaked old payload is a forever-valid request. The window is the point of the timestamp.
  • Constant-time compare. timingSafeEqual (Node) or hmac.compare_digest (Python). Plain === leaks information about the secret over enough requests.

What’s signed

The signature covers exactly <t>.<rawBody>. Headers are not part of the signed payload. If you need to assert that an event arrived for a specific identity, read data.identity from the body — don’t trust the X-Inboxbase- headers without verification (though they happen to match the body in practice).