All Posts
WebhooksTestingReal World

Webhook Testing Was My Least Favourite Part of Development — Until I Changed My Setup

Kiran MayeeFebruary 4, 20258 min read

I've built Stripe integrations for three different products. Each time, the webhook testing experience was the same: painful, slow, and weirdly prone to tunnel-related failures that had nothing to do with my actual code.

Here's what the old workflow looked like:

  1. Start ngrok in a terminal window. Get a new URL.
  2. Update the webhook URL in the Stripe dashboard. Wait for it to save.
  3. Trigger a test event from Stripe's dashboard.
  4. Wait for the event to be delivered. Wonder if ngrok is still running.
  5. Check ngrok inspector at localhost:4040. Find the request. Look at the payload.
  6. Find the bug. Fix the code.
  7. Trigger another event. Wait.
  8. Repeat from step 3.

If ngrok timed out (it does, on the free plan), restart ngrok, get a new URL, update Stripe. Again. Each full cycle took 15–25 minutes if I was unlucky with the tunnel.

What Changed: Separating Webhook Delivery Testing From Business Logic Testing

The insight that fixed this: I was conflating two separate problems.

  • Problem 1: Does my HTTP endpoint correctly receive, validate, and process webhook payloads?
  • Problem 2: Does Stripe successfully deliver to my production endpoint?

Problem 2 is a production monitoring concern. Problem 1 is a development concern. I was using Stripe's infrastructure to debug Problem 1, which is why every bug fix required a full round-trip through Stripe's servers and ngrok.

The New Setup

For local development: I write unit tests directly against my webhook handler function. No HTTP. No Stripe. Just: call the handler with a test payload, assert the result.

import { handleStripeWebhook } from "./webhook-handler"
import { makeStripeEvent } from "./test-utils"

test("sets order status to paid on checkout.session.completed", async () => {
  const event = makeStripeEvent("checkout.session.completed", {
    id: "cs_test_abc",
    amount_total: 4999,
    customer_email: "user@example.com",
  })
  
  const result = await handleStripeWebhook(event)
  
  expect(result.statusCode).toBe(200)
  expect(await db.orders.findByStripeId("cs_test_abc")).toMatchObject({ 
    status: "paid" 
  })
})

This resolves in milliseconds. Completely deterministic. No internet required. I can run it 50 times while iterating on the handler logic.

For integration testing: I use moqapi.dev's built-in webhook test feature. In my moqapi.dev project, I have webhook configurations with my staging endpoint as the target. When I want to test the full flow — real HTTP POST, real signature validation, real response — I hit the Test button in the moqapi.dev dashboard.

It fires a POST to my staging endpoint immediately, logs the response status, headers, body, and latency in the unified log viewer. If I get a 500, I see the exact payload that caused it. I can resend it with one click after fixing the bug.

No ngrok. No Stripe dashboard. No tunnel management.

For Signature Validation

The one thing you always need to test properly is the HMAC signature validation. Don't skip this step because "it's just the test environment." A handler that skips signature validation in production is a security hole.

The Stripe docs on webhook signature verification are good. The same HMAC-SHA256 pattern applies to GitHub webhooks, Twilio, and any platform that signs their payloads. Implement it once as a middleware function, test it with a unit test that uses a known secret + payload + expected signature, and you're covered.

The Production Monitoring Piece

Once your handler is live, you need visibility into delivery failures. moqapi.dev's log viewer gives me a feed of every webhook delivery attempt with status codes and timing. I have a cron job set up that runs a health check every 5 minutes and posts to Slack if a webhook endpoint starts returning 5xx consistently — before users notice something is broken.

The complete webhook testing guide on this blog has more detail on the signature validation and monitoring patterns.

Share this article:

About the Author

Kiran Mayee

Founder and sole developer of moqapi.dev. Full-stack engineer with deep experience in API platforms, serverless runtimes, and developer tooling. Built moqapi to solve the mock data and deployment friction she experienced firsthand building production APIs.

Ready to build?

Start deploying serverless functions in under a minute.

Get Started Free