What Is a Webhook? How They Work and How to Test Them Locally

Webhooks power everything from payment notifications to GitHub CI triggers. Learn what they are, how they differ from REST APIs, and how to test them during local development.

What Is a Webhook? How They Work and How to Test Them Locally

You've set up Stripe payments. Your checkout works. But how does your server know when a payment actually succeeds — especially if the user closes the browser mid-flow? The answer is webhooks. They're the backbone of real-time integrations, and understanding them is essential for any backend or full-stack developer.

What Is a Webhook?

A webhook is an HTTP callback — a way for one system to notify another system when something happens. Instead of your app asking "did anything happen?" (polling), the external service calls your app directly with the event data the moment something occurs.

The flow looks like this:

  1. A user completes a payment on Stripe
  2. Stripe sends an HTTP POST request to your server: https://yourapp.com/webhooks/stripe
  3. Your server receives the event payload, processes it, and responds with 200 OK
  4. Stripe marks the webhook as delivered

If your server isn't reachable, or returns a non-2xx status, Stripe will retry — usually with exponential backoff over several hours.

Webhooks vs APIs: The Key Difference

REST APIWebhook
Who initiatesYour app calls the serviceThe service calls your app
WhenWhen you ask (on-demand)When an event occurs (push)
DirectionPullPush
Use caseFetch current dataReact to events in real-time

Common Webhook Use Cases

  • Payment processors (Stripe, PayPal): Payment succeeded, subscription cancelled, invoice failed
  • GitHub/GitLab: Push events, pull request opened, CI pipeline completed
  • Slack: Message received, user joined a channel
  • Shopify: Order created, product updated, customer registered
  • Twilio: SMS received, call ended
  • Zapier/Make: Triggering automation workflows

What Does a Webhook Payload Look Like?

Here's an example Stripe payment_intent.succeeded webhook payload:

{
  "id": "evt_3OqXkP2eZvKYlo2C1vHabc12",
  "object": "event",
  "type": "payment_intent.succeeded",
  "created": 1710432000,
  "data": {
    "object": {
      "id": "pi_3OqXkP2eZvKYlo2C1abc456",
      "amount": 4999,
      "currency": "usd",
      "status": "succeeded",
      "metadata": {
        "order_id": "ORD-8821"
      }
    }
  }
}

The Problem with Testing Webhooks Locally

Your local development server is at http://localhost:8000. Stripe (or any external service) cannot reach that — it's not on the public internet. This creates a classic problem: how do you test webhook handling code before deploying?

Option 1: Tunneling tools (ngrok, Cloudflare Tunnel)

Tools like ngrok create a public URL that tunnels to your localhost:

ngrok http 8000
# Forwarding: https://a3f8-91-201-x-x.ngrok.io → localhost:8000

You give this URL to Stripe as your webhook endpoint. Works well, but the free tier has URL rotation and session limits.

Option 2: Use a mock API to capture and inspect payloads

If you want to inspect what a webhook payload looks like without writing any handler code yet, you can point the webhook at a mock API endpoint and check the traffic logs. Services like MockServer log every incoming request — headers, body, IP — so you can see exactly what Stripe or GitHub is sending.

Option 3: Replay saved payloads in development

Most webhook providers have a test mode that sends sample events. Capture one real payload, save it as a JSON fixture, and write a script that POSTs it to your local handler:

curl -X POST http://localhost:8000/webhooks/stripe \
  -H "Content-Type: application/json" \
  -H "Stripe-Signature: test_sig" \
  -d @stripe_payment_succeeded.json

This approach is fast, repeatable, and doesn't require any external tools.

How to Handle Webhooks Securely

Anyone can POST to your webhook URL. You must verify that the request is genuinely from the service you expect, not a malicious actor.

Most services sign their payloads with a secret key. Stripe, for example, sends a Stripe-Signature header. You verify it using their SDK:

// Node.js
const event = stripe.webhooks.constructEvent(
  req.body,
  req.headers['stripe-signature'],
  process.env.STRIPE_WEBHOOK_SECRET
);

Always:

  • Verify the signature before processing the event
  • Respond with 200 OK immediately, then process asynchronously
  • Make your handler idempotent — the same event might be delivered more than once
  • Log every received webhook with its event ID for debugging

Conclusion

Webhooks are how modern SaaS services talk to your backend in real time. Understanding the push-based model, knowing how to test locally, and implementing proper signature verification are skills that will serve you across dozens of integrations throughout your career.