Webhook Signature Verification
title: "Webhook Signature Verification" description: "Learn how to implement webhook signature verification using HMAC, RSA, and other signing methods to ensure payload authenticity and integrity." weight: 15 date: 2026-06-28 lastmod: 2026-06-28 tags: ["apis", "webhooks"]
Signature verification ensures that webhook payloads come from the claimed provider and have not been tampered with during transit. It is the most important security measure for webhook consumers.
## What You'll Learn
- HMAC-based signing with shared secrets
- Signature header formats
- Timing-safe comparison
- Key rotation strategies
- Consumer-side verification implementation
## Why It Matters
Without signature verification, any attacker who discovers your webhook URL can send fake events. Signatures provide cryptographic proof of the payload's origin and integrity.
## Real-World Use
A payment processor signs every webhook with an HMAC-SHA256 signature using a per-merchant secret. The merchant's server verifies the signature before processing any payment events, preventing fraud from fake webhook calls.
## Flow Chart
```mermaid
sequenceDiagram
participant P as Provider
participant C as Consumer
P->>P: Create payload
P->>P: Sign(payload, secret) = signature
P->>C: POST payload + X-Signature-256
C->>C: Read secret for webhook ID
C->>C: Compute expected = Sign(payload, secret)
C->>C: Compare signatures
Note over C: If match: process
Note over C: If mismatch: reject
Code Examples
Example 1: HMAC-SHA256 Provider Signing
// Provider: signing webhook payloads
const crypto = require('crypto');
function signWebhook(payload, secret) {
// Normalize payload as string
const payloadStr = typeof payload === 'string'
? payload
: JSON.stringify(payload);
// Create HMAC-SHA256 signature
const signature = crypto
.createHmac('sha256', secret)
.update(payloadStr, 'utf8')
.digest('hex');
return signature;
}
async function sendSignedWebhook(url, payload, secret) {
const payloadStr = JSON.stringify(payload);
const timestamp = Math.floor(Date.now() / 1000);
// Include timestamp in signature to prevent replay attacks
const signedPayload = `${timestamp}.${payloadStr}`;
const signature = signWebhook(signedPayload, secret);
try {
const response = await axios.post(url, payload, {
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature':
`t=${timestamp},v1=${signature}`,
'X-Webhook-ID': payload.id,
},
});
return response.status;
} catch (error) {
throw new Error(`Webhook delivery failed: ${error.message}`);
}
}
// Usage
const webhookSecret = process.env.WEBHOOK_SECRET;
const payload = {
id: 'evt_001',
type: 'order.created',
data: { orderId: 'ord_123' },
};
sendSignedWebhook(
'https://consumer.example.com/webhook',
payload,
webhookSecret
);
Expected output: Provider creates HMAC-SHA256 signature including timestamp and sends it in the X-Webhook-Signature header.
Example 2: Consumer Side Verification
// Consumer: verifying webhook signatures
const crypto = require('crypto');
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const webhookId = req.headers['x-webhook-id'];
if (!signature) {
return res.status(401).json({ error: 'Missing signature' });
}
// Parse signature header
const parts = signature.split(',');
const timestamp = parts.find(p => p.startsWith('t='))?.slice(2);
const signatureValue = parts.find(p => p.startsWith('v1='))?.slice(3);
if (!timestamp || !signatureValue) {
return res.status(401).json({ error: 'Invalid signature format' });
}
// Prevent replay attacks (allow 5 minute tolerance)
const now = Math.floor(Date.now() / 1000);
if (now - parseInt(timestamp) > 300) {
return res.status(401).json({ error: 'Signature expired' });
}
// Look up secret for this webhook
const secret = getWebhookSecret(webhookId);
// Verify signature
const payload = JSON.stringify(req.body);
const signedContent = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedContent, 'utf8')
.digest('hex');
// Timing-safe comparison
if (!crypto.timingSafeEqual(
Buffer.from(signatureValue),
Buffer.from(expectedSignature)
)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Signature verified, process webhook
processWebhook(req.body);
res.status(200).json({ status: 'ok' });
});
function getWebhookSecret(webhookId) {
// Retrieve secret from database
return database.getWebhookSecret(webhookId);
}
Expected output: Consumer parses signature header, converts to timing-safe comparison, validates timestamp for replay protection, and processes the webhook.
Example 3: Multiple Signature Schemes
// Provider supporting multiple signature schemes
const crypto = require('crypto');
function signPayload(payload, options) {
const { algorithm, secret, privateKey, keyId } = options;
const payloadStr = JSON.stringify(payload);
const timestamp = Math.floor(Date.now() / 1000);
const signedContent = `${timestamp}.${payloadStr}`;
let signature;
switch (algorithm) {
case 'hmac-sha256':
signature = crypto
.createHmac('sha256', secret)
.update(signedContent)
.digest('base64');
break;
case 'rsa-sha256':
signature = crypto
.sign('sha256', Buffer.from(signedContent), privateKey)
.toString('base64');
break;
case 'hmac-sha512':
signature = crypto
.createHmac('sha512', secret)
.update(signedContent)
.digest('base64');
break;
}
return {
signature,
algorithm,
timestamp,
keyId,
};
}
// Headers for different schemes
const hmacHeaders = {
'X-Signature-256': `t=${timestamp},hmac=${signature}`,
};
const rsaHeaders = {
'X-Signature-RSA': `t=${timestamp},rsa=${signature},keyid=${keyId}`,
'X-Signature-Algorithm': 'rsa-sha256',
};
// Consumer verification for RSA
function verifyRSASignature(payload, signature, timestamp, publicKey) {
const signedContent = `${timestamp}.${JSON.stringify(payload)}`;
return crypto.verify(
'sha256',
Buffer.from(signedContent),
publicKey,
Buffer.from(signature, 'base64')
);
}
Expected output: Provider supports multiple signing algorithms (HMAC, RSA) and consumers verify using the appropriate method.
Common Mistakes
| Mistake | Explanation |
|---|---|
| Using string comparison for signatures | Use timingSafeEqual or hmac.compare_digest to prevent timing attacks |
| Not including timestamp in signature | Without timestamps, captured signatures can be replayed indefinitely |
| Accepting unsigned webhooks | Even in development, always verify signatures to catch integration issues |
| Hardcoding secrets in source code | Store secrets securely, use environment variables or secret management services |
| Not handling key rotation | Consumers must support multiple active keys during rotation periods |
Practice Questions
- Why is HMAC preferred for webhook signing?
- How does the timestamp prevent replay attacks?
- What is a timing-safe comparison and why is it important?
- How do you rotate webhook signing secrets?
- What should a consumer do when signature verification fails?
Challenge
Implement signature verification for a webhook system that supports both HMAC-SHA256 and RSA-SHA256 signing. Include key rotation support where the provider publishes new keys via an API endpoint and consumers can fetch updated keys.
FAQ
Mini Project
Build a webhook signature verification library that supports HMAC-SHA256 and HMAC-SHA512, includes timing-safe comparison, timestamp validation with configurable tolerance, and automatic key rotation. Include comprehensive tests.
What's Next
Learn about webhook retry policies
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro