Menu

Webhooks APIs Backend Integration March 2026 ⏱ 14 min read

What Is a Webhook? How They Work With Real Examples

Learn what a webhook is, how webhooks differ from polling, how the HTTP webhook flow works step by step, real webhook examples from Stripe and GitHub, how to receive webhooks in Node.js and Python, and how to verify webhook signatures to prevent spoofed events.

Webhooks are one of those concepts that sounds more complicated than it is. Once you understand what they do, you will recognise them everywhere in modern application development: payment notifications, CI/CD pipeline triggers, chat bot messages, order fulfilment events, and real-time data synchronisation between services. This guide explains webhooks completely, from the basic concept to production-ready implementation with security.

What Is a Webhook?

A webhook is a way for one application to automatically notify another application when a specific event happens. Instead of your application repeatedly asking another service for updates (a pattern called polling), the other service sends an HTTP POST request directly to your application the moment something occurs.

In technical terms: a webhook is an HTTP callback. You register a URL on your server with an external service. When the event you care about occurs (a payment succeeds, a user signs up, a commit is pushed), the external service makes an HTTP POST request to your registered URL with the event data as a JSON body. Your server receives that request, processes the data, and responds with a 200 status code to confirm receipt.

A webhook does not wait to be asked. The moment an event happens, the webhook fires a POST request to your server. Your server receives the event, processes it, and responds. Real time. No wasted requests.

The Simple Analogy That Makes Webhooks Click

The classic analogy for understanding webhooks versus polling works like this. Imagine you are waiting for an important package delivery.

Polling is like calling the courier company every 10 minutes to ask “Has my package arrived yet?” They say no, no, no, no, yes on the fifth call. You have made five calls and received one useful answer. You wasted time on the four unsuccessful calls. If the courier company serves thousands of customers doing the same thing, they are overwhelmed with unnecessary calls.

A webhook is like asking the courier company to call you the moment the package is delivered. You do nothing and wait. The moment the delivery happens, they call you with the details. One notification. Zero wasted effort. You receive the information exactly when it is relevant.

This analogy captures exactly what webhooks do in software: instead of your application repeatedly asking an external service for updates, the external service notifies your application the moment something changes.

Polling vs Webhooks: A Side-by-Side Comparison

Polling and webhooks both solve the same problem: how does your application find out that something happened in an external service? They solve it in opposite ways, with very different performance and scalability characteristics:

Polling (the old way)
Your app asks: “Any new orders?”
60 seconds later
Service: “No”
60 seconds later
Your app asks again: “Any orders?”
60 seconds later
Service: “No”
60 seconds later
Service: “Yes! Here it is.”
5 requests. 4 wasted. Up to 60 seconds of delay. Poor at scale.
Webhooks (the modern way)
Your app registers a webhook URL
Your app waits (doing nothing)
Order is placed by customer
Instantly
Service POSTs event to your URL
Instantly
Your app processes the order
1 request. 0 wasted. Real-time notification. Scales to millions of events.

How Webhooks Work: The Complete Request Flow

A webhook is an HTTP POST request that travels from one server to another. The sequence of events is the same for every webhook in every service:

1
You create a webhook endpoint on your server
You write a route in your web application that accepts POST requests at a specific URL, for example https://yourapp.com/webhooks/stripe. This is your webhook receiver endpoint.
2
You register your webhook URL with the external service
In the service’s dashboard (Stripe, GitHub, Shopify, etc.), you add your webhook URL and choose which events you want to receive. The service stores your URL in association with those event types.
3
The event occurs in the external service
A customer completes a payment, a developer pushes code, a user is created: whatever event you registered for happens inside the external service.
4
The service sends an HTTP POST request to your webhook URL
The service immediately sends a POST request to your registered URL. The request body is a JSON object containing the event type and all relevant data about what happened. A signature header is included so you can verify the request is genuine.
5
Your server verifies the signature and processes the event
Your webhook handler verifies the signature to confirm the request came from the claimed service. It then reads the event type from the JSON payload and executes the appropriate business logic: updating a database, sending an email, triggering another API call.
6
Your server returns a 200 response to acknowledge receipt
Your handler responds with a 200 OK status as quickly as possible. This tells the service the webhook was received and does not need to be retried. Return the 200 first, then process the event asynchronously if processing is slow.

What a Webhook Payload Looks Like

The webhook payload is the JSON body of the POST request the service sends to your URL. Every service structures its payloads differently, but most follow a consistent pattern: a top-level event type field and a data object containing the event details. Here is a real Stripe payment webhook payload:

