HookDeploy
Beginner

Webhook Debugging Guide — A Systematic Approach

Systematic approach to debugging webhook issues — delivery logs, signatures, payloads, curl testing, and common HTTP error codes.

HookDeploy Team · May 22, 2026

Webhook bugs are frustrating because the failure is often on someone else’s server sending to yours. This guide gives you a repeatable process to find the problem fast.

Step 1: Confirm the webhook was sent

Before debugging your handler, verify the sender actually dispatched the event.

Provider delivery logs:

  • Stripe: Dashboard → Developers → Webhooks → select endpoint → Recent deliveries
  • GitHub: Repo Settings → Webhooks → Recent Deliveries
  • Shopify: Settings → Notifications → Webhooks → show notifications

Look for the delivery attempt. If there is no entry, the event was not triggered — go back to the provider and fire a test event.

HookDeploy capture: If the sender points at HookDeploy, open your endpoint in the dashboard. If the request is not in the stream, the sender never reached HookDeploy.

Step 2: Check the HTTP response code

The delivery log shows what response the sender received:

CodeMeaningAction
200SuccessSender is happy. Problem is in your handler logic, not delivery
400Bad requestYour handler rejected the payload — check signature verification
401UnauthorizedSignature verification failed
404Not foundWrong URL — check slug/path
413Payload too largeExceeds HookDeploy plan limit or your server’s limit
422LockedHookDeploy endpoint is paused
429Rate limitedPlan limit reached (HookDeploy) or your rate limiter fired
500Server errorYour handler crashed — check application logs
502/503Gateway errorYour server or tunnel is down
TimeoutNo response in timeServer too slow or tunnel disconnected

Step 3: Inspect the payload

Open the request in HookDeploy (or your provider’s delivery log). Check:

  1. Event type — is it the event you expected? (payment_intent.succeeded vs payment_intent.created)
  2. Payload structure — does data.object contain the fields your handler reads?
  3. Headers — is Content-Type correct? Is the signature header present?
  4. Body size — is the payload truncated or empty?
  5. Timestamp — how old is the event?

Common payload surprises:

  • Test mode vs live mode objects have different fields
  • API version differences change payload shape
  • Expanded vs non-expanded objects (Stripe ?expand= parameter)

Step 4: Verify signatures

If your handler returns 400/401, signature verification is the first suspect:

# Check the signature header exists
curl -s "https://api.hookdeploy.dev/v1/endpoints/ID/requests/REQUEST_ID" \
  -H "Authorization: Bearer hd_live_YOUR_KEY" | jq '.data.headers'

Common signature failures:

  • Wrong secret (dev vs prod, CLI vs Dashboard)
  • Parsed JSON body instead of raw bytes
  • Modified body (middleware, gzip decompression)
  • Expired timestamp (Stripe rejects > 5 min old)

Temporarily log the raw body hash and expected signature to compare:

console.log('body length:', rawBody.length);
console.log('sig header:', request.headers['stripe-signature']);

Step 5: Test with curl

Isolate your handler from the provider by sending a test POST:

curl -v -X POST "http://localhost:3000/webhooks/stripe" \
  -H "Content-Type: application/json" \
  -d '{"type":"test","data":{"object":{"id":"test"}}}'

If curl works but provider webhooks fail, the issue is in delivery (URL, tunnel, SSL) not handler logic.

Test HookDeploy capture independently:

curl -X POST "https://hookdeploy.dev/h/YOUR_SLUG" \
  -H "Content-Type: application/json" \
  -d '{"test": true}'

Expected response: {"request_id":"..."}

Step 6: Check your forward/tunnel

If using HookDeploy forwarding or ngrok:

  • Is ngrok/cloudflared still running?
  • Did the tunnel URL change? (ngrok free tier generates new URLs per session)
  • Is your local server listening on the expected port?
  • Check HookDeploy forward status: forward_status_code, forward_error

Step 7: Check handler logs

With a confirmed delivery and valid payload, the bug is in your application code:

  • Add structured logging at the top of your webhook handler
  • Log event type, object ID, and processing result
  • Check for unhandled promise rejections
  • Verify database connections and external API calls within the handler

Return 200 before slow work when possible:

// Good: acknowledge fast, process async
response.status(200).send('OK');
await queue.add('process-webhook', event);

// Bad: Stripe waits while you write to 3 databases
await db1.insert(event);
await db2.sync(event);
await sendEmail(event);
response.status(200).send('OK');  // might timeout

Common error patterns

“It worked yesterday”

  • Tunnel URL changed (ngrok free tier)
  • Webhook secret was rotated
  • Endpoint was paused or deleted
  • Plan limit reached

“Works in test mode, fails in production”

  • Different webhook secrets for test vs live endpoints
  • Different signing secrets — check which endpoint the sender uses

“Handler runs twice”

  • Provider retry because first attempt returned non-2xx or timed out
  • Missing idempotency check — see retry strategies

“Empty body”

  • Sender uses application/x-www-form-urlencoded — check query panel in HookDeploy
  • Body exceeded size limit — check 413 responses

Debugging checklist

  1. Event triggered at provider (delivery log exists)
  2. Request captured in HookDeploy (if using HookDeploy URL)
  3. HTTP response was 200 to sender
  4. Payload contains expected event type and fields
  5. Signature header present and verification passes
  6. Forward/tunnel is active (if using forward)
  7. Handler logs show event received
  8. Handler returns 200 within provider timeout (10–30 seconds)

Related guides