How to debug Stripe webhooks (signature, body, replay)

If your Stripe webhook handler is broken, the problem is almost always one of three things: wrong endpoint URL, wrong signature verification, or wrong event parsing. This walkthrough fixes each one using RequestBin as the inspection + replay layer.

How to debug Stripe webhooks (signature, body, replay)

You wired a Stripe webhook to your handler. The Stripe Dashboard says the event was delivered. Your code never ran. Or it ran but returned 500. Or it ran but the event is parsed wrong. Now what?

Three failure modes, three fixes. Each one easier when you can see the raw request Stripe actually sent.

Setup: get a bin URL

Sign up at requestbin.net (no credit card on FREE). Create a bin → copy the URL → paste into your Stripe Dashboard → Developers → Webhooks → Add endpoint.

Now every Stripe event also hits the bin where you can inspect it.

Failure mode 1: signature verification mismatch

Symptom: your handler logs "Stripe-Signature header missing" or "signature does not match".

Open the bin in RequestBin, click the captured request, look at the headers. You should see:

Stripe-Signature: t=1716220800,v1=5257a869e7eceb...

Three things to check:

  1. Is your handler reading the raw body? Signature verification needs the exact bytes Stripe sent. If you're using Express, that means express.raw({ type: 'application/json' }) for the webhook route, NOT the global express.json() middleware. The latter parses and reserialises the body, which breaks the HMAC.
  2. Is your webhook secret right? The STRIPE_WEBHOOK_SECRET env var must match the signing secret shown in the Stripe Dashboard for THIS specific endpoint. There's a different secret per endpoint.
  3. Is the timestamp drifting? Stripe rejects signatures older than 5 minutes by default. If your server clock is off, signatures fail intermittently.

Failure mode 2: handler never runs

Symptom: Stripe Dashboard shows the delivery succeeded (200 from your endpoint) but your handler logs nothing.

If the bin shows the request landed but your local code didn't, you're either:

  • Not pointed at the right URL — check your .env matches Stripe's endpoint URL
  • Running a different process than you think — kill all node processes and start fresh
  • Using the wrong route — Stripe POSTs to the URL exactly as configured, no path rewriting

Failure mode 3: event parsing wrong

Symptom: handler runs, but pulls the wrong field from the event payload.

Open the bin, look at the Body tab. Stripe payloads look like:

{
  "id": "evt_3OabcDEFGH",
  "type": "payment_intent.succeeded",
  "data": {
    "object": {
      "id": "pi_3OabcDEFGH",
      "amount": 2000
    }
  }
}

The PaymentIntent ID is at data.object.id, not id. The id at the top is the EVENT id, not the resource id. Mixing them up will silently break your idempotency logic.

Replay an event into your local code

Once you've caught a real event in the bin, click Replay on the inspector — RequestBin sends the same request again to your endpoint. Useful when your local code crashes mid-handler and you want to re-run the same event after fixing the bug. Free on every plan.

Replay with edits

If you want to mutate the payload first (e.g. test what happens with a malformed amount), use Replay & Edit — opens the request in the HTTP Requester where you can change headers, body, query params before sending. This is a Pro feature ($12/mo).

Alternative: Stripe CLI

Stripe ships a CLI that can forward events to localhost. Use that for the "send fake events at my local dev server" loop. Use RequestBin for the "something's wrong in production and I need to see the actual bytes Stripe is sending" loop.

Going further

Start free →