Early access — use code HOOKSENSEWELCOME for 1 month free
7 min read

HMAC Webhook Signature Verification: Stripe, GitHub, Shopify

Learn how HMAC webhook signature verification works. Step-by-step guide for Stripe, GitHub, and Shopify with code examples.

SecurityHMACWebhooks
O

Ozer

Developer & Founder of HookSense

Webhook signature verification is a critical security practice. Without it, anyone who knows your webhook URL can send fake events to your server. HMAC (Hash-based Message Authentication Code) signatures ensure that the request genuinely came from the expected provider and hasn't been tampered with.

How HMAC Signatures Work

  1. Shared secret: You and the provider share a secret key (e.g., whsec_... for Stripe)
  2. Signing: The provider computes HMAC-SHA256(secret, requestBody) and includes it in a header
  3. Verification: Your server computes the same HMAC and compares it with the header value
  4. Match: If they match, the request is authentic. If not, reject it.

Stripe Signature Verification

Stripe uses the Stripe-Signature header with a timestamp-based scheme:

const stripe = require('stripe')('sk_...');
const endpointSecret = 'whsec_...';

app.post('/webhooks/stripe', (req, res) => {
  const sig = req.headers['stripe-signature'];

  try {
    const event = stripe.webhooks.constructEvent(
      req.body,   // raw body
      sig,
      endpointSecret
    );
    // Process event...
    res.status(200).send();
  } catch (err) {
    console.error('Signature verification failed:', err.message);
    res.status(400).send('Invalid signature');
  }
});

Important: You must use the raw request body (not parsed JSON) for signature verification. If you use Express, configure the route to receive the raw body.

GitHub Signature Verification

GitHub uses the X-Hub-Signature-256 header:

const crypto = require('crypto');

function verifyGitHubSignature(payload, signature, secret) {
  const expected = 'sha256=' +
    crypto.createHmac('sha256', secret)
      .update(payload)
      .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

app.post('/webhooks/github', (req, res) => {
  const sig = req.headers['x-hub-signature-256'];
  if (!verifyGitHubSignature(req.rawBody, sig, SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  // Process event...
  res.status(200).send();
});

Shopify Signature Verification

Shopify uses the X-Shopify-Hmac-SHA256 header with a Base64-encoded HMAC:

function verifyShopifySignature(body, signature, secret) {
  const computed = crypto.createHmac('sha256', secret)
    .update(body, 'utf8')
    .digest('base64');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(computed)
  );
}

Common Pitfalls

  • Parsed vs raw body: Always verify against the raw request body, not the parsed JSON object. Parsing can change whitespace or key ordering.
  • Timing attacks: Use crypto.timingSafeEqual() instead of === to prevent timing-based attacks.
  • Secret rotation: When rotating secrets, temporarily accept both the old and new secret.
  • Replay attacks: Stripe includes a timestamp — reject requests older than 5 minutes.

Automatic Verification with HookSense

During development, you can use HookSense to automatically verify webhook signatures without writing any code:

  1. Open your endpoint settings
  2. Click the Shield icon
  3. Enter your webhook signing secret
  4. Select the provider (or use auto-detect)
  5. Every incoming request will show a "Verified" or "Invalid" badge

This is especially useful during development when you want to confirm your webhook provider is sending valid signatures before you implement verification in your own code.

HookSense supports Stripe, GitHub, Shopify, and custom HMAC-SHA256 signatures. Try it free.

Related

Try HookSense Free

Inspect, debug, and replay webhooks in real-time. No credit card required.

Get Started Free