Back to Blog
guides10 min readApril 24, 2026

Webhook Security Best Practices: Complete Guide

Webhooks are public HTTP endpoints — any attacker can POST to them. This guide covers every security layer you need to implement.

A
Abinash B
April 24, 2026

Webhooks are public HTTPS endpoints. Anyone on the internet can POST to them. Without proper security, an attacker can send fake payment confirmations, trigger fraudulent order fulfillment, or inject malicious data into your systems. This guide covers every security layer you need.

1. Always Verify the Webhook Signature

Every major webhook provider signs their requests using HMAC-SHA256. They include the signature in a request header. Your server must verify this signature before processing the event.

// Node.js — Generic HMAC-SHA256 webhook signature verification
const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)  // Must be raw Buffer, not parsed JSON
    .digest('hex');
  
  // Use timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(signature, 'hex')
  );
}

// Express.js middleware
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  // Safe to process
});

Common mistake: Using express.json() instead of express.raw(). JSON parsing reformats the body, changing the bytes and breaking HMAC verification.

2. Use HTTPS Exclusively

All webhook providers require HTTPS. Never accept webhooks over HTTP — the payload and signature are transmitted in plaintext, allowing man-in-the-middle interception. Ensure your TLS certificate is valid and up to date.

3. Implement Idempotency

Providers retry webhooks on failure. The same event may arrive 2–10 times. Your handler must be idempotent — processing the same event twice must not create duplicate side effects.

// Store processed event IDs to deduplicate
const processedEvents = new Set(); // Use Redis in production

async function handleWebhook(event) {
  if (processedEvents.has(event.id)) {
    console.log(`Duplicate event ${event.id} — skipping`);
    return; // Already processed
  }
  
  await processEvent(event);
  processedEvents.add(event.id);
}

4. Respond Immediately, Process Asynchronously

Most providers timeout after 3–30 seconds. If your handler takes too long, the provider marks it as failed and retries. This can cause duplicate processing.

// Return 200 immediately, then process
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  // Verify signature first
  if (!verifySignature(req.body, req.headers['x-signature'])) {
    return res.status(401).send();
  }
  
  // Respond immediately
  res.status(200).send();
  
  // Process in background
  const event = JSON.parse(req.body);
  await queue.add('process-webhook', event);
});

5. Validate the Timestamp to Prevent Replay Attacks

Some providers (Stripe, Twilio) include a timestamp in the signature payload. Validate that the timestamp is recent (within 5 minutes) to prevent replay attacks where an attacker resends a valid, old request.

// Stripe includes timestamp in signature
const MAX_TIMESTAMP_AGE_SECONDS = 300; // 5 minutes

function isTimestampValid(timestamp) {
  const now = Math.floor(Date.now() / 1000);
  return Math.abs(now - parseInt(timestamp)) < MAX_TIMESTAMP_AGE_SECONDS;
}

6. Use IP Allowlisting Where Possible

Several providers publish their IP ranges. If your infrastructure supports it, allowlist only the provider's IP addresses at the firewall level. This adds a network-level security layer before your application code runs.

Providers with published IP ranges: Stripe, GitHub, Shopify. Check each provider's documentation for their current IP list.

7. Rate Limit Your Webhook Endpoints

Even with signature verification, an attacker could flood your endpoint with valid-looking requests to degrade service. Apply rate limiting per source IP and per event type.

// Express with express-rate-limit
const rateLimit = require('express-rate-limit');

const webhookLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 100, // Max 100 requests per minute per IP
  message: 'Too many webhook requests'
});

app.post('/webhook', webhookLimiter, handleWebhook);

8. Log Everything for Audit and Debugging

Webhook failures are hard to debug without logs. Store the raw payload, headers, response status, and processing result for every webhook. Retain logs for at least 30 days to support incident investigation.

Testing Webhook Security

WebhookWhisper lets you send test payloads from 35+ providers with real signature headers, so you can verify your security implementation before production. Test that your endpoint correctly rejects unsigned requests and duplicate events.

Frequently Asked Questions

What happens if I don't verify webhook signatures?

An attacker can POST a fake payment confirmation, triggering order fulfillment for a payment that never happened. Always verify HMAC signatures.

Should I use a separate webhook endpoint for each provider?

Yes. Separate endpoints let you apply provider-specific signature verification, rate limits, and logging. A single generic endpoint is harder to secure and debug.

How do I safely store webhook secrets?

Store webhook secrets in environment variables, not in your code repository. Use a secrets manager (AWS Secrets Manager, HashiCorp Vault) in production. Rotate secrets immediately if they are exposed.

#webhooks#security#hmac#best-practices

Ready to test your webhooks?

Get a free HTTPS endpoint in under 5 seconds — no signup required.

Create Free Account
Webhook Security Best Practices (2026) | WebhookWhisper