You’ve deployed your webhook endpoint. The external service says it’s sending requests. But your logs show nothing. Here’s how to systematically debug incoming webhook issues.

The Debugging Checklist

Before diving deep, run through these quick checks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 1. Is your endpoint actually reachable?
curl -X POST https://your-domain.com/webhook \
  -H "Content-Type: application/json" \
  -d '{"test": true}' \
  -w "\nHTTP Status: %{http_code}\n"

# 2. Is DNS resolving correctly?
dig your-domain.com +short

# 3. Is the port open?
nc -zv your-domain.com 443

If your own curl request doesn’t reach the endpoint, the problem is infrastructure. If it does but the external service’s requests don’t arrive, the problem is somewhere in between.

Problem 1: The Endpoint Returns Success But Nothing Happens

Your webhook URL returns 200, but your application logic never runs.

Common cause: Multiple route handlers, and the wrong one is matching.

1
2
3
4
5
6
7
8
# FastAPI example - order matters!
@app.post("/webhook")
async def generic_webhook():
    return {"status": "ok"}  # This catches everything

@app.post("/webhook/stripe")  # Never reached if above matches first
async def stripe_webhook():
    process_stripe_event()

Fix: Be explicit with your routes, or check the order of registration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# More specific routes first
@app.post("/webhook/stripe")
async def stripe_webhook(request: Request):
    # Stripe-specific handling
    pass

@app.post("/webhook/{provider}")
async def generic_webhook(provider: str, request: Request):
    # Fallback for other providers
    pass

Problem 2: Requests Arrive But Get Rejected

The service shows “delivered” but your server returns 4xx/5xx.

Debug by logging everything:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from fastapi import FastAPI, Request
import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

@app.post("/webhook")
async def webhook(request: Request):
    # Log before any processing
    body = await request.body()
    logger.info(f"Headers: {dict(request.headers)}")
    logger.info(f"Body: {body.decode()}")
    
    try:
        # Your actual processing
        data = await request.json()
        process_webhook(data)
        return {"status": "ok"}
    except Exception as e:
        logger.error(f"Webhook failed: {e}")
        raise

Common causes:

  • Missing required headers (signature verification failing)
  • Body parsed as wrong content type
  • Async function not awaited properly

Problem 3: Requests Never Arrive At All

The external service says “sent” but your server shows zero incoming requests.

Check 1: Is a proxy/load balancer eating requests?

1
2
3
4
5
# Check nginx access logs
tail -f /var/log/nginx/access.log | grep webhook

# Check if nginx is returning errors before hitting your app
tail -f /var/log/nginx/error.log

Check 2: Is a firewall blocking the source IP?

Many webhook providers publish their IP ranges. Make sure they’re allowed:

1
2
3
4
5
6
# Check iptables
sudo iptables -L -n | grep -i drop

# Check cloud security groups (AWS example)
aws ec2 describe-security-groups --group-ids sg-xxxxx \
  --query 'SecurityGroups[].IpPermissions'

Check 3: Is the webhook URL actually configured correctly?

This sounds obvious, but check for:

  • HTTP vs HTTPS mismatch
  • Trailing slash differences (/webhook vs /webhook/)
  • URL encoding issues in the path
1
2
3
4
5
# What the provider thinks it's sending to:
echo "https://example.com/webhook?token=abc%20def"

# What you might have configured:
echo "https://example.com/webhook?token=abc def"  # Space breaks it

Problem 4: Intermittent Failures

Sometimes requests arrive, sometimes they don’t.

Likely cause: Timeout issues. Your endpoint takes too long and the sender gives up.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Bad: Processing inline
@app.post("/webhook")
async def webhook(data: dict):
    result = slow_database_operation(data)  # Takes 30 seconds
    send_notifications(result)  # Another 10 seconds
    return {"status": "ok"}  # Sender already timed out

# Good: Accept immediately, process async
from fastapi import BackgroundTasks

@app.post("/webhook")
async def webhook(data: dict, background_tasks: BackgroundTasks):
    background_tasks.add_task(process_webhook_async, data)
    return {"status": "ok"}  # Returns in <100ms

Most webhook senders expect a response within 5-30 seconds. If you can’t respond in time, they’ll retry—and you might process duplicates.

The Nuclear Option: Request Bin

When nothing else works, eliminate your infrastructure entirely:

  1. Go to webhook.site or requestbin.com
  2. Get a temporary URL
  3. Configure the external service to send to that URL
  4. Verify requests actually arrive

If requests arrive at the bin but not your server, the problem is definitely your infrastructure. If they don’t arrive at the bin either, the problem is on the sender’s side.

Quick Reference: Status Code Meanings

When debugging, know what the sender sees:

StatusSender’s Interpretation
200-299Success, won’t retry
400-499Your fault, may not retry
500-599Their fault(?), will retry
TimeoutWill retry

Pro tip: Always return 200 immediately, even if processing fails. Handle failures in a retry queue on your side rather than relying on the sender’s retry logic.

Checklist Summary

  1. ✅ Can you curl your own endpoint?
  2. ✅ Are logs showing any incoming requests?
  3. ✅ Check proxy/nginx/load balancer logs
  4. ✅ Verify firewall allows source IPs
  5. ✅ Confirm exact URL matches (http/https, trailing slash)
  6. ✅ Test with a request bin to isolate the problem
  7. ✅ Ensure response time is under timeout threshold

The webhook that silently fails is the worst kind of bug—no error message, no stack trace, just absence. Systematic elimination is the only way through.