Webhook Security Best Practices for 2026

Learn how to secure your webhook endpoints with signature verification, HTTPS, IP allowlisting, rate limiting, and more. A complete guide to webhook security.

Webhooks are HTTP callbacks — they receive data from external services over the public internet. Without proper security measures, your webhook endpoints are vulnerable to spoofing, replay attacks, and abuse.

This guide covers the essential security practices every developer should implement.

Why Webhook Security Matters

A poorly secured webhook endpoint can lead to:

  • Data spoofing — An attacker sends fake webhook events that your application trusts
  • Replay attacks — Legitimate webhook payloads are captured and re-sent
  • DDoS abuse — Your webhook endpoint is flooded with requests
  • Data leaks — Sensitive payload data is intercepted in transit

1. Always Verify HMAC Signatures

Most webhook providers sign their payloads with an HMAC (Hash-based Message Authentication Code). This is your first and most important line of defense.

How HMAC Verification Works

  1. The webhook provider generates a signature using a shared secret and the request body
  2. The signature is sent in a header (e.g., X-Hub-Signature-256, Stripe-Signature)
  3. Your server recalculates the signature and compares it

Implementation

import crypto from 'crypto'

function verifyHmacSignature(
  payload: string,
  signatureHeader: string,
  secret: string,
  algorithm: string = 'sha256'
): boolean {
  const expected = crypto
    .createHmac(algorithm, secret)
    .update(payload, 'utf8')
    .digest('hex')

  const signature = signatureHeader.replace(`${algorithm}=`, '')

  // Use timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  )
}

Critical: Always use crypto.timingSafeEqual() instead of === for signature comparison. String equality is vulnerable to timing attacks.

Provider-Specific Examples

Stripe:

const sig = req.headers['stripe-signature']
const event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret)

GitHub:

const sig = req.headers['x-hub-signature-256']
const isValid = verifyHmacSignature(req.body, sig, webhookSecret, 'sha256')

Shopify:

const hmac = req.headers['x-shopify-hmac-sha256']
const hash = crypto.createHmac('sha256', secret).update(req.body).digest('base64')
const isValid = hash === hmac

Tip: Mitte can automatically verify HMAC signatures before forwarding webhooks to your API, so you don't have to implement this in every endpoint. Learn more →

2. Enforce HTTPS

Never accept webhooks over plain HTTP. All webhook traffic should be encrypted in transit.

  • Register only https:// URLs with webhook providers
  • Configure your server to redirect HTTP to HTTPS
  • Use TLS 1.2 or higher

If you use a webhook gateway like Mitte, HTTPS is enforced automatically on all endpoints.

3. Validate Timestamps

Many providers include a timestamp in the webhook payload or headers. Use it to reject stale events and prevent replay attacks.

function isTimestampValid(
  timestamp: number,
  toleranceSeconds: number = 300  // 5 minutes
): boolean {
  const now = Math.floor(Date.now() / 1000)
  return Math.abs(now - timestamp) <= toleranceSeconds
}

Stripe example:

const timestamp = parseInt(sig.split(',')[0].split('=')[1])
if (!isTimestampValid(timestamp)) {
  throw new Error('Webhook timestamp too old')
}

4. Implement Rate Limiting

Protect your webhook endpoints from abuse with rate limiting:

import { RateLimiter } from 'limiter'

const limiter = new RateLimiter({
  tokensPerInterval: 100,
  interval: 'minute'
})

app.post('/webhooks/:provider', async (req, res) => {
  const hasToken = await limiter.tryRemoveTokens(1)
  if (!hasToken) {
    return res.status(429).json({ error: 'Too many requests' })
  }
  // ... process webhook
})

5. Use IP Allowlisting (When Available)

Some providers publish their webhook source IP ranges. Use these to add another layer of protection:

const allowedIPs = new Set(['192.168.1.1', '10.0.0.1']) // Example

function isIPAllowed(ip: string): boolean {
  return allowedIPs.has(ip)
}

Note: IP allowlisting should be a secondary measure, not your primary authentication. IPs can be spoofed, and providers may change their IP ranges.

6. Handle Idempotency

Webhooks may be delivered more than once. Your handler must be idempotent.

// Use a database to track processed event IDs
async function processWebhook(event: WebhookEvent): Promise<void> {
  // Check if already processed
  const existing = await db.webhookEvent.findUnique({
    where: { eventId: event.id }
  })
  
  if (existing) {
    console.log(`Event ${event.id} already processed, skipping`)
    return
  }

  // Process and record
  await db.$transaction([
    // Your business logic here
    db.webhookEvent.create({
      data: { eventId: event.id, processedAt: new Date() }
    })
  ])
}

7. Return 200 Quickly, Process Asynchronously

Webhook providers have timeout limits (typically 5-30 seconds). If your processing takes longer, the provider will retry — causing duplicate deliveries.

app.post('/webhooks/stripe', async (req, res) => {
  // 1. Verify signature (fast)
  const isValid = verifySignature(req)
  if (!isValid) return res.status(401).end()

  // 2. Acknowledge immediately
  res.status(200).json({ received: true })

  // 3. Queue for async processing
  await queue.add('process-webhook', {
    provider: 'stripe',
    payload: req.body,
    receivedAt: Date.now()
  })
})

8. Log Everything

Comprehensive logging is essential for debugging webhook issues and detecting security incidents.

Log at minimum:

  • Source IP and headers
  • Timestamp received
  • Signature verification result (pass/fail)
  • Event type and ID
  • Processing result

A webhook gateway like Mitte automatically logs all of this with 30-day retention and real-time search.

Security Checklist

PracticePriorityComplexity
HMAC signature verification🔴 CriticalMedium
HTTPS enforcement🔴 CriticalLow
Timestamp validation🟡 ImportantLow
Rate limiting🟡 ImportantLow
Idempotency handling🟡 ImportantMedium
Async processing🟡 ImportantMedium
IP allowlisting🟢 Nice to haveLow
Comprehensive logging🟢 Nice to haveLow

How Mitte Simplifies Webhook Security

Instead of implementing all these practices in every endpoint, Mitte handles several automatically:

  • HMAC verification — Configure signing secrets per endpoint; invalid signatures are rejected before forwarding
  • HTTPS — All Mitte endpoint URLs use HTTPS by default
  • Rate limiting — Built-in rate limiting on all plans
  • Logging — Full request/response logging with 30-day retention
  • AI analysis — When something fails, AI explains what went wrong

Get started with secure webhook delivery →


Further Reading