Stripe webhook payload — payment_intent.succeeded
{ “id”: “evt_3OqX9XLkdFtm5xIW0ABC123”, “type”: “payment_intent.succeeded”, “created”: 1710000000, “livemode”: false, “data”: { “object”: { “id”: “pi_3OqX9XLkdFtm5xIW0XYZ”, “amount”: 14999, “currency”: “inr”, “status”: “succeeded”, “customer”: “cus_ABC123”, “receipt_email”: “customer@example.com”, “metadata”: { “order_id”: “order_9012” } } } }

Your webhook handler reads the type field to know what happened, then navigates into data.object to get the specific details. For a payment_intent.succeeded event, you would read the amount, currency, customer, and receipt_email to update your order records and send a confirmation email.

💡 Format webhook payloads before writing handler code

Before writing any handler code for a new webhook type, paste a sample payload into the JSON Formatter. The formatted view shows the exact field names, nesting levels, and data types clearly. This prevents the most common webhook integration mistake: accessing a field at the wrong nesting level. Copy the sample payload from the service’s documentation, paste it into the formatter, and map out every field your handler will need before writing a single line of code.

Real Webhook Examples From Popular Services

Webhooks power the real-time integrations in every major SaaS product. Here are six concrete examples of how webhooks are used in production applications today. Each one demonstrates a different event type and a different action your server takes in response:

💳 Stripe
payment_intent.succeeded

Stripe sends this webhook when a customer’s payment is successfully charged. Your server receives the payment details including amount, currency, and customer ID.

Your server: update order status to paid, send receipt email, unlock purchased content or subscription.
🐙 GitHub
push

GitHub sends this webhook every time a developer pushes commits to a branch. The payload includes the commit messages, file changes, and the branch name.

Your CI server: trigger a build, run automated tests, deploy to a preview environment, and post the result back to the pull request.
🛒 Shopify
orders/create

Shopify sends this webhook the moment a new order is placed in your store. The payload includes all line items, the customer, the shipping address, and payment status.

Your fulfilment system: reserve inventory, create a shipment, notify the warehouse, and update your ERP system automatically.
📱 Twilio
IncomingMessage

When someone sends an SMS to your Twilio number, Twilio sends a webhook to your server containing the sender’s phone number and the message body.

Your server: look up the sender in your database, generate an automated reply, log the conversation, or route the message to a support agent.
💬 Slack
message (slash command)

When a user types a slash command in Slack (like /deploy production), Slack sends a webhook to your server with the command text and the user who triggered it.

Your server: validate the user has permission, trigger the deployment, and post a confirmation message back to the Slack channel.
🔔 Any Service
Custom event

You can also send webhooks from your own application to notify other systems you build or maintain. This is how microservices communicate events to each other without tight coupling.

Your services: one service fires a webhook when a user upgrades their plan; another service listens and automatically provisions the new features.

Receiving a Webhook in Node.js

Here is a complete Express.js endpoint that correctly receives and handles a Stripe payment webhook. The key details to notice: use express.raw() instead of express.json() for Stripe webhooks (the raw body is required for signature verification), use a switch statement to handle multiple event types, and always return 200 immediately to acknowledge receipt:

Node.js / Express
const express = require(‘express’); const app = express(); // Use raw body parser — required for Stripe signature verification app.post(‘/webhooks/stripe’, express.raw({ type: ‘application/json’ }), (req, res) => { let event; try { event = JSON.parse(req.body); } catch (err) { return res.status(400).send(‘Invalid JSON in webhook body’); } // Handle each event type your integration needs switch (event.type) { case ‘payment_intent.succeeded’: { const payment = event.data.object; console.log(`Payment succeeded: ${payment.id}`); // Update order status, send email, unlock content, etc. break; } case ‘payment_intent.payment_failed’: { const intent = event.data.object; console.log(`Payment failed: ${intent.last_payment_error?.message}`); // Notify the customer, retry logic, etc. break; } case ‘customer.subscription.deleted’: { const subscription = event.data.object; console.log(`Subscription cancelled: ${subscription.id}`); // Downgrade the customer’s account break; } default: console.log(`Unhandled event type: ${event.type}`); } // Always return 200 quickly to acknowledge receipt res.json({ received: true }); } );

Receiving a Webhook in Python (Flask)

The same webhook receiver in Python using Flask. The pattern is identical: parse the JSON body, switch on the event type, process each event, and return 200 immediately:

