Headers and signed input
The signed input is the timestamp, one literal period, and the exact raw request body:
Verification sequence
1
Preserve the raw body
Read the bytes or exact string before a framework parses and serializes JSON.
2
Require both headers
Reject a request with a missing or malformed timestamp or signature.
3
Check freshness
Parse epoch seconds and apply your receiver’s replay window with a small clock-skew allowance.
4
Calculate HMAC
Sign
timestamp + "." + rawBody with HMAC-SHA256 and format the hex result with the v1= prefix.5
Compare safely
Compare equal-length byte sequences with a constant-time comparison.
6
Parse and accept
Only after verification, parse JSON, deduplicate, and durably store or enqueue the event.
Secret lifecycle
The create-webhook response returns the signing secret once. List and get responses never return it. Store one endpoint secret in a server-side secret manager and redact it from logs and exceptions. When rotating a secret through an endpoint update, coordinate receiver deployment so the accepted-secret window is explicit.Common verification failures
- JSON was parsed and serialized before HMAC calculation.
- A newline or character encoding changed.
- The wrong endpoint secret was selected.
- The
v1=prefix was omitted during comparison. - Seconds were interpreted as milliseconds.
- A reverse proxy consumed or modified the request body.
Implementation example
See a Node.js constant-time verification example.