WebHooks flip the request model: instead of your app asking a service "anything new?" over and over, the service calls you the instant an event happens. Imagine you're waiting for a package. You could call the delivery company every five minutes to ask "is it here yet?" or you could give them your phone number and let them call you when it arrives. Webhooks are the phone number.
What they are, when to use them
Before webhooks, apps used polling : make a GET request on a timer, check for changes, repeat. This wastes bandwidth, hammers rate limits, and adds delay since you only find out about an event on the next scheduled check. Webhooks eliminate all three problems.
A webhook is just an HTTP POST: the event source sends JSON to a URL you registered, and your server processes it. That's the whole mechanism. No special protocol, no persistent connection, no SDK required.
How it works
- Register: you tell the third-party service: "POST to this URL when things happen." Usually done in a dashboard or via an API call.
- Event fires: something happens on their side (a payment completes, a pull request is merged). They build a JSON payload describing it.
- POST arrives: they send an HTTP POST to your URL. Your server must be publicly reachable.
- Respond before the timeout: return a
200 OKbefore the sender's timeout, or it assumes delivery failed and retries. Where you do the work depends on how slow it is:- Quick work, do it inline: for something fast like a small DB write, just do it and then respond. You avoid the moving parts of a queue and the result is reflected immediately.
- Slow work, queue it first: for something slow or unreliable (send an email, call another API, run a report), drop the event on a queue and return
200 OKright away. A background job does the real work outside the response, so a slow task can't blow the timeout and trigger a retry. The trade-off is added latency before the work actually finishes.
Senders typically add custom headers so you can route the event before parsing the body:
x-webhook-event here, x-github-event or stripe-signature in
the wild.
Common reasons you may need one
- Payment events (charge succeeded, subscription cancelled) from Stripe and friends
- CI/CD and repo events (push, PR merged) from GitHub
- Any third-party integration where polling would be too slow or too expensive
- Notifying your own services of events without coupling them together
Idempotency: handling duplicate deliveries
Networks are unreliable. If your server is slow to respond, the sender will retry, delivering the same event more than once. Without a guard, you might charge a card twice or send two welcome emails. Every webhook payload includes a unique event ID. Before processing, check whether you've seen that ID before. If yes, respond 200 and skip the work. This property, where running the same operation multiple times has the same effect as running it once, is called idempotency.
// Use an atomic insert to prevent race conditions
async function handleWebhook(event) {
// If two duplicate webhooks hit at exactly the same time,
// the database enforces the unique constraint and fails one.
const inserted = await db.events.insertIfNotExists(event.id);
if (!inserted) return; // another request beat us to it
await processEvent(event); // safe to run exactly once
} Check your understanding
A slow handler keeps getting retried
Your handler sends a confirmation email inline before returning 200. The email provider is occasionally slow, and on those deliveries the sender gives up waiting and re-sends the same event. What's the right fix?