---
title: "Debug Webhooks: A Practical Troubleshooting Guide (2026) | WebhookRelay"
meta:
  "og:description": "Troubleshoot webhooks step by step: when an event never arrives, when it arrives but fails (signature mismatch, wrong content-type, body parsing), plus duplicates, ordering, timeouts and retries. Inspect, forward to localhost, and replay."
  "og:title": "Debug Webhooks: A Practical Troubleshooting Guide (2026)"
  description: "Troubleshoot webhooks step by step: when an event never arrives, when it arrives but fails (signature mismatch, wrong content-type, body parsing), plus duplicates, ordering, timeouts and retries. Inspect, forward to localhost, and replay."
---

![Stripes](https://webhookrelay.com/blog/how-to-debug-webhooks/images/stripes.svg)

# **Debug Webhooks: A Practical Troubleshooting Guide (2026)**

Troubleshoot webhooks step by step: when an event never arrives, when it arrives but fails (signature mismatch, wrong content-type, body parsing), plus duplicates, ordering, timeouts and retries. Inspect, forward to localhost, and replay.

A webhook that "doesn't work" almost always fails at one of two points: **it never reaches your handler**, or **it reaches your handler and the handler chokes on it**. The trick to debugging webhooks quickly is figuring out which of those two it is — and then you only have a handful of likely causes to check.

This guide walks the failure modes in order, with the fastest way to confirm each one. If you haven't already, the companion piece on [how to test webhooks](https://webhookrelay.com/blog/how-to-debug-webhooks/blog/how-to-test-webhooks/) covers the setup; here we focus on what to do when something's broken.

## [First: is it arriving at all?](#first-is-it-arriving-at-all)

Before you touch your code, find out whether the request is even leaving the provider. Most providers keep a delivery log — Stripe, GitHub and Shopify all show every attempt and the HTTP response they received. Look there first.

**The URL is wrong.** This is the boring, common one. A trailing slash, the wrong path (`/webhook` vs `/webhooks`), `http` instead of `https`, or a stale endpoint from a previous environment. Copy the URL straight from your dashboard and compare it character for character with what the provider has saved.

**The provider isn't configured to send the event.** Many providers only deliver the event types you subscribe to. If you wired up `payment_intent.succeeded` but you're triggering a `checkout.session.completed`, nothing arrives — and that's correct behaviour, not a bug. Confirm the event type is enabled.

**A firewall or private network is blocking it.** If your endpoint lives on `localhost`, a private LAN, or behind a corporate firewall, the provider physically can't reach it. Public providers can only call public URLs. This is exactly what [Webhook Relay](https://webhookrelay.com) solves: it exposes a stable public endpoint and the [relay agent](https://webhookrelay.com/blog/how-to-debug-webhooks/webhooks/) forwards each request inbound to your private destination, no open ports required.

**Confirm the request leaves the provider.** Point the provider at a free [Webhook Bin](https://webhookrelay.com/blog/how-to-debug-webhooks/webhook-bin/) and trigger the event. If it shows up in the bin, the provider is sending correctly and the problem is downstream (your URL, server or firewall). If it _doesn't_ show up in the bin, the problem is on the provider's side — wrong event subscription, disabled endpoint, or an account/auth issue.

## [It's arriving but failing](#its-arriving-but-failing)

Now the request is reaching you and something downstream rejects it. Inspect the **exact** request first — in a Webhook Bin or your Webhook Relay request log — so you're debugging facts, not assumptions.

### [Signature mismatch](#signature-mismatch)

If your handler returns 401/400 on a "valid" webhook, signature verification is the usual suspect. The number-one cause: **you're verifying against a parsed-and-re-serialized body instead of the raw bytes.** Re-encoding JSON changes whitespace and key ordering, and the HMAC no longer matches.

Checklist:

- Verify against the **raw request body**, exactly as received.
- Use the **correct signing secret** (the per-endpoint secret, not your API key — they're often different).
- Read the **right header**. GitHub sends `X-Hub-Signature-256` (SHA256); Stripe and Shopify also use SHA256 but format the header differently.

Walk through the mechanics in [how to verify a webhook signature](https://webhookrelay.com/blog/how-to-debug-webhooks/blog/verify-webhook-signature/), and use the free [HMAC generator & verifier](https://webhookrelay.com/blog/how-to-debug-webhooks/hmac-verification/) to paste the raw body, the secret and the header value and confirm they actually match — that isolates whether the problem is your verification code or the data feeding it.

### [Wrong content-type](#wrong-content-type)

Your framework picks a body parser based on the `Content-Type` header. If the provider sends `application/x-www-form-urlencoded` (Twilio, some Slack endpoints) but your code assumes JSON — or vice versa — you'll get an empty object or a parse error even though the body is perfectly fine. Check the header in the captured request and match your parser to it.

### [Body parsing](#body-parsing)

Even with the right content-type, the structure may not be what you expect. Some providers wrap the real payload in an envelope (`{ "data": { "object": {...} } }`), send a JSON string inside a form field, or batch multiple events into an array. Inspect a real captured payload before writing field accessors, and guard against missing keys instead of assuming a shape.

## [Duplicates and out-of-order events](#duplicates-and-out-of-order-events)

Webhook delivery is **at-least-once**, not exactly-once. Two realities follow:

- **Duplicates.** A provider that times out waiting for your `2xx` will retry — even if your handler actually succeeded. Make handlers **idempotent**: key off the event ID (or a hash of the payload) and skip anything you've already processed.
- **Ordering.** Events can arrive out of order. A `subscription.updated` can land before the `subscription.created`. Don't assume sequence; reconcile against the provider's current state, or fetch the latest object by ID when order matters.

## [Timeouts and retries](#timeouts-and-retries)

Most providers give your endpoint a short window (often a few seconds) to respond, and treat anything that's slow or non-`2xx` as a failure to retry.

- **Respond fast, work later.** Acknowledge with `2xx` immediately, then do heavy lifting (DB writes, third-party calls) asynchronously. A handler that does all its work before responding is the most common cause of phantom retries and duplicates.
- **Return the right status.** A `500` invites retries; a `400` may make the provider give up. Return `2xx` once you've safely accepted the event.

## [The debugging workflow](#the-debugging-workflow)

A loop that makes all of the above fast:

1. **Inspect** — send the webhook to a [Webhook Bin](https://webhookrelay.com/blog/how-to-debug-webhooks/webhook-bin/) to capture the exact method, headers, query and body. Now you know what's really being sent.
2. **Forward to localhost** — run the [relay agent](https://webhookrelay.com/blog/how-to-debug-webhooks/webhooks/) and forward into your dev environment so you can set breakpoints in your IDE against live events:```
relay forward --bucket my-app http://localhost:8080/webhook
``` The agent connects outbound, so there's nothing to expose and the public URL stays stable while you iterate.
3. **Replay** — capture a request once and resend it as many times as you need. You re-run the handler without re-triggering the source event (no need to create another payment to test the `payment.succeeded` path).

Inspect, forward, replay — repeat until the handler is green.

## [Common pitfalls, at a glance](#common-pitfalls-at-a-glance)

- Verifying the signature against a re-serialized body instead of the raw bytes.
- Assuming JSON when the provider sent form-encoded (check `Content-Type`).
- Doing slow work before responding, triggering retries and duplicates.
- Non-idempotent handlers that double-process retried events.
- A stale or private endpoint URL the provider can't actually reach.

## [Start debugging](#start-debugging)

[Open a Webhook Bin](https://webhookrelay.com/blog/how-to-debug-webhooks/webhook-bin/) to see the exact request a provider is sending, or [create a free account](https://my.webhookrelay.com/register) to forward live webhooks into your IDE and replay them against your handler until it works.