Webhooks are one of the most powerful ways to receive real-time updates from third-party services. A single HTTP POST from Stripe, GitHub, Shopify, or Twilio can trigger critical business logic in your application — charging a customer, updating a repository, shipping an order, or sending a confirmation SMS.
But every webhook request arrives over the public internet. And that means anyone who guesses or discovers your webhook URL can send malicious payloads that look completely legitimate. Without proper authentication, your application has no way to tell the difference between a real event and a forged one.
That’s where webhook signature verification comes in. It’s a simple, standardized mechanism that ensures every incoming webhook request is genuinely from the service you expect and hasn’t been altered in transit.
In this comprehensive guide, you’ll learn exactly how webhook signature verification works, and how to implement it correctly in popular languages. You’ll also see common mistakes to avoid and how to test everything end-to-end — quickly and reliably.
What Is Webhook Signature Verification?
Webhook signature verification is the process of confirming that an incoming webhook request actually comes from the service you expect and hasn’t been tampered with.
Most providers use HMAC (Hash-based Message Authentication Code) with SHA-256 or SHA-512. The service computes:
signature = HMAC-SHA256(secret_key, payload)
Then they send the signature in a header (usually X-Signature, Signature, or X-Hub-Signature-256).
Your server:
- Receives the payload as raw bytes (important!)
- Recomputes the HMAC using your stored secret
- Compares the computed signature with the received one
If they match exactly, you process the webhook. Otherwise, you return HTTP 401 or 403.

Why HMAC-SHA256 Is the Industry Standard
Providers choose HMAC-SHA256 for good reasons:
- Fast: Even on modest hardware, it’s blazing quick.
- Secure: SHA-256 remains unbroken in 2025.
- Simple: One secret key, no public/private key pairs to manage.
- Standardized: Libraries in every language implement it correctly.
GitHub, Stripe, Shopify, Slack, and dozens of others all use HMAC-SHA256.
How to Implement Webhook Signature Verification in Node.js
Let’s start with a real-world example in Node.js.
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
const computedSignature = hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(computedSignature),
Buffer.from(signature)
);
}
Key points to notice:
- Always use
crypto.timingSafeEqualto prevent timing attacks. - Never use
===on strings — it’s vulnerable. - Use
Buffer.from()to ensure constant-time comparison.
Express middleware example:
app.post('/webhooks/stripe', (req, res, next) => {
const signature = req.headers['stripe-signature'];
const secret = process.env.STRIPE_WEBHOOK_SECRET;
// Get raw body (Express needs middleware to preserve raw body)
const rawBody = req.rawBody || req.body; // use body-parser with verify option
if (!verifyWebhookSignature(rawBody, signature, secret)) {
return res.status(401).send('Invalid signature');
}
// Signature is valid → process the event
next();
});
Python Implementation (FastAPI + Pydantic)
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
app = FastAPI()
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
computed = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, signature)
@app.post("/webhooks/github")
async def github_webhook(request: Request):
signature = request.headers.get("X-Hub-Signature-256")
if not signature:
raise HTTPException(status_code=401, detail="Missing signature")
payload = await request.body()
if not verify_signature(payload, signature.split('=')[1], SECRET):
raise HTTPException(status_code=401, detail="Invalid signature")
# Process webhook
return {"status": "ok"}
Common Pitfalls Developers Make (and How to Avoid Them)
1. Using JSON.stringify() or Parsed Body
Many frameworks parse JSON automatically. This breaks verification because whitespace, key order, and formatting differ.
Solution: Always capture the raw body before parsing.
In Express: Use body-parser with { verify: true }
In FastAPI: Use await request.body()
2. Comparing Strings with ===
Timing attacks can leak information. Use crypto.timingSafeEqual or hmac.compare_digest.
3. Storing Secrets in Code
Use environment variables or secret managers (AWS Secrets Manager, HashiCorp Vault, etc.).
4. Forgetting to Handle Replay Attacks
Most providers include a timestamp. Check that the event is recent (e.g., within 5 minutes).
const timestamp = req.headers['X-Signature-Timestamp'];
if (Date.now() - timestamp > 5 * 60 * 1000) {
return res.status(401).send('Timestamp too old');
}
5. Using SHA-1 (Still Happens!)
GitHub deprecated SHA-1 in 2022. Always use SHA-256.
Testing Webhook Signature Verification with Apidog
Manually testing webhooks is painful. You send a request, check logs, fix, repeat.
Apidog makes this trivial:
In your Apidog project, click the + icon in the left sidebar and choose "New Other Protocol APls" > "Webhook".
After creating the Webhook, fill out the following fields in the editor:Request Method: TypicallyPOST.Webhook name: This will appear in the API documentation and OpenAPI export, e.g., order.Debug URL(optional): The actual URL used for sending test requests. Note: This is for testing purposes only and will not be included in documentation.Other Info: Such as the request body.
ClickSaveonce you've completed all required fields.
Just enter your Webhook URL into the Debug URL field, then click Send to simulate a Webhook call.
I’ve saved hours of debugging with Apidog’s webhook simulator. It even supports Stripe’s exact stripe-signature format and GitHub’s sha256=... prefix.
Real-World Example: Verifying Stripe Webhooks
Stripe uses a special header format:
stripe-signature: t=1681234567,v1=abc123...,v0=def456...
You must:
- Extract the
t=timestamp - Extract the
v1=signature (preferred) - Recompute using
payload + timestamp - Compare only the
v1part
Stripe provides official libraries to handle this complexity:
const stripe = require('stripe')('sk_...');
stripe.webhooks.constructEvent(payload, sigHeader, endpointSecret);
But understanding the underlying HMAC is crucial when you need to implement it yourself.
Advanced Topics: Tolerating Multiple Signatures
Some providers (like Stripe) send multiple signatures for backward compatibility. Your code should:
- Split the header by
, - Try each one
- Accept if any matches
Security Best Practices in 2025
- Rotate webhook secrets every 90 days.
- Use short-lived secrets when possible.
- Log failed verifications but never log the secret.
- Rate-limit webhook endpoints to prevent brute-force attacks.
- Always use HTTPS.
Conclusion: Small Verification Step, Big Security Gain
Webhook signature verification sounds like a tiny detail. But it’s the difference between a secure application and one that attackers can trivially exploit.
Implement it correctly, test it thoroughly with tools like Apidog, and sleep better knowing your integrations are protected.
Download Apidog for free today and verify your first webhook in under 5 minutes. It’s the fastest way to prove your code actually works.



