System Design
What Are Webhooks?
A webhook flips the API model around: instead of you constantly asking a service 'has anything happened?', it calls you the moment something does. Here's how that reversal works, and how to build one that's reliable and secure.
Imagine your app needs to know the moment a customer's payment succeeds at a payment provider. One way: ask the provider over and over - "is it done yet? is it done yet?" - polling their API every few seconds. That's wasteful (most checks find nothing) and slow (you only learn about the payment at your next poll). There's a far better way: have the provider tell you the instant the payment completes, by calling your server directly. That reversal - the service calling you instead of you asking it - is a webhook.
Webhooks are how modern services notify each other about events in real time, and they're everywhere: payment confirmations, code-push notifications, form submissions, third-party integrations. This guide explains what a webhook is, how it works, how it differs from a regular API call, and - crucially - how to build a receiver that's reliable and secure, because the details matter.
What a webhook is
A webhook is a way for one server to notify another server when an event happens, by sending an HTTP request to a URL that the second server provided in advance. It's often described as a "reverse API call" or a "callback over HTTP": instead of you calling a service's API to request data, the service calls your URL to push data to you when something occurs.
The setup has two phases:
- Registration (once, ahead of time): you tell the service, "when event X happens, send a request to this URL of mine." You register your webhook endpoint with the provider.
- Notification (every time the event happens): when event X occurs, the service automatically sends an HTTP request (usually a POST) to your registered URL, with details about the event in the body. Your server receives it and reacts.
That's the whole idea: you provide a URL, and the service calls it whenever the relevant event happens. No polling, no asking - the information arrives the instant it's available.
sequenceDiagram
participant Y as Your Server
participant P as Provider (e.g. payment service)
Y->>P: register "call https://myapp.com/hook when payment succeeds"
Note over P: ...later, a payment succeeds...
P->>Y: POST https://myapp.com/hook { event: "payment.succeeded", ... }
Y->>P: 200 OK (acknowledged)
Note over Y: Your server reacts to the event
This sequence diagram shows how a webhook works. Your server registers a URL with a provider, saying "call this URL when a payment succeeds." Later, when a payment actually succeeds, the provider sends an HTTP POST request to your registered URL with the event details. Your server responds with 200 OK to acknowledge receipt, and then reacts to the event. No polling is involved - the notification arrives the moment the event happens.
Why webhooks beat polling
The alternative to webhooks is polling - repeatedly asking a service "anything new?" Webhooks are dramatically better for event notification because:
- Efficiency. With polling, most requests return "nothing new," wasting bandwidth and resources on empty checks. With webhooks, a request happens only when there's actually an event - zero wasted calls.
- Real-time. Polling only learns about an event at the next poll, so there's always a delay. A webhook fires the instant the event happens, so you learn about it immediately.
- Simplicity at scale. Instead of every client constantly hammering a service's API to check for changes, the service pushes notifications only when needed - far less load all around.
This is why services overwhelmingly use webhooks (not polling) for event notifications. It's the difference between constantly checking your mailbox versus having the mail carrier ring your doorbell when something arrives.
What a webhook request looks like
When a webhook fires, the provider sends an HTTP request - typically a POST - to your URL, with a body (usually JSON) describing the event. Your server has an endpoint waiting to receive it. In rough terms, the receiving side looks like this:
// An endpoint on your server that receives webhook calls
app.post("/hook", (req, res) => {
const event = req.body; // the event details the provider sent
if (event.type === "payment.succeeded") {
fulfillOrder(event.data.orderId); // react to the event
}
res.status(200).send("ok"); // acknowledge receipt quickly
});
Your endpoint reads the event, does whatever the event requires, and responds with a success status to acknowledge it. That acknowledgment matters - it tells the provider the webhook was received, so it doesn't think delivery failed.
The details that make webhooks reliable and secure
Webhooks sound simple - "just call my URL" - but building a robust receiver requires handling several realities. These details are what separate a production-grade integration from a fragile one.
Security: verify the caller. Your webhook URL is reachable over the internet, so anyone who discovers it could send fake events to it. To prevent this, providers typically sign each webhook with a shared secret - they include a signature computed from the payload and a secret only you and they know. Your server recomputes the signature and checks it matches before trusting the event. Verifying this signature is essential; without it, an attacker could send your server forged events (a fake "payment succeeded").
// Verify the webhook genuinely came from the provider
app.post("/hook", (req, res) => {
const signature = req.headers["x-webhook-signature"];
const expected = computeSignature(req.rawBody, MY_SECRET);
if (signature !== expected) {
return res.status(401).send("invalid signature"); // reject forgeries
}
// ...trusted: process the event...
res.status(200).send("ok");
});
Reliability: expect retries and duplicates. Networks fail. If your server is down or returns an error when a webhook fires, the provider will usually retry delivery - which means you might receive the same event more than once. Your handler must therefore be idempotent: processing the same event twice should have the same effect as processing it once (don't ship the order twice). A common approach is to record each event's unique ID and ignore any you've already handled.
Acknowledge fast. Providers expect a quick response (often a 200 status) to confirm receipt. If your handler does slow work before responding, the provider may time out and retry unnecessarily. The standard pattern is to acknowledge immediately and do any heavy processing afterward - often by dropping the event onto a queue and returning 200 right away.
Warning: The three details that trip people up: (1) not verifying the signature, leaving the endpoint open to forged events; (2) not handling duplicate deliveries, causing the same event to be processed twice; (3) doing slow work before acknowledging, causing timeouts and retries. A webhook receiver that ignores these works in testing and breaks in production. Verify, deduplicate, acknowledge fast.
A worked example: a payment from click to fulfillment
Let me trace a complete webhook flow. An online store uses a third-party payment provider and needs to ship orders only after payment truly succeeds.
First, the setup (once): the store registers a webhook URL with the provider - "when a payment event happens, POST the details to https://store.com/payment-hook."
Now a customer checks out. The store sends the payment to the provider, and the customer sees a "processing" screen. The key: the store does not poll the provider asking "done yet?" It waits for the callback.
The provider processes the payment (a few seconds, involving banks and fraud checks). When it succeeds, it sends a webhook: a POST to https://store.com/payment-hook with a body like { "event": "payment.succeeded", "orderId": "1234" }. The store's endpoint receives it, verifies the signature to confirm it genuinely came from the provider, checks it hasn't already processed this event (idempotency), and then fulfills order 1234 - triggering the shipment. It responds 200 immediately to acknowledge.
The order ships the instant payment cleared, with no polling anywhere. Had the payment failed, the provider would have fired a payment.failed webhook, and the store would handle that case (notify the customer, release the held inventory). One registered URL, and the store reacts in real time to whatever happens - the webhook pattern doing exactly what it's for.
Where webhooks fit among the alternatives
Seeing where webhooks sit among related tools clarifies when to use them:
- vs Polling an API: Polling means you repeatedly ask a service for updates. Webhooks invert this - the service tells you when there's news. Webhooks are far more efficient (no wasted checks) and real-time (no interval delay). Use webhooks when the service supports them; fall back to polling only when it doesn't.
- vs A regular API call: A normal API call is you calling them to request something on demand. A webhook is them calling you when an event happens. They're complementary - you call their API to do things, and they call your webhook to notify you when things happen.
- vs WebSockets: WebSockets maintain a persistent connection, typically browser-to-server, for continuous two-way real-time communication. Webhooks are stateless, server-to-server, one-event-at-a-time HTTP calls. Use WebSockets for ongoing interactive streams (chat); use webhooks for discrete server-to-server event notifications (a payment cleared).
The clean model: a webhook is for one server notifying another that a specific event occurred, efficiently and in real time.
A checklist for a robust receiver
The receiving end is where webhooks succeed or fail in production. A practical checklist:
- Verify authenticity. Check the signature (using the shared secret) on every incoming webhook before trusting it. Reject anything that doesn't verify - your defense against forged events.
- Be idempotent. Record each event's unique ID and skip any you've already processed, since providers retry and may deliver duplicates. Never let a duplicate cause a duplicate action (a second shipment, a double charge).
- Acknowledge fast, process async. Respond with a success status quickly (often just 200), and do slow work afterward - commonly by putting the event on a queue and returning immediately. This prevents the provider from timing out and retrying.
- Return the right status codes. Return success when you've accepted the event and an error status when you genuinely can't, so the provider's retry logic behaves correctly.
- Secure the endpoint. Beyond signature verification, serve the endpoint over HTTPS so event data is encrypted in transit.
Tick all of these and your integration is robust. Skip them and it'll appear to work in testing, then fail in subtle ways under real conditions.
The challenges webhooks bring
Webhooks are elegant but come with challenges worth naming:
- You must expose a public endpoint. The provider has to reach your server, which means a publicly accessible, secured URL. (This also makes webhooks tricky to test locally, where your machine isn't publicly reachable - developers often use tunneling tools during development.)
- Ordering isn't guaranteed. Related events might arrive out of order (an "updated" before a "created," due to timing and retries). If order matters, account for it - using timestamps, or fetching current state from the provider rather than assuming the webhook reflects the latest.
- Delivery isn't perfectly guaranteed. Despite retries, a webhook could rarely fail to be delivered (your endpoint was down for the entire retry window). Robust systems sometimes pair webhooks with periodic reconciliation - occasionally checking the provider's state directly to catch anything missed.
- Debugging is harder. Because events arrive asynchronously and unprompted, diagnosing "why didn't this order ship?" means tracing whether the webhook fired, was delivered, and was processed - which requires good logging at the receiver.
None of these undermine webhooks - they remain the best tool for server-to-server event notification - but the recurring theme is that the receiver carries the responsibility: verify, deduplicate, acknowledge fast, and don't assume perfect ordering or delivery.
Where webhooks are used
Webhooks power countless integrations between services:
- Payment processing. A payment provider fires a webhook when a payment succeeds, fails, or is refunded, so your system can fulfill orders or update records in real time.
- Code and CI/CD. A code-hosting service fires a webhook when code is pushed or a pull request opens, triggering automated builds and deployments.
- Messaging and notifications. Services fire webhooks to notify your system of incoming messages, form submissions, or user actions.
- Third-party integrations. Webhooks are the standard way SaaS products notify each other - connecting tools so an event in one triggers an action in another.
The common pattern is always the same: one service has an event, another service registered a URL, and the first calls the second the moment the event happens.
Webhook events and payloads
A practical detail about working with webhooks: providers usually offer many event types, and you subscribe to the ones you care about. A payment provider might emit dozens - payment.succeeded, payment.failed, refund.created, dispute.opened, and so on - and your endpoint inspects each incoming event's type to decide how to react (or to ignore types you don't handle). The payload (the request body) carries the event's details as structured data, typically JSON, including the event type, a unique event ID (used for idempotency), a timestamp, and the relevant object (the payment, the order, the user). Well-designed webhook systems keep these payloads consistent and documented, so receivers can reliably parse them. Part of integrating with a provider's webhooks is reading their documentation to learn which events exist, what each payload contains, and how their signatures work - the mechanics are universal, but the specific event names and payload shapes are defined by each provider.
Interview: Webhooks come up when a design involves services reacting to each other's events, especially third-party integrations. Proposing them over polling shows you understand event-driven efficiency: "Rather than polling the payment provider, I'll register a webhook so they notify us the instant a payment succeeds." The senior signal is naming the reliability details - "I'll verify the signature, make the handler idempotent since deliveries can be retried, and acknowledge fast by queuing the work." That awareness of the failure modes is what separates real experience from naming the concept.
The bottom line
A webhook is a way for one server to notify another when an event happens, by calling a URL the receiver registered in advance - a "reverse API call" that pushes data to you instead of you polling for it. It's far more efficient and real-time than polling, which is why services use webhooks for event notifications. But building a robust receiver means handling the realities: verify the signature so forged events are rejected, make the handler idempotent because deliveries can be retried and duplicated, and acknowledge quickly so the provider doesn't time out.
Get those details right and webhooks are the clean, efficient backbone of server-to-server event notification across the modern web. We cover event-driven integration and API design across System Design Fundamentals.
Go deeper
- Webhook (Wikipedia) - the concept and how it works.
- GitHub webhooks documentation - a real, well-documented webhook system.
- Stripe webhooks documentation - an excellent guide to webhooks including signature verification and retries.
- Push technology (Wikipedia) - the broader category of pushing data rather than polling for it.