The Ultimate Webhook Debugging Guide

Webhook debugging is one of the most frustrating tasks in software development. Unlike API calls where you control both sides of the conversation, webhooks involve external systems sending requests to your endpoint — and when things go wrong, it can be difficult to determine whether the problem is on the sender's side, your side, or somewhere in between. This guide gives you a systematic approach to diagnosing and fixing every common webhook issue.
The Webhook Debugging Mindset
Before diving into specific failures, establish the right debugging mindset:
- Isolate the problem. Is the webhook being sent? Is it reaching your server? Is your server processing it correctly? Each failure point has different solutions.
- Check the simplest things first. Typos in URLs, wrong environment variables, and inactive subscriptions cause more webhook issues than complex bugs.
- Use tools to observe. You cannot debug what you cannot see. Logging, monitoring, and inspection tools are essential.
Common Webhook Failure Modes
1. Webhook Not Being Sent
Symptoms: Your endpoint receives no requests at all.
Possible causes:
- The webhook subscription is not active in the provider's dashboard
- You are subscribed to the wrong event types
- The triggering event did not actually occur (e.g., test mode vs live mode mismatch)
- The provider has disabled your webhook due to too many past failures
How to diagnose:
# Check if the webhook subscription is active (example with Stripe CLI)
stripe webhooks list
# Check recent webhook attempts in the provider's dashboard
# Most providers show delivery history with status codes
Fix: Verify the subscription is active, check that you are subscribed to the correct event types, and confirm you are triggering events in the correct mode (test vs live).
2. Connection Refused or Timeout
Symptoms: The provider's delivery logs show connection errors or timeouts.
Possible causes:
- Your server is not running
- Your server is not publicly accessible (using localhost or a private IP)
- A firewall or security group is blocking port 443 (HTTPS)
- DNS is not resolving your domain correctly
- Your server is behind a load balancer that is not forwarding traffic
How to diagnose:
# Test if your endpoint is reachable from the internet
curl -X POST https://your-app.com/webhook \
-H "Content-Type: application/json" \
-d '{"test": true}' \
-v
# Check DNS resolution
dig your-app.com
# Check if the port is open
nc -zv your-app.com 443
# Check firewall rules (AWS Security Group)
aws ec2 describe-security-groups --group-ids sg-your-group
Fix: Ensure your server is running and publicly accessible. Check firewall rules to allow incoming HTTPS traffic. Verify DNS records are correct.
3. HTTP 4xx Errors
Symptoms: Your endpoint returns 400, 401, 403, 404, or 405 errors.
| Status | Common Cause | |---|---| | 400 | Payload parsing error or validation failure | | 401 | Signature verification failed or missing authentication | | 403 | IP blocked by WAF, Cloudflare, or security middleware | | 404 | Wrong URL path — typo or incorrect route configuration | | 405 | Endpoint only accepts GET, not POST (method not allowed) |
How to diagnose:
# Simulate a webhook request and check the response
curl -X POST https://your-app.com/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: test-signature" \
-d '{"event":"test","data":{}}' \
-w "\nHTTP Status: %{http_code}\n"
Fix: Check your server logs for the specific error. For 401 errors, see the section on signature verification debugging. For 404, verify the exact URL path matches your route definition.
4. HTTP 5xx Errors
Symptoms: Your endpoint returns 500, 502, 503, or 504 errors.
| Status | Common Cause | |---|---| | 500 | Unhandled exception in your webhook handler code | | 502 | Reverse proxy cannot connect to your application server | | 503 | Server is overloaded or in maintenance mode | | 504 | Your handler takes too long — proxy times out |
How to diagnose:
# Check your application logs
tail -f /var/log/your-app/error.log
# Check reverse proxy logs
tail -f /var/log/nginx/error.log
# Check if the application process is running
ps aux | grep node
systemctl status your-app
Fix: For 500 errors, check your application error logs for the stack trace. For 502/504, ensure your application is running and responding within the proxy timeout. See our error handling guide for building resilient handlers.
5. Signature Verification Failures
Symptoms: Your endpoint returns 401 or your verification function consistently returns false.
This is one of the most common webhook debugging challenges. Here are the typical causes:
Cause 1: Parsed vs raw body mismatch
// WRONG — body has been parsed by express.json() middleware
app.use(express.json()); // This parses the body BEFORE your handler
app.post('/webhook', (req, res) => {
const body = JSON.stringify(req.body); // Re-serialized — may differ from original
verify(body, signature, secret); // FAILS because body bytes changed
});
// RIGHT — use raw body for verification
app.post('/webhook',
express.raw({ type: 'application/json' }), // Gives you the raw Buffer
(req, res) => {
verify(req.body, signature, secret); // Raw bytes — matches what was signed
const event = JSON.parse(req.body);
}
);
Cause 2: Wrong secret or environment variable
# Check which secret your code is actually using
echo $WEBHOOK_SECRET
# Compare with the secret shown in the provider's dashboard
# Common issues: extra whitespace, wrong environment, old secret after rotation
Cause 3: Wrong signature format
Different providers encode signatures differently:
- Stripe:
v1=hexstring(prefixed) - GitHub:
sha256=hexstring(prefixed) - Shopify: Base64-encoded (not hex)
Check the provider's documentation and ensure your code handles the exact format. See our guide on webhook authentication methods for provider-specific examples.
The most common signature verification bug: a framework-level body parser runs before your webhook route, modifying the request body. In Express.js, if you have app.use(express.json()) at the top of your app, it will parse the body for ALL routes — including your webhook endpoint. Either exclude the webhook route from the global parser or use express.raw() specifically for webhook routes.
Step-by-Step Debugging Workflow
When a webhook is not working, follow this systematic workflow:
Verify the webhook is being sent
Check the provider's webhook delivery logs. Most dashboards show recent delivery attempts with status codes, response bodies, and timestamps.
If no deliveries appear, the issue is on the provider side — check subscription status, event types, and test/live mode.
Verify your endpoint is reachable
Use curl or a tool like Webhookify to send a test request to your endpoint URL:
curl -X POST https://your-app.com/webhook \
-H "Content-Type: application/json" \
-d '{"test": true}' \
-v
If this fails, the issue is network-level — DNS, firewall, or server availability.
Check your application logs
If the request reaches your server but fails, the error will be in your application logs. Look for:
- Parse errors (malformed JSON, unexpected content type)
- Authentication errors (signature verification failure)
- Business logic errors (database constraint violations, null reference errors)
# Check for recent errors in your logs
grep -i "error\|exception\|failed" /var/log/your-app/app.log | tail -20
Inspect the actual webhook payload
Use Webhookify or a similar tool to capture the exact request — headers, body, and timing. Compare the actual payload with what your code expects.
# Quick inspection endpoint for debugging
app.post('/webhook/debug', express.raw({ type: '*/*' }), (req, res) => {
console.log('=== WEBHOOK DEBUG ===');
console.log('Headers:', JSON.stringify(req.headers, null, 2));
console.log('Body:', req.body.toString());
console.log('Content-Type:', req.headers['content-type']);
console.log('====================');
res.status(200).json({ received: true });
});
Test with a known-good payload
Construct a minimal test case that simulates the webhook:
# Send a minimal test webhook
curl -X POST https://your-app.com/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: $(echo -n '{"event":"test"}' | openssl dgst -sha256 -hmac 'your-secret' | awk '{print $2}')" \
-d '{"event":"test"}'
If this works but real webhooks do not, the issue is in the payload structure, signature format, or provider-specific behavior.
Check for upstream issues
If everything on your side looks correct, check for known issues on the provider side:
- Provider status page (e.g., status.stripe.com)
- Provider community forums or support
- Recent changes to the provider's webhook format or signing scheme
Essential Debugging Tools
Webhookify
Webhookify is purpose-built for webhook debugging. Create an endpoint in seconds and point any webhook provider at it. Every delivery is logged with:
- Full HTTP headers (including signature and timestamp headers)
- Complete request body (raw and formatted)
- Source IP address and user agent
- Precise timing information
- AI-powered payload analysis
When you are stuck on a webhook issue, the fastest path to resolution is often: point the webhook at a Webhookify endpoint, inspect the raw request, and compare it with what your code expects.
curl
curl is the Swiss Army knife of webhook debugging. Use it to simulate webhook requests:
# Basic webhook simulation
curl -X POST https://your-app.com/webhook \
-H "Content-Type: application/json" \
-d '{"event":"payment.completed","data":{"amount":1000}}' \
-v
# With custom headers (simulating a provider)
curl -X POST https://your-app.com/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: sha256=abc123" \
-H "X-Webhook-Timestamp: $(date +%s)" \
-d @webhook-payload.json \
-w "\n\nResponse Code: %{http_code}\nTime: %{time_total}s\n"
# Test from a specific IP (using a proxy)
curl -X POST https://your-app.com/webhook \
--proxy socks5://proxy-server:1080 \
-H "Content-Type: application/json" \
-d '{"test": true}'
ngrok
For local development, ngrok creates a public URL that tunnels to your local server:
# Start ngrok tunnel to local port 3000
ngrok http 3000
# Output:
# Forwarding: https://abc123.ngrok.io -> http://localhost:3000
# Use https://abc123.ngrok.io/webhook as your webhook URL
ngrok provides a web dashboard at http://127.0.0.1:4040 where you can inspect every request, including headers and body — useful for debugging during development.
Provider CLI Tools
Many providers offer CLI tools for webhook testing:
# Stripe CLI: Listen for webhook events and forward to local server
stripe listen --forward-to localhost:3000/webhook
# Stripe CLI: Trigger a test event
stripe trigger payment_intent.succeeded
# GitHub CLI: View recent webhook deliveries
gh api repos/owner/repo/hooks/12345/deliveries
# Shopify CLI: (within your Shopify app project)
shopify app webhook trigger --topic orders/create --address http://localhost:3000/webhook
Debugging Specific Scenarios
Webhooks Work in Testing but Fail in Production
Common causes:
- Different environment variables (test vs production webhook secrets)
- Different middleware configuration (production has WAF, rate limiting, or body size limits)
- Different URL paths (missing /api prefix in production)
- DNS or CDN caching serving stale configuration
# Compare environment configurations
# Development
echo $WEBHOOK_SECRET # Should match your test webhook secret
# Production (check your deployment platform)
heroku config:get WEBHOOK_SECRET
# or
aws ssm get-parameter --name /prod/webhook-secret
Webhooks Arrive But Processing Fails Silently
If your endpoint returns 200 but the expected actions do not happen:
// Add comprehensive error logging to your handler
app.post('/webhook', async (req, res) => {
// Respond immediately to prevent timeout
res.status(200).json({ received: true });
try {
const event = req.body;
console.log(`Processing webhook: ${event.type} (${event.id})`);
const result = await processEvent(event);
console.log(`Webhook processed successfully: ${event.id}`, result);
} catch (error) {
// This error happens AFTER the response is sent
// Without logging, you would never know it occurred
console.error(`Webhook processing FAILED: ${req.body?.id}`, {
error: error.message,
stack: error.stack,
payload: JSON.stringify(req.body).slice(0, 500)
});
// Alert your team
await alertTeam(`Webhook processing failed: ${error.message}`);
}
});
Silent failures are the most dangerous webhook issues. Your endpoint returns 200 (so the provider does not retry), but the processing fails. Without proper error logging and monitoring, these failures go undetected until a customer complains. Always log errors that occur after the 200 response, and set up alerting for processing failures.
Intermittent Webhook Failures
If webhooks work sometimes but fail randomly:
Check for race conditions. If multiple webhooks for the same resource arrive simultaneously, they might compete for database locks or conflict with each other:
// Use database transactions with row-level locking
async function handleOrderWebhook(event) {
await db.transaction(async (tx) => {
// SELECT FOR UPDATE locks the row, preventing concurrent updates
const order = await tx.query(
'SELECT * FROM orders WHERE id = $1 FOR UPDATE',
[event.data.order_id]
);
if (!order) {
// Order might not exist yet if webhooks arrive out of order
await tx.query(
'INSERT INTO orders (id, status) VALUES ($1, $2)',
[event.data.order_id, event.data.status]
);
} else {
await tx.query(
'UPDATE orders SET status = $1 WHERE id = $2',
[event.data.status, event.data.order_id]
);
}
});
}
Check for memory or resource exhaustion. Under load, your server might run out of memory or database connections:
# Monitor server resources during webhook delivery
top -p $(pgrep -f "node")
# Check database connection pool status
# Check for memory leaks with --inspect flag
node --inspect app.js
Check for timeout issues. If your processing sometimes takes longer than the provider's timeout:
// Measure processing time
app.post('/webhook', async (req, res) => {
const startTime = Date.now();
res.status(200).json({ received: true });
try {
await processEvent(req.body);
const duration = Date.now() - startTime;
console.log(`Webhook processed in ${duration}ms`);
if (duration > 5000) {
console.warn(`Slow webhook processing: ${duration}ms for ${req.body.type}`);
}
} catch (error) {
console.error('Processing error:', error);
}
});
Building a Webhook Debug Dashboard
For ongoing debugging, build (or use) a dashboard that shows:
// Simple webhook logging middleware
function webhookLogger(req, res, next) {
const startTime = Date.now();
const requestId = crypto.randomUUID();
// Log incoming request
const logEntry = {
requestId,
timestamp: new Date().toISOString(),
method: req.method,
url: req.url,
headers: {
'content-type': req.headers['content-type'],
'user-agent': req.headers['user-agent'],
'x-webhook-id': req.headers['x-webhook-id'],
'x-forwarded-for': req.headers['x-forwarded-for']
},
bodyPreview: req.body?.toString?.()?.slice(0, 1000) || 'N/A',
sourceIP: req.ip
};
// Capture the response
const originalSend = res.send;
res.send = function(body) {
logEntry.responseStatus = res.statusCode;
logEntry.responseTime = Date.now() - startTime;
logEntry.responseBody = typeof body === 'string' ? body.slice(0, 500) : 'N/A';
console.log('WEBHOOK_LOG:', JSON.stringify(logEntry));
// Or store in a database for dashboard display
return originalSend.call(this, body);
};
next();
}
app.use('/webhook', webhookLogger);
Rather than building custom debugging infrastructure, use Webhookify. It gives you an instant, production-ready webhook inspection dashboard with full request logging, AI-powered analysis, and real-time alerts via Telegram, Discord, Slack, email, or push notifications. When you are debugging at 2 AM, having a clear view of every webhook delivery — with headers, payloads, timing, and source information — makes the difference between a quick fix and hours of frustration.
Webhook Debugging Checklist
Use this checklist when troubleshooting webhook issues:
- [ ] Subscription active? Check the provider's dashboard for subscription status
- [ ] Correct URL? Verify the endpoint URL matches your server's route exactly
- [ ] Correct event types? Confirm you are subscribed to the events you expect
- [ ] Server reachable? Test with curl from an external network
- [ ] Firewall open? Check security groups and WAF rules for port 443
- [ ] DNS resolving? Verify your domain resolves to the correct IP
- [ ] TLS valid? Check your SSL certificate is valid and not expired
- [ ] Correct secret? Verify the webhook secret in your environment matches the provider's
- [ ] Raw body used? Ensure signature verification uses the raw request body, not parsed JSON
- [ ] Correct signature format? Check hex vs base64, prefixes, and provider-specific formatting
- [ ] Responding fast enough? Ensure your endpoint responds within 5-10 seconds
- [ ] Logging errors? Check that post-response processing errors are captured
- [ ] Handling duplicates? Verify idempotency logic is working
- [ ] Test/live mode match? Confirm you are using the correct mode for the environment
Debug Webhooks Faster with Webhookify
Create instant webhook endpoints with full request logging, AI-powered payload analysis, and real-time alerts. See every header, every byte, and every timing detail — without writing logging code.
Start Debugging FreeFurther Reading
- Webhook Error Handling Best Practices — build handlers that fail gracefully
- Webhook Security Best Practices — fix authentication issues
- Webhook Authentication Methods — provider-specific signature verification
- Webhook Testing Guide — prevent issues before they reach production
- Webhook Retry Logic and Idempotency — handle retries and duplicates
Related Articles
- How to Test Webhooks: A Complete Guide
- Webhook Error Handling Best Practices
- How to Set Up Stripe Webhook Notifications
- How to Set Up GitHub Webhook Notifications
- Best Webhook Testing Tools in 2026