In this article
Before you start
This page sits in the system design guide, where a webhook is one of the standard ways two services talk to each other.
Why interviewers ask this
A candidate who answers "a webhook is a callback URL" has given the definition and nothing an engineer can build from. Interviewers ask about webhooks because the topic hides three reliability questions: what happens when the receiver is down, what happens when the same event arrives twice, and how the receiver knows the sender is genuine. The question usually appears inside a design prompt such as a payment flow, a notification service or an integration with a third-party tool.
A weak answer describes the happy path. A strong answer states the delivery semantics, names the verification step, and says where the work happens relative to the response.
Webhooks from first principles
In plain words
- Webhook
- A webhook is an HTTP request that a provider sends to a URL registered by the receiver each time a chosen event occurs. It reverses the usual direction: the provider calls the receiver, not the other way round.
- At-least-once delivery
- At-least-once delivery is a delivery promise in which the sender retries until it hears success, so each event arrives one or more times and never zero times while retries last. It is the delivery semantics (the promise a sender makes about how often a message arrives) of most webhook providers, and it is why duplicates happen.
- Idempotent handler
- An idempotent handler is a piece of receiving code that produces the same final result whether it processes one delivery of an event or several deliveries of the same event. It usually works by recording the identifier of each event it has already handled.
- HMAC signature
- An HMAC signature is a hash-based message authentication code: a fingerprint computed from a message and a shared secret key, as defined in Request for Comments (RFC) 2104 (February 1997). Only someone who holds the secret can produce a matching value, so it proves who sent the message and that nobody changed it.
Picture ordering a parcel. You can walk to the front door every thirty seconds to see whether it has arrived, or you can leave your address with the courier and wait for the knock. Walking to the door is polling, which means repeatedly asking a service whether anything changed. The knock is a webhook.
The analogy breaks in one place: a courier knocks once, but a webhook sender may knock twice if it did not hear your reply.
In one precise sentence, a webhook is an event notification delivered as an HTTP POST to a URL the receiver controls. The steps below follow one event from registration to retry.
Step 1: Register a URL and choose the events
The receiver gives the provider a public URL and the list of event types it wants. The provider stores the pair and, in most systems, creates a secret for it. Stripe's documentation (as of October 2026) requires the registered URL to be a publicly accessible Hypertext Transfer Protocol Secure (HTTPS) address (HTTP sent over an encrypted connection) in live mode and advises subscribing only to the event types the integration needs, because extra events strain the server.
The input is a URL and a list of event names. The output is a registration the provider will use for every future matching event. Nothing has happened yet: the provider will only call you later, when an event occurs.
You can test before you have a public address. Tunnelling tools and the provider's command-line listener forward events to a laptop, which is why a local handler can be developed without deploying anything.
Step 2: The event happens and the provider sends a signed POST
When the event occurs, the provider builds a JSON body that describes it and sends it to your URL with an HTTP POST. It also adds a signature header computed from the body and your secret. Stripe puts the signature in a header named Stripe-Signature, and GitHub puts it in X-Hub-Signature-256, a value that starts with sha256= followed by a hexadecimal digest (GitHub Docs, as of October 2026).
The input is the event. The output is a request on the wire: a body, a signature and, in Stripe's scheme, a timestamp that is part of the signed text. The signature exists because anyone on the internet can send a POST to a public URL. Without a check, an attacker could post a fake "order paid" event.
Step 3: Verify the signature, store the event and answer with a 2xx quickly
The receiver recomputes the signature from the raw request body and its own copy of the secret and compares it with the header. It then writes the verified event to a durable queue or table, and only then replies with a 2xx status code (any success status). Both providers stress the raw body.
Compare in constant time, not with a plain equality operator, because a plain comparison can leak how many leading characters matched. Stripe's libraries reject old timestamps, with a default tolerance of five minutes (Stripe documentation, as of October 2026), so a captured request cannot be replayed much later; a replay inside that window is caught by the log of processed IDs.
The input is the raw request. The output is a rejection, or a verified event stored durably and then acknowledged with a fast 2xx. A 2xx tells the provider to stop retrying, so an event acknowledged but not stored is lost if the process dies in between; Stripe's rule is a 2xx before complex logic, and one fast write is not complex.
Step 4: Queue the work and ignore duplicates
A worker takes the stored event from the queue and does the slow work there; Stripe recommends queues because a spike, such as every subscription renewing at month start, can overwhelm hosts that work inside the request.
Duplicates are the second reason: Stripe states that an endpoint might get the same event twice and recommends logging processed event IDs and skipping repeats.
To make the check safe, insert the event ID under a unique constraint (a database rule that refuses a second row with the same ID) in the same database transaction (a group of writes that all succeed or all fail together) as the effect, so two copies running at once cannot both win; for outside effects, pass the event ID as an idempotency key (an ID the downstream service uses to ignore a repeated request). Stripe also notes (as of October 2026) that some duplicates carry different event IDs; compare data.object id and event.type.
The input is a verified event. The output is one effect, however many deliveries occur, if the ID check and effect commit together.
Step 5: The provider retries when it hears no success
If the receiver answers with an error, times out, or never answers, the provider tries again later. Stripe states (as of October 2026) that in live mode it attempts delivery for up to three days with exponential backoff, meaning the gap between attempts grows. It treats redirects as failures, so the URL you register must be the final one. Not every provider retries on its own: GitHub states (as of October 2026) that it does not automatically redeliver failed deliveries (GitHub's failed-delivery page), so there a manual redelivery or the reconciliation poll is the safety net.
The retries make webhooks at-least-once delivery, and that is why Step 4 matters. A delivery that succeeded on your side but lost its acknowledgement on the way back looks like a failure to the sender, and it will send the event again. The sender cannot tell "you never got it" from "you got it but I never heard".
The input is a failed or missing acknowledgement. The output is a repeat delivery of the same event, which the idempotent handler absorbs.
Step-by-step description
- Give the provider a URL and event types.
- The provider sends JSON plus a signature.
- Check the signature, enqueue, reply 2xx fast.
- Work from the queue, skip seen event IDs.
- No success heard, so the event comes again.
- The receiver registers a public URL and the event types it wants.
- The provider sends a signed POST with a JSON body.
- The receiver verifies the signature, stores the event durably, then replies with a 2xx status.
- A worker takes the stored event from the queue and skips repeats of processed event IDs.
- The provider retries when it hears no success, so duplicates arrive.
Knowing it is half the job. Saying it is the other half.
Interview Clozo is an AI interview copilot for real interview rounds: it hears the question and puts a strong answer on your screen, and you say it in your own words. See the features or how it works.
Start Free Trial →Worked example
Consider a shop that expects about ten paid orders a day. The numbers below are illustrative inputs, computed in a script1. The signature uses the Secure Hash Algorithm (SHA) family in its 256-bit form. These numbers show three things: why a webhook beats polling for rare events, what a signature looks like, and how often duplicates appear.
Polling the provider every 30 seconds makes 2,880 requests a day, and with ten events a day 2,870 of them, 99.65 percent, return nothing new1. The average event also waits half the interval, 15 seconds, before polling notices it. A webhook makes ten calls for ten events and delivers each as it happens.
Text description of the diagram
- Polling every 60 seconds makes 1,440 requests a day.
- Polling every 30 seconds makes 2,880 requests a day.
- Polling every 10 seconds makes 8,640 requests a day.
- A webhook makes 10 calls for 10 events.
| Fact | Value |
|---|---|
| Polling requests a day at a 60-second interval | 1,440 |
| Polling requests a day at a 30-second interval | 2,880 |
| Polling requests a day at a 10-second interval | 8,640 |
| Webhook calls a day for ten events | 10 |
| Length of a SHA-256 HMAC in hexadecimal | 64 characters |
| Signature of the sample body, first 16 characters | de913aae6e90f9f2 |
| Signature after changing the amount 4200 to 4201, first 16 characters | d7430858dde22245 |
| Mean deliveries per event with 5 percent lost acknowledgements | 1.0526 |
| Events delivered more than once, same assumption | 4.99 percent |
The signature rows use the signed text format of Stripe's scheme, the timestamp, a full stop, then the body, with the illustrative secret whsec_illustrative, and the 256-bit hash1. Changing one digit gives an unrelated value, which is why a tampered body fails verification.
The duplicate rows come from a simulation of 100,000 events in which each acknowledgement is lost with probability 0.05, and the sender repeats until one is received. The measured mean of 1.0526 deliveries per event matches the formula 1 divided by 0.951. In the simulation 95.01 percent of events arrived once, 4.72 percent twice, 0.25 percent three times and 0.01 percent four or more times.
A handler without a processed-ID log would apply 105,260 effects for 100,000 events: 5,260 extra charges, emails or shipments spread over 4,990 orders (4.99 percent), some of them hit three or four times. A log whose check and effect run in one transaction under a unique constraint brings that back to 100,000; a log that merely checks first and acts later can still double-apply when two deliveries of one event run at the same moment.1
Text description of the diagram
- In a simulation of 100,000 events with 5 percent lost acknowledgements, 95.01 percent arrived once.
- 4.72 percent arrived twice and 0.25 percent three times.
Retry timing follows the same arithmetic. An illustrative schedule that starts at one minute and doubles for ten retries waits 1,023 minutes, 17.1 hours, in total1. Stripe's real window is up to three days in live mode (Stripe documentation, as of October 2026), so a receiver that is down for a weekend can still catch up, but only if it comes back and answers with a 2xx.
Trade-offs and edge cases
Webhooks trade freshness against control. The sender decides when to call, so the receiver must stay reachable and absorb bursts. Polling puts the receiver in charge of its own rate but wastes requests and adds delay, as the example showed. A common design uses both: webhooks for speed and a slow reconciliation poll that repairs anything a webhook missed.
Three edge cases cause incidents. First, out-of-order events: a "subscription cancelled" event can arrive before "subscription created", so handlers should read current state from the provider's application programming interface (API) instead of trusting arrival order (Stripe documentation, as of October 2026). Second, a long-running handler: if work happens before the 2xx, the provider sees a timeout and retries while the first attempt is still running, which creates a duplicate by design. Third, secret rotation: Stripe lets you keep the old secret active for up to 24 hours (as of October 2026) and signs with both during that window, so a rollout does not drop events.
Reported practice differs on whether to also allowlist the sender's Internet Protocol (IP) addresses. Stripe recommends both an allowlist and signature verification, while GitHub's validation page centres on the signature. Treat the signature as the check that cannot be skipped and the allowlist as an extra layer. Tracing every delivery by event ID is also what a payment flow needs, as in how to design a payment system.
Last, a webhook is not a stream. It usually carries one event per request (some providers post a batch of events as a JSON array) and makes no ordering or replay promise beyond retries, so a high-volume, ordered feed belongs on a message broker (a service that stores messages in order until consumers read them), covered in the system design guide.
How to answer it in an interview
What is a webhook, and how would you build a receiver you can trust?
A webhook is an HTTP callback: when an event happens, the provider POSTs a signed JSON body to a URL I registered, so I do not have to poll. The sender delivers at least once, so I design for repeats. I verify the signature on the raw body using the shared secret and a constant-time comparison, check the timestamp, write the event to a durable queue, and only then return a 2xx.
The worker inserts each event ID under a unique constraint in the same transaction as the effect, so retries do no harm; some duplicates carry different IDs, so I also match on object ID and type. I would also run a slow reconciliation poll to catch anything dropped.
The spoken structure is: define it in a sentence, say the delivery semantics, then verify, enqueue, acknowledge, then deduplicate in the worker.
For a design round, tie it to the problem; the dedupe check is a race-condition problem of the kind in the concurrency interview questions. In a payment flow the dedupe key protects against charging twice. In a notification service it protects against emailing twice. Say which side effect your queue worker guards.
Common mistakes
- "Webhooks arrive exactly once" (wrong). Most providers retry when they hear no success, so the same event can arrive several times (right: log processed event IDs and make the handler idempotent).
- "I do the work, then return a success status" (wrong). A slow handler times out and triggers a retry while it is still running (right: store the verified event durably, return the 2xx, then work from the queue).
- "I compare the signature with ==" (wrong). A plain comparison can leak timing information (right: use a constant-time comparison on the raw body).
- "Events arrive in order" (wrong). Providers such as Stripe state that order is not promised (right: fetch current state instead of trusting sequence).
- "A webhook replaces an API" (wrong). The webhook says something happened; you usually call the API to get the full, current object (right: treat the webhook as a trigger).
Follow-up questions
What if a large share of acknowledgements are lost?
The duplicates grow quickly. Re-running the same simulation with 20 percent of acknowledgements lost gives 1.25 deliveries per event, and 20.09 percent of events arrive more than once, against 4.99 percent at 5 percent loss1. The design does not change, but the share of events hit more than once rises about four-fold. Interviewers also ask how webhooks survive a consumer outage; the answer is the same retry-plus-idempotency pair.
What is the difference between a webhook and an API?
An API is a set of endpoints you call when you want something, while a webhook is an endpoint of yours that the provider calls when something happens. Most integrations use both: the webhook announces the event and the API returns the full current object.
What is the difference between a webhook and polling?
Polling asks repeatedly and wastes most requests when events are rare, as 2,870 of 2,880 daily polls did in the example. A webhook is pushed by the provider, so the receiver hears about an event as it happens, at the price of staying reachable.
How do you secure a webhook endpoint?
Use HTTPS, verify an HMAC signature of the raw body with a constant-time comparison, reject old timestamps, keep the secret out of source code and rotate it periodically. An IP allowlist can sit on top as an extra layer, as in Stripe's guidance.
What happens if my server is down when a webhook is sent?
Most providers treat a missing reply as a failure and retry with growing gaps. Stripe's live-mode window is up to three days (Stripe documentation, as of October 2026), after which events must be recovered manually, so alert on failed deliveries.
What if the same event arrives twice?
A correct handler produces the same result. Keep a table of processed event IDs and skip repeats, and for effects such as payments pass the event ID as an idempotency key to downstream calls as well. For the surrounding interface design, see the representational state transfer (REST) API interview questions and the API design interview questions.
- A webhook is an HTTP POST the provider sends to your URL when an event happens; it replaces polling.
- Verify an HMAC signature on the raw body with a constant-time comparison and a timestamp check.
- Verify and enqueue durably, then return a 2xx; do the work from the queue.
- Delivery is at least once and unordered: log event IDs and fetch current state.
- With 5 percent lost acknowledgements, about 1 event in 20 arrives more than once, so idempotency is not optional.
Sources
- Receive Stripe events in your webhook endpoint — Stripe (page shows no date; accessed October 2026), 24 Oct 2026
- Validating webhook deliveries — GitHub Docs (page shows no date; accessed October 2026), 24 Oct 2026
- Handling failed webhook deliveries — GitHub Docs (page shows no date; accessed October 2026), 25 Oct 2026
- RFC 2104: HMAC: Keyed-Hashing for Message Authentication — IETF (Krawczyk, Bellare, Canetti), 1 Feb 1997