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
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:
- Send a fake
checkout.session.completedevent to trigger order fulfillment without paying - Send a fake
customer.subscription.deletedevent to cancel active user subscriptions - Replay a legitimate old event to trigger duplicate processing
- Send a fake
orders/createShopify event to trigger inventory reduction or shipping label generation - Send a fake GitHub
pushevent to trigger CI runs with an arbitrary payload
How Do You Verify a Stripe Webhook Signature?
Stripe sends a Stripe-Signature header with every webhook delivery. The header contains:
t=— Unix timestamp of when the payload was signedv1=— HMAC-SHA256 signature computed overtimestamp.payload
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):
- Body parsed before the raw bytes reach your handler (body parser middleware)
- Wrong webhook secret configured — e.g., using the live secret in a test environment
- Signature computed over a modified body — middleware that transforms the request body (decompression, encoding conversion) will break verification
- Rotated secret not yet deployed — after rotating the webhook secret, old requests signed with the previous secret will fail
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:
- Stripe: stripe.com/docs/ips
- GitHub: returned by
GET https://api.github.com/metaunder thehookskey
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?
What is the Stripe webhook signature header?
What happens if I don't verify webhook signatures?
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