What Is a Webhook? How It Works With Examples
What a webhook is, how it differs from polling, and a payment notification example end to end, including signature checks, retries, duplicates and testing.
A webhook is an automated HTTP request that one system sends to a URL you provide the moment an event happens, such as a payment succeeding or an order shipping. Instead of your app repeatedly asking "anything new?" (polling), the other service pushes the event to you, usually as a JSON payload signed so you can verify it is genuine.
Photo: Unsplash
On this page
A customer pays on your website, then closes the browser before the “thank you” page loads. Did the order go through? Should you ship it? If your app only learns about payments when the customer lands on that page, you have a problem. Webhooks are how payment providers, shipping carriers, CRMs and code platforms tell your system what happened, whether or not anyone is still looking at a screen.
This guide explains what a webhook is in plain terms, contrasts it with polling, and walks through a payment notification from start to finish.
What is a webhook, in plain English?
A webhook is a “don’t call us, we’ll call you” arrangement between 2 systems.
- You give a service (say, a payment processor) a URL on your server, called a webhook endpoint.
- You choose which events you care about, such as “payment succeeded” or “subscription canceled.”
- When one of those events happens, the service sends an HTTP POST request to your URL with details of the event, usually as JSON.
- Your server checks the request is genuine, replies with a success status code, and does whatever work the event requires.
Stripe’s documentation describes a webhook as a real-time push notification sent to your application as a JSON payload over HTTPS. Webhooks are sometimes called “reverse APIs” or “HTTP callbacks,” because the provider calls you instead of you calling the provider.
Common examples:
- A payment processor notifies your store that a charge succeeded, failed or was disputed.
- GitHub notifies your CI system that code was pushed, so it can run tests.
- A form tool sends new submissions to your CRM.
- A shipping platform sends tracking updates.
- A scheduling app tells your website that an appointment was booked or canceled.
Webhooks vs polling
The alternative to a webhook is polling: your app calls the provider’s API on a schedule (“any new payments since 10:05?”) and compares the answer with what it already knows.
| Polling | Webhooks | |
|---|---|---|
| Who starts the request | Your app, on a timer | The provider, when an event happens |
| Freshness | As stale as your polling interval | Near real time |
| Wasted requests | Many calls return “nothing new” | Requests only when something happens |
| API rate limits | Polling eats into them | Not affected by your request volume |
| What you must run | A scheduled job | A public HTTPS endpoint |
| Reliability concerns | Missed changes between polls, pagination | Downtime, retries, duplicates, out-of-order events |
| Security | You authenticate to the provider | You must verify the provider’s signature |
| Best for | Simple, low-volume syncs, or providers without webhooks | Payments, notifications, anything time-sensitive |
Key takeaway: Webhooks are faster and more efficient than polling, but they shift responsibility to you: your endpoint must be reachable, verify every request, respond quickly and handle retries and duplicates safely. Many robust systems use webhooks for speed plus an occasional polling job as a safety net.
Anatomy of a webhook request
A typical webhook delivery is an HTTPS POST that looks something like this:
```http POST /webhooks/payments HTTP/1.1 Host: example.com Content-Type: application/json Stripe-Signature: t=1760000000,v1=5257a869e7ec…
{ “id”: “evt_1Q2w3E4r5T”, “type”: “checkout.session.completed”, “created”: 1760000000, “data”: { “object”: { “id”: “cs_test_a1b2c3”, “payment_status”: “paid”, “amount_total”: 4999, “currency”: “usd” } } } ```
The parts that matter:
- Event ID (
id): unique per event, used to detect duplicates. - Event type (
type): what happened. - Payload (
data): the object involved, such as a payment or checkout session. - Signature header: a cryptographic signature (Stripe uses an HMAC with SHA-256) computed from the raw body, a timestamp and a secret only you and the provider know.
A payment notification example, end to end
Here is how a webhook-driven checkout works with a payment provider such as Stripe. The same pattern applies to most providers, even though header names and event types differ. If you are still choosing a provider, see how to accept payments on your website and Stripe vs PayPal for online stores.
Step 1: Register the endpoint. In the provider’s dashboard, add https://example.com/webhooks/payments and subscribe only to the events you need, such as checkout.session.completed. Stripe gives you a signing secret beginning with whsec_. Store it as an environment variable, never in code.
Step 2: The customer pays. The provider processes the card and creates an event.
Step 3: The provider sends the webhook. Your endpoint receives the POST shown above.
Step 4: Verify and acknowledge. Your handler checks the signature using the raw request body, then returns a 2xx status quickly. Here is a minimal Node.js and Express example using Stripe’s library:
```javascript const express = require(“express”); const Stripe = require(“stripe”);
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; const app = express();
// Use the raw body: signature checks fail if the JSON is parsed and re-serialized app.post(“/webhooks/payments”, express.raw({ type: “application/json” }), async (req, res) => { let event; try { event = stripe.webhooks.constructEvent(req.body, req.headers[“stripe-signature”], endpointSecret); } catch (err) { return res.status(400).send(“Invalid signature”); }
if (event.type === “checkout.session.completed”) { await jobQueue.add(“fulfill-order”, { sessionId: event.data.object.id, eventId: event.id }); }
res.sendStatus(200); // acknowledge fast; do the heavy work in the background }); ```
Step 5: Fulfill once, in the background. A worker picks up the job, checks whether that checkout session was already fulfilled, and if not, marks the order paid, sends the receipt and triggers shipping.
Stripe’s fulfillment guide is explicit about why webhooks are required here: you cannot rely on the post-checkout landing page alone, because a customer can pay successfully and then lose their connection before the page loads. It also warns that your fulfillment function may be called multiple times, even concurrently, for the same checkout session, so it must be safe to run repeatedly. Some payment methods, like bank debits, confirm later and send a separate event (checkout.session.async_payment_succeeded) when funds succeed.
Webhook best practices: security and reliability
These come straight from the providers’ own documentation:
- Verify every signature. Stripe warns that without verification, an attacker could send fake events to trigger actions like fulfilling orders. Use the provider’s official library and the raw request body.
- Reject stale timestamps. Stripe includes a timestamp in the signed header to prevent replay attacks; its libraries default to a 5-minute tolerance.
- Use HTTPS. Stripe requires HTTPS endpoints in live mode, and GitHub recommends HTTPS with certificate verification enabled.
- Respond fast. GitHub expects a 2xx response within 10 seconds or it counts the delivery as failed. Stripe says to return a 2xx before any complex logic that could time out. Push slow work to a queue.
- Expect retries. Stripe retries failed deliveries in live mode for up to 3 days with exponential backoff.
- Handle duplicates (idempotency). Both Stripe and GitHub note you may receive the same event more than once. Log processed event IDs (GitHub provides an
X-GitHub-Deliveryheader) and skip repeats. - Do not assume order. Stripe does not guarantee events arrive in the order they were created. Fetch the latest object from the API when order matters.
- Subscribe only to what you need. Extra events add load for no benefit.
- Rotate secrets periodically and whenever you suspect a leak, and never put credentials in the webhook URL.
- Exempt the route from CSRF checks if your framework enforces them, since the provider cannot send your CSRF token.
Webhooks also pair naturally with event-driven designs. If you are splitting a system into services, see microservices vs monolith, and for API style choices, REST vs GraphQL.
How to test webhooks locally
Your laptop is not reachable from the internet, so providers offer tools to bridge the gap:
- Provider CLIs. Stripe’s CLI can forward events to your machine with
stripe listen --forward-to localhost:4242/webhookand send test events withstripe trigger payment_intent.succeeded. - Tunnels. Tools such as ngrok create a temporary public HTTPS URL that forwards to localhost.
- Delivery logs. Provider dashboards show each delivery attempt, the status code you returned and when the next retry is scheduled. Use them to debug failures and resend events.
- Automated tests. Save sample payloads and write tests for each event type, including invalid signatures and duplicate deliveries.
Where to start: a webhook checklist
- [ ] List the events your app truly needs.
- [ ] Create a dedicated HTTPS endpoint for each provider.
- [ ] Store the signing secret in an environment variable or secrets manager.
- [ ] Verify signatures using the raw body and the official library.
- [ ] Return 2xx quickly and queue the real work.
- [ ] Store processed event IDs and make handlers idempotent.
- [ ] Handle out-of-order events by fetching current state when needed.
- [ ] Monitor failures and alert when deliveries start failing.
- [ ] Add a periodic reconciliation job that compares your records with the provider’s API.
Hosting the endpoint as a function on a serverless platform is a common, low-maintenance option, as long as it responds within the provider’s time limit.
Conclusion
A webhook is simply an HTTP request a service sends to you when something happens. It replaces wasteful polling with near-real-time notifications, which is why payment providers, developer platforms and SaaS tools rely on them. The catch is that your endpoint must act like a careful receptionist: verify who is calling, say “got it” quickly, write things down once even if the same message arrives twice, and never assume messages arrive in order. Get those basics right and webhooks become one of the most dependable parts of your stack.
Sources
- Stripe Docs: Receive Stripe events in your webhook endpoint — signature verification, 5-minute tolerance, retries for up to 3 days, duplicates, ordering, CLI testing
- Stripe Docs: Fulfill orders — why webhooks are required for fulfillment and why it must be idempotent
- GitHub Docs: Best practices for using webhooks — 10-second response window, secrets, HTTPS and delivery IDs
Frequently asked questions
What is the difference between an API and a webhook?
With an API, your app sends a request and gets a response when it chooses to ask. With a webhook, the other service sends a request to your app automatically when an event happens. Webhooks are often described as reverse APIs. Most integrations use both: webhooks to learn something changed, and API calls to fetch details or take action.
Are webhooks secure?
Webhooks can be secure if you verify them. Use HTTPS, check the provider's signature on every request using the raw body and your signing secret, reject old timestamps to block replays, and never put credentials in the URL. Without signature verification, anyone who finds your endpoint could send fake events, such as a fake "payment succeeded."
What happens if my webhook endpoint is down?
Most providers retry failed deliveries. Stripe, for example, retries live-mode events for up to 3 days with exponential backoff, and shows each attempt in its dashboard so you can resend events. Even so, add monitoring and a periodic reconciliation job that checks the provider's API, so nothing is missed during a long outage.
Why do I receive the same webhook twice?
Duplicate deliveries happen because of retries, network timeouts or the provider generating more than one event for a change. Stripe and GitHub both warn about this. Store the IDs of events you have already processed and skip repeats, and design actions like order fulfillment so running them twice causes no harm.
Is a webhook a push notification?
In a sense, yes. A webhook is a server-to-server push: the provider pushes event data to your server over HTTP as soon as something happens. It is different from a mobile push notification, which goes to a person's phone, though a webhook can trigger your system to send one.
Do webhooks use GET or POST?
Webhooks almost always use HTTP POST, with the event data in a JSON body and metadata such as signatures in headers. Your endpoint should accept POST requests, verify the signature, and return a 2xx status code quickly. Some providers send a one-time GET or challenge request when you first register an endpoint to confirm you own it.
How do I test a webhook on localhost?
Use the provider's CLI if it has one, such as Stripe's `stripe listen --forward-to` command, which forwards real test events to your local server. Alternatively, use a tunneling tool such as ngrok to get a temporary public HTTPS URL. Then trigger test events and check the provider's delivery logs to confirm your responses.
Was this guide useful
No reviews yet. Share your experience and help other readers.