Python / Flask
from flask import Flask, request, jsonify import json app = Flask(__name__) @app.route(‘/webhooks/stripe’, methods=[‘POST’]) def stripe_webhook(): payload = request.get_data() try: event = json.loads(payload) except json.JSONDecodeError: return jsonify(error=‘Invalid JSON’), 400 event_type = event.get(‘type’) if event_type == ‘payment_intent.succeeded’: payment = event[‘data’][‘object’] print(f”Payment succeeded: {payment[‘id’]}”) # Update database, send email, etc. elif event_type == ‘payment_intent.payment_failed’: intent = event[‘data’][‘object’] print(f”Payment failed: {intent[‘id’]}”) # Handle failure else: print(f”Unhandled event type: {event_type}”) # Always return 200 to acknowledge receipt return jsonify(received=True), 200

HTTP Response Codes and Webhook Retry Behaviour

The response code your server returns to the webhook sender determines whether the service considers the delivery successful or whether it will retry. Getting this right is critical for preventing duplicate event processing:

Response Code Meaning to the Webhook Sender What Happens Next
200 OK Delivery successful No retry. The event is marked as delivered. Always return this for successfully received events, even if your processing fails (handle errors separately).
2xx (any) Delivery successful Any 2xx response is treated as success. 200 is conventional but 201 or 204 also work.
4xx (Client Error) Delivery failed, do not retry The service treats this as a permanent failure and does not retry. Only return 4xx if the request is genuinely malformed (invalid signature, invalid JSON).
5xx (Server Error) Delivery failed, will retry The service retries the webhook with exponential backoff (Stripe retries for up to 72 hours). This can cause duplicate processing if your handler already partially processed the event.
Timeout (no response) Delivery failed, will retry If your server takes too long to respond (typically over 20-30 seconds), the service times out and retries. Always return 200 immediately, then process asynchronously.
⚠ Return 200 first, process asynchronously

If your webhook handler does anything slow (sending emails, calling other APIs, performing database writes), return 200 immediately and move the processing to a background job queue. A slow response causes the sender to timeout and retry the webhook, which means your handler runs twice for the same event. The correct pattern: receive the webhook, acknowledge with 200, queue the event for background processing, and make your background processor idempotent so running it twice for the same event is safe.

Securing Webhooks: Signature Verification

Your webhook URL is an HTTP endpoint on the public internet. Anyone who knows the URL can send a POST request to it. Without signature verification, a malicious actor could send fake webhook events to your server: fake payment success events, fake subscription upgrades, or fake order events that trigger real business actions.

Webhook signature verification solves this. The sending service includes a cryptographic signature in the request headers. The signature is computed using a shared secret key (known only to you and the service) and the webhook body. Your server recomputes the expected signature and compares it with the received one. If they match, the webhook is genuine. If they do not, reject it.

Here is how signature verification looks in practice with Stripe:

