Blog / Security

The three essential webhook security measures are: (1) verify the HMAC signature on every request — Stripe uses Stripe-Signature, Shopify uses X-Shopify-Hmac-SHA256, GitHub uses X-Hub-Signature-256; (2) reject requests without a valid signature with 401; (3) set up active monitoring to detect when signature failures cause delivery retries.

Webhook Security Guide: Signature Verification, HTTPS, and Monitoring

· 7 min read · Webhook best practices

Your webhook endpoint is a publicly reachable URL that executes business logic based on the request body. Without signature verification, it's essentially an unauthenticated API endpoint that anyone can call with fabricated data. Stripe, Shopify, and GitHub all provide the tooling to fix this — but you have to implement it.

Why Do Webhooks Need Security Hardening?

Webhook endpoints are different from authenticated API endpoints in one critical way: the request comes from the platform, not from an authenticated user session. There's no Bearer token, no API key in the header that uniquely identifies the caller. The only way to verify the request is genuine is to verify its signature.

Without signature verification, an attacker who discovers your webhook URL can:

How Do You Verify a Stripe Webhook Signature?

Stripe sends a Stripe-Signature header with every webhook delivery. The header contains:

Verification steps:

// Node.js — manual signature verification
import crypto from 'crypto';

function verifyStripeSignature(payload, header, secret) {
  const parts = header.split(',');
  const timestamp = parts.find(p => p.startsWith('t=')).slice(2);
  const signature = parts.find(p => p.startsWith('v1=')).slice(3);

  // Reject if timestamp is more than 5 minutes old (replay attack prevention)
  const tolerance = 300; // seconds
  if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > tolerance) {
    throw new Error('Timestamp outside tolerance window');
  }

  const signedPayload = `${timestamp}.${payload}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedPayload, 'utf8')
    .digest('hex');

  // Use timingSafeEqual to prevent timing attacks
  const expectedBuf = Buffer.from(expected);
  const receivedBuf = Buffer.from(signature);
  if (expectedBuf.length !== receivedBuf.length ||
      !crypto.timingSafeEqual(expectedBuf, receivedBuf)) {
    throw new Error('Signature mismatch');
  }
}

In practice, use the Stripe SDK's built-in method: stripe.webhooks.constructEvent(payload, header, secret). It handles timestamp validation, signature comparison, and throws a Stripe.errors.StripeSignatureVerificationError if verification fails.

Critical: pass the raw request body as a Buffer, not a parsed JSON object. Stripe signs the raw bytes. If your framework parses the body before your handler sees it, the signature will not match.

How Do You Verify a Shopify Webhook Signature?

Shopify sends the signature in the X-Shopify-Hmac-SHA256 header as a Base64-encoded HMAC-SHA256 of the raw request body.

// Node.js — Shopify signature verification
import crypto from 'crypto';

function verifyShopifySignature(rawBody, header, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('base64');

  const received = header; // X-Shopify-Hmac-SHA256 value

  const expectedBuf = Buffer.from(expected);
  const receivedBuf = Buffer.from(received);

  if (expectedBuf.length !== receivedBuf.length ||
      !crypto.timingSafeEqual(expectedBuf, receivedBuf)) {
    throw new Error('Invalid Shopify signature');
  }
}

Shopify uses Base64 encoding (not hex). The secret is the one shown in your Shopify admin → Settings → Notifications → Webhooks. Different webhook subscriptions share the same secret for a given store.

How Do You Verify a GitHub Webhook Signature?

GitHub sends the signature in X-Hub-Signature-256 as sha256={hex_digest}.

// Node.js — GitHub signature verification
import crypto from 'crypto';

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

  const expectedBuf = Buffer.from(expected);
  const receivedBuf = Buffer.from(header); // X-Hub-Signature-256 value

  if (expectedBuf.length !== receivedBuf.length ||
      !crypto.timingSafeEqual(expectedBuf, receivedBuf)) {
    throw new Error('Invalid GitHub signature');
  }
}

GitHub also sends the older X-Hub-Signature (SHA-1) header. Prefer X-Hub-Signature-256 (SHA-256). The secret is set when creating the webhook in repository or organization settings.

What Happens If Webhook Signature Verification Fails?

Return HTTP 401. Do not process the event. Do not return 200 to a request with an invalid signature — a 200 response tells the platform the delivery succeeded, which prevents retries even though your handler ignored the payload.

Common causes of legitimate signature verification failures (not attacks):

What Other Webhook Security Measures Should You Implement?

HTTPS only

Never accept webhooks over plain HTTP. All three platforms — Stripe, Shopify, and GitHub — require HTTPS for webhook endpoints in production. Without HTTPS, the payload and signature travel in plaintext and an attacker with network access can intercept and replay deliveries.

IP allowlisting

Both Stripe and GitHub publish their webhook sender IP ranges. You can configure your load balancer or firewall to reject webhook requests from any IP not in these ranges, adding a second layer of defense:

Shopify does not publish a stable IP list, so IP allowlisting is not practical for Shopify webhooks.

Short-lived and rotatable secrets

Store webhook secrets in environment variables, not in source code. Rotate secrets periodically — all three platforms let you update the webhook secret without downtime. During rotation, you may need to temporarily accept both the old and new secret for a brief window to avoid dropping in-flight deliveries.

Do not log raw payloads

Webhook payloads can contain PII: customer email addresses, card last four digits, billing addresses. Do not log the full raw payload to application logs, especially in production. Log the event ID, event type, and relevant identifiers — not the full body.

How Does Signature Verification Failure Affect Delivery Logs?

When your endpoint returns 401 due to a signature verification failure, the platform sees a non-2xx response and marks the delivery as failed. Stripe will retry. Shopify will retry. GitHub will not — it records the failure and stops.

A sudden spike in delivery failures across all event types, starting after a deploy, is a strong signal that you've broken signature verification — possibly by deploying a wrong secret, changing body parsing middleware, or rotating the secret without updating the handler.

Webhook Guardian monitors your delivery logs and alerts you within 5 minutes when failures spike — giving you the signal you need to diagnose whether it's an application error, a misconfigured secret, or an infrastructure issue, before it compounds.

FAQ: Webhook Security

How do I verify a webhook signature?
Compute HMAC-SHA256 of the raw request body using your webhook secret as the key. Compare to the platform's signature header using a constant-time comparison. Header names: Stripe-Signature (Stripe), X-Shopify-Hmac-SHA256 (Shopify), X-Hub-Signature-256 (GitHub). Return 401 if signatures do not match — never return 200 on a failed verification.
What is the Stripe webhook signature header?
Stripe-Signature. It contains a timestamp (t=) and HMAC-SHA256 signature (v1=). Verification: concatenate timestamp + "." + raw body, compute HMAC-SHA256 with your signing secret, compare to v1. Also verify the timestamp is within 300 seconds to prevent replay attacks. Use stripe.webhooks.constructEvent() to handle this automatically.
What happens if I don't verify webhook signatures?
Any HTTP client can call your endpoint with a fabricated payload. Attackers can trigger order fulfillment without paying, cancel active subscriptions, or inject arbitrary data. Signature verification is the only way to confirm a webhook request originated from the actual platform and has not been tampered with.

Signature verification secures your handler. Delivery monitoring secures your reliability. Connect Webhook Guardian to get alerted within 5 minutes when deliveries fail — including failures caused by misconfigured secrets after a deploy. Learn how it works →

Also see: Webhook signature glossary entry · Webhook best practices · Stripe webhook monitoring