DEV Community

Jack
Jack

Posted on • Originally published at anethoth.com

The Webhook Debugging Checklist I Wish I Had 3 Years Ago

I have spent hundreds of hours debugging webhooks. Stripe payments not processing. GitHub Actions not triggering. Shopify orders getting lost.

Every time, I wished I had a systematic approach instead of flailing around. So I wrote one.

The Checklist

Step 1: Verify the webhook is actually being sent

Before debugging YOUR code, confirm the sender is actually firing:

  • Stripe: Dashboard > Developers > Webhooks > Recent events
  • GitHub: Settings > Webhooks > Recent Deliveries
  • Shopify: Settings > Notifications > Webhooks > Check responses

If the sender shows no attempts, the issue is on their side (wrong event type selected, webhook disabled, etc).

Step 2: Capture the raw request

Do not guess what is being sent. Inspect the actual payload:

# Create a temporary capture endpoint (free, no signup)
curl -X POST https://webhookvault.anethoth.com/api/v1/demo/endpoints

# Response: {"url": "https://webhookvault.anethoth.com/hook/abc123", ...}
# Point your webhook sender to this URL temporarily
Enter fullscreen mode Exit fullscreen mode

Or use the echo endpoint to see exactly what arrives:

curl -X POST https://webhookvault.anethoth.com/api/v1/echo \
  -H "Content-Type: application/json" \
  -H "X-Custom-Header: test" \
  -d ''{"event": "test"}'' 
Enter fullscreen mode Exit fullscreen mode

Step 3: Check your response code

Webhook senders care about your response:

Code Meaning Sender behavior
200-299 Success Marks delivered
301-399 Redirect Most senders DONT follow redirects
400-499 Client error Retries then disables
500-599 Server error Retries with backoff
Timeout No response Retries with backoff

Common gotcha: Returning 200 after your handler crashes. Many frameworks catch exceptions and return 200 by default.

Step 4: Verify the signature

If you are not verifying webhook signatures, stop everything and add it. An unverified webhook endpoint is an unauthenticated API that anyone can call.

import stripe

@app.post("/stripe-webhook")
async def stripe_webhook(request: Request):
    payload = await request.body()
    sig = request.headers.get("stripe-signature")

    try:
        event = stripe.Webhook.construct_event(payload, sig, webhook_secret)
    except stripe.error.SignatureVerificationError:
        raise HTTPException(status_code=400, detail="Invalid signature")

    handle_event(event)
Enter fullscreen mode Exit fullscreen mode

Step 5: Handle idempotency

Webhooks can fire multiple times for the same event. Your handler MUST be idempotent:

@app.post("/webhook")
async def webhook(request: Request):
    data = await request.json()
    event_id = data["id"]

    if db.execute("SELECT 1 FROM processed_events WHERE event_id = ?", (event_id,)).fetchone():
        return {"ok": True, "status": "already_processed"}

    process_event(data)
    db.execute("INSERT INTO processed_events (event_id) VALUES (?)", (event_id,))
    return {"ok": True}
Enter fullscreen mode Exit fullscreen mode

Step 6: Handle out-of-order delivery

Webhooks are NOT guaranteed to arrive in order. Design your handler to be order-independent, or use event timestamps to detect and handle out-of-order delivery.

Quick debugging toolkit

Free tools you can use right now:

Tool What it does
WebhookVault Echo Mirror any HTTP request back as JSON
WebhookVault Headers Inspect your request headers
WebhookVault Demo Create temporary capture endpoints

What is the worst webhook debugging experience you have had? Mine was a Shopify webhook that only failed on orders over 999.99 because of a locale-dependent number formatting issue.

Top comments (0)