All Posts
WebhooksTestingDeveloper Tools

How to Test Webhooks Locally and in Production: The Complete Guide

Kiran MayeeAugust 1, 20258 min read

Webhooks are HTTP callbacks: a remote service sends a POST request to your server when something happens. Testing them is awkward by default — your local development server isn't reachable from the internet, payloads are one-shot, and debugging a failed delivery means convincing Stripe or GitHub to resend it.

This guide covers four phases of the webhook development lifecycle and the right tool for each.

Phase 1: Local Development

The standard tool is ngrok — it tunnels requests from a public URL to your localhost:3000.

npx ngrok http 3000
# Forwarding: https://abc123.ngrok.io → http://localhost:3000

# Register this URL in your Stripe/GitHub/etc. dashboard
# https://abc123.ngrok.io/api/webhooks/stripe

The ngrok dashboard at http://localhost:4040 shows every request in real time — headers, body, timing, and the ability to replay any request with one click. This is invaluable when your handler returns a 500 and you want to iterate without retrieving the event again.

Other options: Cloudflare Tunnel (cloudflared tunnel) is free with no time limits, and VS Code Dev Tunnels (built into the editor) require no install at all.

Phase 2: Writing the Handler

A production-grade webhook handler does three things before any business logic:

  1. Validates the signature. Every major platform (Stripe, GitHub, Twilio) signs requests with HMAC-SHA256. Reject unsigned requests immediately.
  2. Acknowledges receipt fast. Return 200 OK within two seconds. Move slow work to a background queue.
  3. Handles duplicates idempotently. Webhooks are delivered at-least-once. Store the event ID and skip re-processing if you've seen it before.
import { createHmac, timingSafeEqual } from "crypto"

function verifySignature(payload: string, signature: string, secret: string) {
  const expected = createHmac("sha256", secret).update(payload).digest("hex")
  return timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}

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

  if (!verifySignature(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)) {
    return new Response("Unauthorized", { status: 401 })
  }
  
  const event = JSON.parse(body)
  // enqueue for async processing, return immediately
  await queue.enqueue(event)
  return new Response("OK", { status: 200 })
}

Phase 3: Integration Testing

With moqapi.dev webhooks, you configure delivery endpoints and test against real HTTPS URLs in staging. The platform provides built-in test delivery — hit the Test button on any webhook config and it sends a sample payload immediately, logging the request, response, and latency.

For CI, keep a staging endpoint running and add a job that sends a test payload after every deploy:

curl -X POST https://api.yourapp.com/api/webhooks/test   -H "Authorization: Bearer $TOKEN"   -d '{"webhookId": "wh_abc123"}'

Phase 4: Production Monitoring

Once live, you need visibility into:

  • Delivery success rate — what percentage of webhooks are acknowledged with 2xx?
  • Retry storms — failed deliveries trigger retries; a broken handler can cause hundreds of duplicate events.
  • Latency — webhook providers give up after 5–30 seconds depending on the platform.

moqapi.dev's unified log viewer captures every webhook delivery attempt, status code, and response time in one searchable view. Filter by webhook ID, date range, or status to spot degraded endpoints before they fully fail.

Quick Reference: Webhook Signature Libraries

  • Stripe: stripe.webhooks.constructEvent()
  • GitHub: @octokit/webhooks
  • Twilio: twilio.validateRequest()
  • Custom: crypto.createHmac("sha256", secret).update(payload).digest("hex")
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