Skip to main content
Always verify the webhook signature before processing. Never trust unverified payloads.

Signature format

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

  1. Read the raw request body as a string.
  2. Get X-Timestamp and X-Signature from headers.
  3. Compute HMAC-SHA256(secret, timestamp + "." + body).
  4. Compare with X-Signature using constant-time comparison (e.g. crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python).
  5. 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.