Webhook Not Firing? How to Debug a Webhook That Isn't Working
You set up a webhook. The event fires on the sender's side. Nothing arrives on yours. Your logs are empty and you have no idea where the request went. This guide walks through the most common causes, in the order most likely to find the problem fast.
Webhook failures almost always fall into one of six categories: the event was never sent, the URL is wrong, your server returned an error, a firewall blocked the request, something in your handler silently rejected it, or signature verification failed before your code ever ran. Work through them in order.
Step 1: Verify the event is actually being sent
Before assuming something is wrong on your end, check the provider's delivery logs. Every major platform keeps a record of webhook delivery attempts and their outcomes.
- Stripe: Developers > Webhooks > select your endpoint > check the event log for delivery attempts and HTTP response codes.
- GitHub: Repository Settings > Webhooks > select your webhook > Recent Deliveries tab.
- Shopify: Settings > Notifications > scroll to your webhook and check delivery status.
- Linear: Settings > API > Webhooks > click your webhook to view delivery history.
If you see delivery attempts marked as failed, the event was sent. The problem is on your server or network. If there are no delivery attempts at all, the event was never triggered, or you may be looking at the wrong environment (test vs. live, dev vs. production workspace).
Step 2: Check your endpoint URL
URL mistakes are the most common cause of webhook failures, and they're usually subtle. Check every part of the URL you registered.
HTTP vs HTTPS. Most providers require HTTPS and will refuse to deliver to a plain HTTP URL. If you registered http:// when your server expects https:// (or vice versa), the delivery fails or goes to the wrong place.
Trailing slash. Some frameworks treat /webhooks/stripe and /webhooks/stripe/ as different routes. If your route is registered with a trailing slash but the URL in the provider's dashboard doesn't have one, the request hits a 301 redirect. Providers typically don't follow redirects.
Wrong path or typo. Double-check the path exactly. A common mistake is registering the webhook URL from memory instead of copying it. Also verify you're editing the right webhook endpoint in the provider's dashboard if you have more than one.
Environment mismatch. Stripe test mode webhooks go to one endpoint; live mode webhooks go to another. If you updated the test webhook but events are firing in live mode, nothing will arrive.
Step 3: Check your server's response code
Providers expect a 2xx response within a timeout window, typically 5 to 30 seconds depending on the platform. Stripe allows 30 seconds. GitHub allows 10. Slack allows 3. If your server returns a 4xx, 5xx, or times out, the provider marks the delivery as failed and may retry with exponential backoff.
Check your server logs for the exact response code your endpoint returned at the time the delivery was attempted. Common problems here include unhandled exceptions returning 500, missing route registrations returning 404, and authentication middleware blocking the request with 401 or 403.
Also check response time. If your handler does heavy synchronous work (database queries, third-party API calls, image processing), it may be timing out before it returns a response. The fix is to accept the webhook immediately, return 200, and queue the work asynchronously.
# Accept fast, process async
@csrf_exempt
def stripe_webhook(request):
payload = request.body
process_stripe_event.delay(payload) # Celery/BullMQ task
return HttpResponse(status=200)
Step 4: Check firewall and network rules
If you're running on a cloud host with a restrictive security group, or behind a firewall that blocks unknown inbound IPs, webhook requests may be silently dropped before they reach your application.
Most providers publish their outbound IP ranges. Stripe's are at stripe.com/files/docs/ips. GitHub's are in the hooks key of api.github.com/meta. If you have IP allowlisting enabled, add the provider's ranges.
Also check: load balancer config (some load balancers strip headers or block POST requests without explicit rules), reverse proxy settings (nginx or Caddy may return 413 if the payload exceeds a configured body size limit), and WAF rules (Web Application Firewalls sometimes block requests that look like automated POSTs).
Step 5: Use a webhook inspector to isolate the problem
At this point, if you still don't know where the request is going, swap your endpoint URL with a public webhook inspector. This confirms whether the delivery problem is on the provider's side, in your network, or in your application code.
Payloader gives you a public URL that captures every incoming request and shows what was received: headers, body, and timestamp. You can also replay captured requests to your local machine. To use it:
- Create a free endpoint on Payloader. You get a URL like
https://hook.payloader.dev/e/abc123def456. - Temporarily replace your webhook URL in the provider's dashboard with the Payloader URL.
- Trigger the event again.
- Check Payloader to see if the request arrived.
If the request arrives in Payloader but not in your application, the delivery is working and the problem is in your server setup (firewall, routing, or handler). If nothing arrives in Payloader either, the provider is not sending the request, or there is a DNS or routing issue between the provider and any public URL.
This step isolates whether the problem is delivery or your handler, which tells you exactly where to focus next.
Step 6: Check signature verification
If your application runs signature verification before processing the event, a failed check can cause the request to be rejected silently. From the outside, this looks identical to nothing arriving at all: the request hits your server, but no processing happens and no log entries are written (if verification failure returns early without logging).
Common signature verification mistakes:
- Wrong secret. Using the live-mode signing secret in test mode, or the wrong endpoint's secret if you have multiple webhooks registered.
- Reading the body twice. Signature verification requires the raw request body. Some frameworks or middleware will parse the body into an object before your handler runs. Once parsed, the raw bytes are gone and verification fails. Read the raw body first, verify, then parse.
- Body encoding mismatch. The provider signs the raw bytes of the payload. If your framework re-serializes the JSON (even identically), the signature check fails because byte order or whitespace may differ.
# Correct: verify on raw bytes before parsing
@csrf_exempt
def stripe_webhook(request):
payload = request.body # raw bytes
sig_header = request.META.get('HTTP_STRIPE_SIGNATURE')
try:
event = stripe.Webhook.construct_event(
payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
)
except stripe.error.SignatureVerificationError:
return HttpResponse(status=400)
# Now safe to process event
...
Common mistakes checklist
If you've gone through all six steps and still haven't found the issue, run through this list.
- Wrong Content-Type expected. Your route may only accept
application/jsonbut the provider sendsapplication/x-www-form-urlencoded, or vice versa. Check the provider's docs for the exact content type. - Reading request body twice. Logging middleware that reads and doesn't restore
request.bodywill cause your handler to receive an empty body. This is a common Django pitfall. - Async handler missing await. In Node.js, an async route handler that throws inside a promise without
awaitwill silently swallow the error and may return 200 without doing any work. - Route not registered. The handler exists but the route was never wired up in your router. The request hits a catch-all 404 handler instead.
- CSRF protection. Django's CSRF middleware will reject any POST without a valid CSRF token. Webhook endpoints need
@csrf_exempt. Many frameworks have similar protections that must be explicitly disabled for webhook routes. - SSL certificate issue. Providers verify your server's SSL certificate. A self-signed cert or an expired certificate will cause the delivery to fail at the TLS handshake stage.
- Subscription not active. Some providers (GitHub Apps, for example) will only send webhooks if your subscription or app installation is active. Check the status of the integration in the provider's dashboard.
Once you find it
Webhook failures are almost always caused by one of the issues above. The key is to work systematically: confirm the event was sent, confirm the URL is correct, confirm your server is reachable and responding, and then narrow it down to application code.
Adding a persistent webhook inspector to your workflow means you have a request log to look back at when something breaks in production, without needing to reproduce the event from scratch.