Node.js — Stripe webhook with signature verification
const stripe = require(‘stripe’)(process.env.STRIPE_SECRET_KEY); app.post(‘/webhooks/stripe’, express.raw({ type: ‘application/json’ }), (req, res) => { // 1. Read the signature from the request header const signature = req.headers[‘stripe-signature’]; // 2. Verify the signature using Stripe’s SDK let event; try { event = stripe.webhooks.constructEvent( req.body, // raw body (not parsed) signature, // header from Stripe process.env.STRIPE_WEBHOOK_SECRET // your webhook signing secret ); } catch (err) { console.error(`Signature verification failed: ${err.message}`); return res.status(400).send(‘Webhook signature verification failed’); } // 3. Event is verified — safe to process switch (event.type) { case ‘payment_intent.succeeded’: // This is a real Stripe payment — safe to unlock content break; } res.json({ received: true }); } );

The STRIPE_WEBHOOK_SECRET is different from your Stripe API key. You find it in the Stripe dashboard under Developers, Webhooks, then clicking on your webhook endpoint. Each webhook endpoint has its own signing secret. Store it in your environment variables, never in source code.

Webhook security best practices
Always follow these rules for production webhook handlers
🔐 Always verify signatures before processing any webhook event. Never trust the event type or data without first confirming the request came from the claimed service.
📦 Use the raw request body for signature verification. Parsing the JSON before verifying breaks the HMAC check because JSON serialisation can alter whitespace and key order.
🔑 Store signing secrets in environment variables (not in source code). Use a different signing secret per webhook endpoint so a leaked secret affects only one integration.
🔄 Make your handler idempotent: processing the same webhook event twice should produce the same result as processing it once. Services retry webhooks on failure.
Return 200 immediately and process slow operations in a background job queue. Slow responses cause timeouts and retries, which cause duplicate processing.
🔢 Track processed event IDs in your database to detect and skip duplicate deliveries. Use the webhook event ID (like Stripe’s evt_xxx) as a unique key.

Testing Webhooks on Your Local Machine

Webhook services need a publicly accessible URL to send events to. Your localhost is not publicly accessible, which creates a challenge for development and testing. Three tools solve this problem by creating a public URL that tunnels to your local server:

bash — webhook testing tools
# Option 1: Stripe CLI (best for Stripe webhooks) stripe listen –forward-to localhost:3000/webhooks/stripe # Forwards real Stripe test-mode events to your local server # Also handles signature verification automatically in test mode # Option 2: ngrok (works for any webhook service) ngrok http 3000 # Creates a public URL like: https://abc123.ngrok.io # Use this URL as your webhook endpoint in any service’s dashboard # Option 3: Cloudflare Tunnel (free, no rate limits) cloudflared tunnel –url http://localhost:3000 # Creates a permanent tunnel URL you can use in multiple services
✅ Use Stripe CLI for Stripe webhook development

The Stripe CLI is the best tool for Stripe webhook development because it does more than just forward events. It can replay specific events, trigger specific event types on demand, and provides real-time logging of all events and your server’s responses. Install it with brew install stripe/stripe-cli/stripe on macOS or download from the Stripe docs. Run stripe listen –forward-to localhost:3000/webhooks/stripe and Stripe automatically sends test events to your local server with valid signatures.

7-Step Guide to Integrating a Webhook From Any Service

Follow this sequence for integrating any webhook, from any service, into any application. The steps are the same whether you are handling a Stripe payment, a GitHub push, a Shopify order, or a custom webhook from your own service:

  1. Find the service’s webhook documentation and read the payload structure first. Before writing any code, read the service’s webhook documentation and find a sample payload for the event type you want to handle. Paste the sample payload into the JSON Formatter to see the complete structure clearly. Note the exact field names, nesting levels, and data types your handler will need to access. This prevents the most common integration mistake: accessing a field at the wrong path.
  2. Create a webhook endpoint in your application. Write a route that accepts HTTP POST requests at a dedicated URL. Use a clear path that identifies the source: /webhooks/stripe, /webhooks/github, /webhooks/shopify. Do not reuse existing API endpoints for webhooks. The endpoint should initially just log the received body and return 200, so you can inspect real payloads before writing handler logic.
  3. Set up a local tunnel for development testing. Install the Stripe CLI, ngrok, or Cloudflare Tunnel to expose your local server publicly. Use the provided public URL as your webhook endpoint in the service’s dashboard during development. Test with real events in the service’s test or sandbox mode before using production data.
  4. Register your webhook URL in the service’s dashboard and choose your events. In the service’s settings, add your webhook URL and select only the specific event types your integration needs. Subscribing to all events generates unnecessary traffic. For development, use your tunnel URL. For production, use your actual application’s URL. Copy the signing secret immediately and store it in your environment variables.
  5. Add signature verification before any event processing. Implement signature verification using the service’s SDK or by manually computing the HMAC signature. Reject any request that fails verification with a 400 response. Test that verification works correctly by sending a request with an invalid signature and confirming your handler rejects it. Never skip this step for webhooks that trigger financial or security-sensitive operations.
  6. Implement your event handlers with idempotency in mind. For each event type your integration handles, write the business logic that responds to it. Design every handler to be safe to run twice: if the service retries the webhook, the second execution should produce the same result as the first. Store the webhook event ID in your database and check for it before processing: if the ID already exists, skip processing and return 200.
  7. Move to production and monitor delivery. Deploy your webhook handler to your production server and update the webhook URL in the service’s dashboard to your production endpoint. Monitor delivery success rates in the service’s dashboard. Set up alerts for sustained delivery failures (5xx responses or timeouts). Use the Text Diff Checker to compare webhook payloads between your development and production environments when debugging discrepancies between how events are handled in each environment.

Frequently Asked Questions About Webhooks

What is the difference between a webhook and an API?

An API is a request-response interface: your application makes a request to a service’s API and the service responds with data. Your application initiates the communication. A webhook reverses this: the service initiates the communication by sending a request to your application. APIs are pull-based (you pull data when you need it). Webhooks are push-based (the service pushes data to you when something happens). Both are built on HTTP, but they serve different scenarios. Use an API when you need data on demand. Use a webhook when you need to react to events in real time without polling. Most complete integrations use both: an API to fetch current state and webhooks to receive notifications of changes.

What happens if my server is down when a webhook is sent?

Most webhook services implement retry logic with exponential backoff. If your server returns a 5xx error or times out, the service will retry the webhook after a delay, then again after a longer delay, and so on. Stripe retries for up to 72 hours. GitHub retries for 8 hours. The number of retries and the backoff schedule varies by service: always check the service’s documentation. This retry behaviour means your handler needs to be idempotent: if the same webhook is delivered twice (once when your server was struggling and once after retry), processing it a second time should not create a duplicate order, charge, or email. The standard solution is to store the webhook event ID in your database and skip processing if that ID has already been seen.

How do I make my webhook handler idempotent?

An idempotent webhook handler produces the same result whether it runs once or many times with the same input. The standard implementation: before processing an event, check whether your database contains a record of that event’s ID. If it does, skip processing and return 200. If it does not, process the event and store the ID. For Stripe, the event ID looks like evt_3OqX9XLkdFtm5xIW0ABC123. Create a processed_webhook_events table with the event ID as a unique key. Attempt to insert the event ID. If the insert fails due to a unique constraint violation, the event was already processed: return 200 and skip. If the insert succeeds, proceed with processing. This pattern safely handles both retries from the service and any duplicate events caused by race conditions in your own infrastructure.

What is a webhook secret and where do I find it?

A webhook secret (also called a signing secret or webhook key) is a randomly generated string that only you and the webhook service know. The service uses it to sign each webhook request by computing an HMAC hash of the request body. You use the same secret to verify the signature in your handler. The webhook secret is not the same as your API key. For Stripe, find it in the Stripe Dashboard under Developers, then Webhooks, then click on your specific webhook endpoint: the signing secret is shown there. For GitHub, you set the secret yourself when creating the webhook in the repository settings. Store it in your environment variables immediately, do not commit it to version control, and treat it like a password. If it is ever compromised, regenerate it in the service’s dashboard and update your environment variable.

Can I send webhooks from my own application to other systems?

Yes. Sending webhooks from your own application is a common pattern for integrating with third-party services and for communication between your own microservices. When a significant event occurs in your system (user created, subscription upgraded, order shipped), your application sends an HTTP POST request to one or more registered webhook URLs with the event data as JSON. To build this properly: maintain a registry of webhook endpoint URLs and the event types each URL subscribes to, queue webhook deliveries in a background job system rather than sending synchronously during request handling, implement retry logic with exponential backoff for failed deliveries, sign your webhook requests with a shared secret so receivers can verify authenticity, and provide a dashboard where webhook subscribers can view delivery logs and manually replay failed events.

Why does Stripe require the raw request body for signature verification?

Stripe’s signature verification computes an HMAC (Hash-based Message Authentication Code) over the exact bytes of the raw request body. The HMAC is a precise, byte-level operation. If you parse the JSON body first (using express.json() or body-parser) and then reserialise it for verification, the byte sequence may differ from the original even if the data is logically identical. JSON serialisation libraries differ in how they handle whitespace, key ordering, and number formatting. Any difference in bytes produces a completely different HMAC and the verification fails. This is why Stripe’s documentation specifies using express.raw() to get the raw body buffer before any parsing. The same principle applies to any webhook service that uses HMAC signature verification.

Free browser-based developer tools

Tools for working with webhook payloads and APIs

Format and inspect JSON webhook payloads before writing handler code, compare payloads between environments, convert data formats, and more. All free, all in your browser, no login required.

Webhooks Are How the Modern Web Talks to Itself

Webhooks replace polling with real-time event delivery. Instead of your application repeatedly asking external services for updates and receiving mostly empty responses, webhooks let those services notify your application the instant something relevant happens. The result is faster responses, lower resource usage, and integrations that work at any scale.

The four things to get right with every webhook integration are: format and understand the payload structure before writing code, verify signatures before processing any event, return 200 immediately and process asynchronously for slow operations, and make your handler idempotent so retries are safe. Get these four things right and webhook integrations are reliable, secure, and maintainable.

Start every new webhook integration by pasting a sample payload into the JSON Formatter to understand the exact structure before writing a single line of handler code. When debugging webhook differences between environments, use the Text Diff Checker to compare payloads side by side and identify exactly what differs. Both tools are free and work directly in your browser.