The Ultimate Webhook Debugging Guide

Published Feb 21 202613 min read
Webhook debugging workflow showing common failure modes and troubleshooting steps

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:

  1. 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.
  2. Check the simplest things first. Typos in URLs, wrong environment variables, and inactive subscriptions cause more webhook issues than complex bugs.
  3. 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:

1

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.

2

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.

3

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
4

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 });
});
5

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.

6

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 Free

Further Reading

Related Articles

Frequently Asked Questions

The Ultimate Webhook Debugging Guide - Webhookify | Webhookify