What Is a Webhook? How It Works, Explained
A plain-English guide to webhooks: how the event-to-POST flow works, how they differ from APIs and polling, where they are used, and how HMAC signatures keep them secure.

A webhook is an automated message a web application sends the instant something happens inside it — a payment clears, a code commit lands, a message arrives — delivered as an HTTP POST request to a URL you provide in advance. Instead of your application repeatedly asking "has anything changed yet?" through an API, the provider pushes the data to you the moment the event occurs. That one difference, push instead of pull, is why webhooks have become the default way that payment processors, chat platforms, code-hosting services and e-commerce systems notify other software of real-time events.
- Developer Jeff Lindsay coined the term "webhook" in 2007, describing it as a "hook" for the web — a user-defined HTTP callback, according to Wikipedia.
- A webhook is almost always an HTTP POST request containing a JSON payload, sent to an endpoint URL you registered with the provider.
- Providers expect a fast 2xx response — GitHub requires one within 10 seconds — and treat anything else as a failed delivery.
- Failed deliveries are typically retried automatically; Stripe retries for up to three days using exponential backoff.
- Because anyone who finds your endpoint URL could try to fake an event, every major provider signs its payloads with an HMAC signature that your server must verify.
What is a webhook?
A webhook is a way for one application to notify another, automatically, when a specific event occurs — without the receiving application having to ask. Mechanically, it is nothing exotic: it's an outbound HTTP request, almost always a POST, that a server fires toward a URL you configured ahead of time, carrying details about the event in its body. Wikipedia traces the term to a May 2007 blog post by programmer Jeff Lindsay, who adapted it from the older "hook" concept in software — a point in a program where other code can attach custom behavior — and applied it to the web.
In practice, you set up a webhook by registering an endpoint — a public HTTPS URL on your server — with a provider such as a payments platform, a code host, or a messaging service, and telling it which events you want to hear about. From that point on, whenever a matching event happens on the provider's side, its servers make an HTTP call to your URL, and your server is responsible for being there to receive it. This is the same basic model used for linking content on sites like GitHub Actions CI/CD pipelines, which are themselves frequently triggered by webhook-style events such as a push or a pull request.
Webhooks vs. a normal API call vs. polling
Most developers meet webhooks after they've already worked with a conventional REST API, where your application is the one making requests — "give me the current status of order 4821" — and the provider replies. GitHub's own documentation contrasts this directly with polling, which it defines as "calling an API intermittently to check whether data is available." Polling works, but it means making requests on a schedule whether or not anything has actually changed, which wastes requests, burns through rate limits, and introduces lag between an event happening and your system noticing it. A webhook flips that relationship: you express interest once, when you create the subscription, and the provider contacts you only when there's something to report. If you're unfamiliar with how a basic API call or a single request-response cycle works in the first place, this plain-English guide to how an API works is a useful primer before going further into webhooks specifically.
| Approach | Who initiates the request | Timeliness | Resource cost |
|---|---|---|---|
| Polling an API | Your application, on a repeating schedule | Delayed by the polling interval | High — most requests return "nothing new" |
| Webhook | The provider, only when an event occurs | Near real-time | Low — one request per real event |
| Standard (synchronous) API call | Your application, on demand | Immediate, but only when you ask | Moderate — depends on call volume |
How the request/response flow actually works
Strip away the branding on any individual provider's docs and the webhook lifecycle follows the same basic sequence everywhere:
- Event. Something happens inside the provider's system — a customer's card is charged, a pull request is merged, an inbound text message arrives.
- HTTP POST. The provider's servers build a payload describing that event and send it as an HTTP POST request to the endpoint URL you registered.
- Endpoint. Your server, listening at that URL, receives the request. Stripe's documentation is explicit that this must be a publicly reachable HTTPS address — a webhook can't be delivered to a machine sitting behind a firewall with no public endpoint.
- Payload. Your application reads the JSON body, typically verifies the sender's signature first, and then parses out the event type and data it needs.
- Response code. Your server replies with an HTTP status code. A 2xx response tells the provider the delivery succeeded; anything else — a timeout, a 4xx, or a 5xx — is treated as a failure.
- Retries. If the delivery failed, most providers will attempt to resend the same event later, usually several times, before giving up.
Both Stripe and GitHub stress the same operational rule at step five: respond fast, before doing any real work. Stripe's guidance is to "quickly return a successful status code (2xx) before any complex logic that could cause a timeout," for example returning a 200 before you've actually finished updating a customer's invoice in your own accounting system. GitHub is more specific still, telling developers their server "should respond with a 2XX response within 10 seconds of receiving a webhook delivery," after which it "terminates the connection and considers the delivery a failure." The usual pattern is to acknowledge receipt immediately, then hand the payload off to a background queue for the actual processing.
What's inside a webhook payload
A webhook payload is normally a JSON document describing one event. Stripe's events, for instance, carry an event type such as payment_intent.succeeded along with a data object holding the relevant resource — the specific PaymentIntent that succeeded, in that case. GitHub's deliveries work the same way: an HTTP POST body containing structured JSON about the triggering activity, such as a push or an issue comment, sent to the URL configured on the webhook. Because the sender and receiver have to agree in advance on that structure, most API providers publish a full catalog of event types and their payload shapes in their developer documentation, and you typically filter, when you first set up the webhook, to only the specific event types your integration actually needs.
Two header-level details matter for almost any implementation. First, the request carries a content type of JSON, so your framework needs to read it before anything mutates or re-serializes the raw body — Stripe warns explicitly that any manipulation of the raw request body will break signature verification later. Second, the sender usually attaches extra headers beyond the body itself, most importantly a signature header (Stripe-Signature on Stripe's platform, X-Hub-Signature-256 on GitHub's) that your server uses to confirm the request is authentic before trusting anything inside it.
Response codes and retry behavior
Because a webhook delivery is just an HTTP request, the status code your endpoint returns carries real meaning for the sender, not only for your own logs. The general pattern, consistent with how HTTP status codes are defined by the Mozilla Developer Network and applied in provider webhook systems:
| Status code | Meaning | What a sender typically does |
|---|---|---|
| 200 / 201 / 204 (2xx) | Request succeeded; your endpoint accepted the event | Marks the delivery as successful, no retry |
| 3xx (redirect) | Your endpoint tried to redirect the request | Stripe treats redirect responses to webhooks as failures outright |
| 400 Bad Request | The server can't process a malformed request | Usually not retried — the payload or handler needs fixing |
| 401 / 403 | Authentication or authorization failed | Not retried until credentials or access are corrected |
| 404 / 410 | Endpoint not found, or gone permanently | Senders may eventually stop delivering to a 404/410 endpoint |
| 429 Too Many Requests | Receiver is rate-limiting the sender | Sender backs off and retries later |
| 5xx (server error) | Receiver's server failed or timed out | Retried automatically, typically with exponential backoff |
Retry policies differ by provider but follow a similar shape: give the receiving server multiple chances, spread further and further apart, before giving up entirely. Stripe says it "attempts to deliver events to your destination for up to three days with an exponential back off in live mode," and separately offers a manual resend from the dashboard for up to 15 days, or via its CLI for up to 30 days, after the event was created. GitHub's documentation describes failed deliveries as something you can manually redeliver from a repository's webhook settings, in addition to whatever automatic retry behavior applies to a given delivery. The practical lesson is the same across platforms: your endpoint will likely receive the same event more than once over its lifetime, whether due to an automatic retry or a manual resend, so it needs to handle duplicates gracefully — Stripe's own advice is to log event IDs you've already processed and ignore repeats.
Common uses: payments, chat, CI/CD and e-commerce
Webhooks show up anywhere one system needs to tell another about something that just happened, asynchronously and without a human watching a dashboard. A few of the most common categories:
- Payments. Stripe's documentation lists exactly this use case first: webhooks let an integration "automatically trigger reactions" to events like a bank confirming a payment, a customer disputing a charge, or a recurring subscription payment succeeding — all things that happen on a timetable the merchant's own server can't predict.
- Chat and messaging. Twilio's developer docs describe an inbound SMS to one of its phone numbers as triggering a webhook to the receiving application, and note that products like Twilio Conversations and Sync rely on webhooks the same way. Twilio further splits these into "instruction" webhooks, where your app must answer with instructions for what Twilio should do next, and purely informational ones, such as notice that a call recording has finished.
- CI/CD and source control. A push to a repository, a new pull request, or a completed build are classic webhook triggers; GitHub's webhook system underlies much of the automation described in how GitHub Actions CI/CD works, where an event on the repository kicks off a pipeline elsewhere.
- E-commerce. Order placed, inventory updated, shipment status changed — storefronts and fulfillment systems trade these events constantly with payment processors, shipping carriers and marketing tools, almost always over webhooks rather than polling.
Webhook security: why signature verification matters
A webhook endpoint is, by definition, a public URL that accepts incoming POST requests — which means anyone who discovers it could try sending a fake event to trigger an action in your system, such as marking an unpaid order as paid. Stripe's documentation states this risk plainly: "Without verification, an attacker could send fake webhook events to your endpoint to trigger actions like fulfilling orders, granting account access, or modifying records." Its fix, shared in substance by nearly every major provider, is cryptographic signing.
Stripe signs every webhook event with an HMAC using SHA-256, placing the result in a Stripe-Signature header alongside a timestamp; your server recomputes the same HMAC over the raw request body using a shared secret and compares it, using a constant-time comparison, to the value Stripe sent. The timestamp exists specifically to blunt replay attacks — someone capturing a legitimate signed request and resending it later — and Stripe's own libraries reject anything outside a five-minute tolerance window by default.
GitHub's approach is functionally identical: it signs each delivery with HMAC-SHA256 over the raw payload body, using a secret you configure on the webhook, and sends the resulting hex digest in an X-Hub-Signature-256 header prefixed with sha256=. GitHub's documentation walks through computing that same HMAC on your end and comparing it with a constant-time function rather than a simple equality check, which can otherwise leak timing information to an attacker. An older X-Hub-Signature header using SHA-1 still exists for backward compatibility, but GitHub recommends the SHA-256 version. This same cryptographic approach — a shared secret, an HMAC over the request body, a header carrying the result — is conceptually similar to the token-based trust that underpins how OAuth 2.0 secures API access, even though the two mechanisms solve different problems.
Signature checks aside, Stripe also recommends IP allowlisting — restricting your firewall to only accept webhook requests from the sender's published IP ranges — as a second layer, and requires TLS 1.2 or higher on the connection itself. The common thread across every provider's advice is the same: never trust a webhook payload just because it arrived at the right URL.
Best practices for handling webhooks reliably
A handful of habits separate a webhook integration that holds up in production from one that quietly drops events:
- Acknowledge first, process second. Return your 2xx response immediately, and push the real work — updating a database, sending a notification — onto an asynchronous queue, exactly as Stripe and GitHub both recommend.
- Verify every signature. Reject anything that doesn't match before you read a single field out of the payload.
- Deduplicate by event ID. Expect to see the same event more than once, due to retries or manual resends, and make your handler idempotent.
- Don't assume ordering. Stripe explicitly warns that it "doesn't guarantee the delivery of events in the order that they're generated," so an integration shouldn't depend on one event always arriving before another.
- Subscribe only to what you need. Listening for every event type available puts unnecessary load on both sides of the connection.
- Keep an eye on delivery logs. Most providers expose a dashboard of recent deliveries, their response codes, and pending retries, which is usually the fastest way to diagnose a silently broken integration.
Getting started with your first webhook
If you're building your first webhook integration, the fastest path is usually to pick the single provider you're already working with — a payments platform, a code host, a messaging API — and follow its own webhook quickstart, since the exact signing scheme, retry window and payload shape vary provider to provider even though the underlying pattern doesn't. Most platforms, including Stripe and Twilio, offer a local tunneling or CLI-based testing tool so you can receive real webhook deliveries on a development machine before you have a public HTTPS endpoint ready. From there, the build order is consistent: stand up an endpoint that returns a fast 2xx, add signature verification before trusting the payload, make your handler safe to run twice on the same event, and only then wire in the business logic the webhook is actually there to trigger.
Frequently asked questions
What is a webhook in simple terms?
A webhook is an automated HTTP request, almost always a POST, that one application sends to a URL you provide whenever a specific event happens, so you receive the data immediately instead of having to ask for it.
How does a webhook differ from an API call?
With a normal API call, your application contacts the provider and asks for data. With a webhook, the provider contacts your application automatically the moment an event occurs, without you asking.
Is a webhook the same thing as polling?
No. Polling means your application repeatedly calls an API on a schedule to check for changes, which wastes requests and adds delay. A webhook eliminates that by having the provider push the event to you only when it actually happens.
What happens if my server doesn't respond to a webhook?
If your endpoint doesn't return a successful 2xx status quickly enough, the provider treats the delivery as failed and typically retries it automatically for a period of time, such as Stripe's up to three-day retry window, before giving up.
How do I verify that a webhook request is genuine?
Providers like Stripe and GitHub sign each webhook payload with an HMAC-SHA256 signature sent in a header (Stripe-Signature or X-Hub-Signature-256). Your server recomputes that HMAC using a shared secret and the raw request body, then compares it with a constant-time check before trusting the payload.
What are common real-world uses of webhooks?
Webhooks are widely used for payment notifications (Stripe), inbound chat and SMS messages (Twilio), CI/CD triggers like code pushes and pull requests (GitHub), and e-commerce events such as new orders or inventory changes.
Sources
- Stripe Docs: Receive Stripe events in your webhook endpointdocs.stripe.com
- GitHub Docs: About webhooksdocs.github.com
- GitHub Docs: Validating webhook deliveriesdocs.github.com
- GitHub Docs: Handling webhook deliveriesdocs.github.com
- Twilio Docs: Webhookstwilio.com
- Wikipedia: Webhooken.wikipedia.org
Felix Moreau writes Pandromeda's software coverage and how-to guides. He covers Windows, macOS and Linux updates, the apps people rely on, emulators and developer tools, and turns official documentation into clear, numbered steps that work on the current version.

