What Are Webhooks and How to Integrate Events Between Systems
Understand what webhooks are, how they differ from an API, and how to integrate events between systems with security, retries, and idempotency.
You wired two systems together and everything works — but only because a cron job fires a request every minute asking "anything new?" The overwhelming majority of the time the answer is "no." You're burning bandwidth, CPU, and latency just to learn that nothing changed. And when something does change, the user waits up to 60 seconds to see it. That waste has a name: polling. The way to kill it is to flip the direction of the conversation. That's exactly what a webhook does.
What a webhook is, in one sentence
A webhook is an HTTP request that one system sends automatically to a URL of yours when an event happens. Instead of you repeatedly asking "did the payment arrive yet?", the payment gateway notifies you the instant it lands. The logic inverts: instead of pull (you fetch the data), you get a push (the data is sent to you).
That's why webhooks are sometimes called a "reverse API" or an "HTTP callback." You don't call the service — the service calls you.
Polling versus webhook in practice
The difference is clearest with numbers. Say you want to know when an order changes status.
With polling, your code runs in a loop:
// Every 30 seconds, ask whether anything changed
setInterval(async () => {
const order = await fetch('https://api.shop.com/orders/123');
const data = await order.json();
if (data.status === 'shipped') notifyCustomer();
}, 30_000);
That's 2,880 requests a day for a single order — and the status changes maybe once. Multiply by 10,000 orders and you have a scaling problem.
With a webhook, you register one endpoint and wait. The shop's server does the work:
// You only hear from it when something actually happens
app.post('/webhooks/orders', (req, res) => {
const { event, order } = req.body;
if (event === 'order.shipped') notifyCustomer(order);
res.sendStatus(200); // acknowledge quickly
});
One request, at the right moment. Zero waste.
How the flow works end to end
A webhook's lifecycle has three actors and four steps:
- Registration. You tell the provider (Stripe, GitHub, PayPal) which URL should receive notifications and which events you care about. Usually done in the service dashboard or via API.
- Event. Something happens on the provider's side — a payment is approved, a push hits the repo, an email gets opened.
- Delivery. The provider builds a payload (almost always JSON) and fires a
POSTat your URL. - Processing. Your endpoint receives it, validates it, does its thing, and responds
2xxto confirm.
The detail that separates an amateur integration from a robust one lives in step 4. From the provider's point of view, webhooks are fire-and-forget — but with guarantees: if you don't answer 2xx quickly, it will try again.
The three mistakes that take down webhook integrations
1. Processing everything synchronously before responding
Most providers expect a response within seconds (Stripe times out around 20s, GitHub around 10s). If your handler hits a slow database, generates a PDF, and sends an email before responding 200, you'll blow the timeout — and the provider will resend the same event, creating duplicates.
The golden rule: respond first, process later. Queue the heavy work.
app.post('/webhooks/payments', async (req, res) => {
await queue.enqueue('process-payment', req.body); // milliseconds
res.sendStatus(200); // acknowledge instantly
});
2. Trusting the payload without verifying the signature
Your webhook URL is public. Anyone who finds it can send a fake POST claiming "payment approved." That's why serious providers sign every request with an HMAC, usually in a header like X-Signature. You recompute the hash with your shared secret and compare.
import crypto from 'crypto';
function signatureValid(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
Always use timingSafeEqual (constant-time comparison) so you don't leak information through a timing attack. And validate against the raw body, not the JSON after it's been parsed and re-serialized — key order changes and the signature breaks.
3. Not handling redelivery and ordering
Webhooks offer at-least-once delivery, not exactly-once. The same event can arrive twice (because your 200 got lost in transit, for instance). And nothing guarantees events arrive in the order they happened. The fix is to make your handler idempotent: each event carries a unique ID, and you store the IDs you've already processed.
if (await alreadyProcessed(event.id)) return res.sendStatus(200);
await process(event);
await markProcessed(event.id);
Where webhooks shine (and where they don't)
Webhooks are the right call for: payments (async confirmation), CI/CD (a push triggers the pipeline), communication between microservices, no-code integrations via Zapier/Make, and chat notifications (a finished deploy pings Slack).
But they're not a silver bullet. If you need an immediate, bidirectional response — like a real-time chat or a dashboard that updates on every tick — WebSockets or Server-Sent Events serve you better. And if the consumer can't expose a public endpoint (a mobile app, say), webhooks don't apply; you fall back to polling or push notifications.
Worth noting: webhooks and REST aren't competitors. Webhooks are usually implemented on top of the same principles as a good HTTP API. If you're still modeling your endpoints, the same care around verbs, status codes, and versioning that I cover in REST API: principles and best practices applies to the endpoint that receives the webhook too.
How to test webhooks without losing a night
The classic problem: the provider needs a public URL, but your code runs on localhost. Three paths solve it:
- Local tunnel: tools like
ngrokorcloudflaredexpose your local port with a temporary HTTPS URL. You paste that URL into the provider's dashboard and events land straight on your machine. - The provider's own CLI: Stripe and GitHub ship CLIs (
stripe listen,gh webhook forward) that forward real events to localhost without a tunnel. - Request inspectors: sites like webhook.site give you a throwaway URL that shows you the exact payload and headers received — great for understanding the format before you write a single line of code.
Start with the inspector to learn the format, then move to the tunnel when you're coding the real handler.
Frequently asked questions
What's the difference between a webhook and an API?
A REST API is something you call: you make a request and get the response right away (pull). A webhook is the reverse: the server calls you when an event occurs (push). In practice a webhook is delivered through an HTTP API, but the direction of the initiative is flipped. Use an API when you need the data now; use a webhook when you want to be told when the data changes.
Are webhooks secure?
They can be, as long as you do three things: serve the endpoint over HTTPS only, verify the HMAC signature on every request (to confirm it really came from the provider), and treat the handler as idempotent. Without signature verification, anyone who discovers your URL can forge events — so that step isn't optional.
What happens if my server is down when the event fires?
Most providers have a retry policy: if you don't answer 2xx, they resend the event with exponential backoff for hours or days. Even so, design for failure — log missed events and, if the provider offers it, use the "resend event" API or a listing endpoint to reconcile what slipped through.
Can I use webhooks between my own internal microservices?
Yes, and it's a common pattern. But internally, where you control both sides, a message queue (RabbitMQ, Kafka, SQS) or an event bus is often a better fit — they give you ordering, retry, and persistence guarantees without you reimplementing everything. HTTP webhooks make the most sense when one end is an external system you don't control.
What to take from this guide
A webhook is neither new nor complicated: it's just one HTTP POST at the right moment instead of a thousand GETs in the dark. What separates an integration that survives production from one that wakes you up at 3 a.m. isn't the protocol — it's discipline at the three critical points: respond fast and process in the background, verify the signature on every request, and make the handler idempotent. Nail those three and you'll have a reliable event pipeline between any two systems.
- 01 UTF-8 and encoding problems: why accents break and how to fix them What UTF-8 is, code points, mojibake, and why accented text turns to garbage — the mechanism explained end to end.
- 02 MIT, Apache, and GPL Licenses: Practical Differences for Developers MIT, Apache 2.0, and GPL are not the same thing. Understand permissive vs copyleft, patent protection, and compatibility before shipping code to production.