What Is a Webhook? Event-Driven HTTP, Explained
A webhook is an HTTP callback that notifies your server the moment something happens — no polling. How webhooks work and how to use them safely.
A webhook is a user-defined HTTP callback: rather than your application repeatedly asking a service “did anything happen?”, the service sends an HTTP POST to a URL you control the moment an event occurs. Stripe fires a webhook when a payment succeeds. GitHub fires one when someone pushes a commit. The webhook receiver is just an endpoint — a route in your web application — that accepts the POST and acts on its payload. The model is sometimes called a reverse API because the data flows from provider to consumer without the consumer asking.
How a webhook works
The lifecycle is straightforward:
- Register your URL. You tell the provider (Stripe, GitHub, Shopify, etc.) your endpoint URL and which events you care about —
payment_intent.succeeded,push,order.created. - An event fires. Something happens on the provider’s side that matches your subscription.
- The provider POSTs a payload. The provider sends an HTTP POST to your URL with a JSON body describing the event.
- You respond quickly. Your endpoint returns a
2xxstatus code — ideally within a few seconds. The provider interprets anything else (or a timeout) as a failure and will retry.
The payload is almost always JSON. Here’s a simplified Stripe example:
{
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_abc123",
"amount": 4999,
"currency": "usd"
}
}
}
Verifying authenticity
Anyone who knows your webhook URL can POST to it. Legitimate providers prevent spoofing by signing each request. Stripe, for example, includes a Stripe-Signature header containing an HMAC-SHA256 signature computed from the raw request body and a secret you hold. Your handler recomputes the signature and rejects requests that don’t match.
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
app.post("/webhooks/stripe", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["stripe-signature"];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
return res.status(400).send(`Webhook error: ${err.message}`);
}
// handle event
res.json({ received: true });
});
Always verify signatures. A missing check is a common security gap — malicious payloads could trigger fulfillment, refunds, or privilege escalation. Pair this with HTTPS-only endpoints; never accept webhook deliveries over plain HTTP. If you issue JWTs elsewhere in your stack, the same principle applies: always verify tokens and signatures before trusting their contents.
Respond fast, process async
Providers typically expect a 2xx response within 5–30 seconds. If your handler does anything slow — sending an email, calling a third-party API, writing to a database — it risks timing out, which triggers a retry. The pattern to follow:
- Validate the signature.
- Return
200 OKimmediately. - Enqueue the work (a job queue, a Kafka topic, or even a simple async task).
- Process in the background.
This keeps your endpoint fast and decouples ingestion from processing.
Idempotency and retries
Providers will retry failed deliveries — often with exponential backoff over minutes or hours. Your handler must be idempotent: processing the same event twice should produce the same result as processing it once. The standard approach is to store the event ID in a database and skip processing if you’ve seen it before. Without this, a retry can duplicate a charge, send two confirmation emails, or create two accounts.
Webhooks vs. polling vs. streaming
| Technique | Who initiates | Latency | Complexity |
|---|---|---|---|
| Polling | Your app, on a timer | Up to your interval | Low |
| Webhook | The provider, on event | Near-instant | Medium |
| SSE / WebSocket | Server (persistent connection) | Near-instant | Higher |
Polling is the easiest starting point but wastes resources and adds latency proportional to your poll interval. A REST API you poll every minute misses events for up to 60 seconds. Webhooks eliminate that gap entirely. Server-Sent Events and WebSockets offer similar latency but require a persistent connection and are better suited to browser clients rather than server-to-server integrations.
Debugging tips
Testing webhooks locally means your localhost isn’t reachable from the internet. Tools like ngrok or the Stripe CLI (stripe listen --forward-to localhost:3000) create a public tunnel to your local server. Most provider dashboards also let you resend specific events, which is invaluable during development.
Log every incoming payload before doing anything with it. When something goes wrong — and it will — having the raw payload makes debugging straightforward.
The takeaway
Webhooks are the simplest way to build event-driven integrations between services. Register a URL, verify signatures, return 2xx fast, process asynchronously, and handle retries with idempotency checks. Get those four things right and webhooks are reliable, low-overhead, and much more efficient than polling. They’re the backbone of every modern payment integration, CI/CD pipeline trigger, and third-party automation.
Tagged
Keep reading
The Lycoris Team · · 3 min read What Is an API? The Contracts That Connect Software
An API is a defined contract that lets one piece of software talk to another. Learn what APIs are, how they work, and why modern software runs on them.
Takina · · 4 min read What Is a Lockfile? Reproducible Dependency Installs
A lockfile records the exact dependency versions your package manager resolved, so every install — from your laptop to CI — reproduces the same tree.
Takina · · 4 min read JavaScript Intl API: Formatting Dates and Numbers
The Intl API formats dates, numbers, and currency using a user's locale without a library. How Intl.DateTimeFormat and Intl.NumberFormat work.