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:
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:
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:
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.
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 sends this webhook when a customer’s payment is successfully charged. Your server receives the payment details including amount, currency, and customer ID.
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.
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.
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.
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.
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.
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:
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:
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. |
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:
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.
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:
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:
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
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.
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.
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.
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.
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.
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.
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.