Webhook Security Best Practices for 2026
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
- The webhook provider generates a signature using a shared secret and the request body
- The signature is sent in a header (e.g.,
X-Hub-Signature-256,Stripe-Signature) - 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:
- Stripe: Webhook IP addresses
- GitHub: API IP ranges
- Shopify: Published in their documentation
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
| Practice | Priority | Complexity |
|---|---|---|
| HMAC signature verification | 🔴 Critical | Medium |
| HTTPS enforcement | 🔴 Critical | Low |
| Timestamp validation | 🟡 Important | Low |
| Rate limiting | 🟡 Important | Low |
| Idempotency handling | 🟡 Important | Medium |
| Async processing | 🟡 Important | Medium |
| IP allowlisting | 🟢 Nice to have | Low |
| Comprehensive logging | 🟢 Nice to have | Low |
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
Top 5 Svix Alternatives for Webhook Infrastructure in 2026
Exploring Svix alternatives? Compare Mitte, Hookdeck, Convoy, AWS EventBridge, and Inngest for your webhook infrastructure needs.
Top 5 Webhook.site Alternatives for Developers in 2026
Need more than Webhook.site offers? Compare the best alternatives for webhook testing and delivery — Mitte, RequestBin, Beeceptor, Pipedream, and hookbin.