Always verify the webhook signature before processing. Never trust unverified payloads.
HMAC-SHA256(webhook_secret, "{timestamp}.{raw_json_payload}")
timestamp = value of X-Timestamp header
raw_json_payload = raw request body as string (do not parse JSON first)
Verification steps
- Read the raw request body as a string.
- Get
X-Timestamp and X-Signature from headers.
- Compute
HMAC-SHA256(secret, timestamp + "." + body).
- Compare with
X-Signature using constant-time comparison (e.g. crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python).
- Verify
X-Timestamp is within 5 minutes of current time (replay protection).
Use constant-time comparison to prevent timing attacks. Do not use == or string equality.
Example (Node.js)
const crypto = require("crypto");
function verifyWebhook(payload, timestamp, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${payload}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
}
Example (Python)
import hmac
import hashlib
def verify_webhook(payload: bytes, timestamp: str, signature: str, secret: str) -> bool:
message = f"{timestamp}.{payload.decode()}".encode()
expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)
See Setup for full implementation.