How to verify webhook requests using HMAC signatures.

Verification

Why Verify Webhooks?

Anyone who knows your webhook URL could send fake requests. Verification ensures the request actually came from Passkallet using HMAC-SHA256 signatures.

How It Works

  1. Passkallet builds the payload (event, timestamp, data) as JSON
  2. Passkallet signs the JSON string with your webhook secret using HMAC-SHA256
  3. Passkallet adds the signature to the payload body and the X-Passkallet-Signature header
  4. You extract the payload without the signature field, compute the same HMAC, and compare
Header / FieldDescription
X-Passkallet-SignatureHMAC-SHA256 signature with sha256= prefix (in HTTP header)
signatureSame signature included in the JSON body
The signature is computed over the JSON payload without the signature field. The User-Agent header is always Passkallet-Webhook/1.0.

Verification Code

javascript
const crypto = require("crypto");

function verifyWebhookSignature(req, secret) {
  const signatureHeader = req.headers["x-passkallet-signature"];

  // Remove the signature field from the body before computing HMAC
  const { signature: bodySig, ...payloadWithoutSig } = req.body;
  const payloadString = JSON.stringify(payloadWithoutSig);

  const expectedSignature =
    "sha256=" +
    crypto
      .createHmac("sha256", secret)
      .update(payloadString)
      .digest("hex");

  // Use constant-time comparison
  const isValid = crypto.timingSafeEqual(
    Buffer.from(signatureHeader),
    Buffer.from(expectedSignature)
  );

  return isValid;
}

Important Security Notes

  • Always use crypto.timingSafeEqual for comparing signatures (prevents timing attacks)
  • Keep your webhook secret secure in environment variables
  • The webhook secret is auto-generated when you configure the webhook -- you cannot set a custom one
  • If you reconfigure the webhook, a new secret is generated (the old one stops working)