1 min read

Debugging Webhooks Without Deploying to Find Out

Replay a Stripe webhook against your laptop, inspect every header, and stop guessing what the payload looked like.

Nilan SahaNilan Saha

Webhooks are fire-and-forget by design, which is exactly what makes them miserable to debug. The provider sends a request, something throws, and all you have is a 500 in a log somewhere. Let's make that loop tight.

Expose localhost over HTTPS

Most providers won't deliver to http://localhost. Open a public link and point the webhook at it:

portline share api --public
# → https://teal-otter-42.portline.live  →  http://api.localhost

That subdomain forwards straight to your machine. TLS is terminated for you, so the provider sees a valid HTTPS endpoint.

Inspect the exact request

Every request that hits the tunnel shows up in the inspector with full headers and body. No more console.log(JSON.stringify(req.body)) redeploys:

POST /webhooks/stripe HTTP/1.1
Host: teal-otter-42.portline.live
Stripe-Signature: t=1717286400,v1=8f3a...c2d1
Content-Type: application/json

{
  "id": "evt_1PabcdEfgh",
  "type": "checkout.session.completed",
  "data": { "object": { "amount_total": 1900 } }
}

Replay until it's green

The killer feature is replay. Fix your handler, hit replay, and the identical request is delivered again — same signature, same body:

// app/api/webhooks/stripe/route.ts
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export const POST = async (req: Request) => {
  const sig = req.headers.get("stripe-signature");
  const body = await req.text();

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, sig!, process.env.STRIPE_WHSEC!);
  } catch (err) {
    // Replay the captured request after fixing this — no new test charge needed.
    return new Response(`bad signature: ${(err as Error).message}`, { status: 400 });
  }

  if (event.type === "checkout.session.completed") {
    // ...fulfill the order
  }

  return Response.json({ received: true });
};

The loop, shortened

  1. Trigger the event once from the provider.
  2. Inspect the captured request.
  3. Edit your handler.
  4. Replay — no need to re-trigger upstream.

That fourth step is the whole game. You go from "wait for another real event" to "press a button" — and your debugging loop drops from minutes to seconds.