If you’ve worked with any REST API before, you know the drill: your app sends a request, the server sends a response. You ask, it answers. A webhook flips that relationship on its head. Instead of your app constantly asking “did anything change?”, the server calls you when something happens.
That’s why webhooks are sometimes called reverse APIs — the server initiates the conversation.
Polling vs. Webhooks
Imagine you’re waiting for a package. You have two options:
- Polling: Call the delivery company every five minutes and ask “is it here yet?” Most of the time the answer is no, and you’ve wasted a phone call.
- Webhooks: Tell the delivery company “text me when it arrives.” You go about your day and only get notified when it matters.
Polling works, but it’s wasteful. You burn API rate limits, add server load, and still have a delay between checks. Webhooks are event-driven — the notification is instant and you only process data when there’s actually something new.
| Polling | Webhooks | |
|---|---|---|
| Who initiates? | Your app | The external service |
| Timeliness | Delayed (depends on interval) | Near real-time |
| Resource usage | High (many empty requests) | Low (only fires on events) |
| Complexity | Simple to set up | Requires a public endpoint |
How Webhooks Work
The lifecycle of a webhook boils down to three steps:
-
You register a URL. You tell the external service: “When event X happens, send an HTTP request to this endpoint.” This is usually done through a dashboard or an API call.
-
An event fires. Something happens on the external service — a payment succeeds, a commit is pushed, a form is submitted.
-
The service sends an HTTP POST. The service makes a POST request to your registered URL with a JSON payload describing the event. Your server receives it, processes it, and returns a
200 OKto acknowledge receipt.
That’s it. No long-polling, no WebSocket connections, no cron jobs. Just a plain HTTP request triggered by an event.
Real-World Examples
Webhooks are everywhere. Here are a few you’ll run into quickly:
- Stripe sends a webhook when a payment succeeds, fails, or is refunded. This is how most apps confirm purchases server-side rather than trusting the client.
- GitHub fires webhooks on pushes, pull requests, issues, and more. This is the backbone of CI/CD pipelines — tools like GitHub Actions are triggered by these events.
- Slack uses webhooks in both directions: incoming webhooks let you post messages to a channel, and outgoing webhooks let Slack notify your app when someone types a slash command.
- Twilio sends a webhook to your server every time an SMS arrives or a call connects.
If a service has an API, there’s a good chance it also supports webhooks.
Build a Webhook Receiver in 5 Minutes
Let’s build a minimal Express.js server that receives a webhook. This is all you need:
import express from "express";
const app = express();
app.use(express.json());
app.post("/webhook", (req, res) => {
console.log("Event received:", req.body);
// Process the event here (update DB, send email, etc.)
res.sendStatus(200);
});
app.listen(3000, () => console.log("Listening on port 3000"));
That’s ten lines. Run it with node server.js, expose it with a tool like ngrok during development (ngrok http 3000), and point the external service’s webhook settings at your public URL.
The key rule: always return a 200 status quickly. Do your heavy processing asynchronously. If you take too long to respond, the sender may assume delivery failed and retry.
Security: Verify the Signature
Anyone who knows your webhook URL can send fake requests to it. That’s why most services sign their payloads with a shared secret. You should always verify the signature before trusting the data.
Here’s how signature verification typically works with an HMAC-based approach:
import crypto from "crypto";
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Use the raw request body (not the parsed JSON) when computing the HMAC. Most frameworks let you access this with a custom body parser. The exact header name varies by service — Stripe uses Stripe-Signature, GitHub uses X-Hub-Signature-256, and so on. Check the docs for whichever service you’re integrating with.
Retry Logic
Networks fail. Servers go down. Webhook providers know this, so most of them implement automatic retries. If your endpoint doesn’t return a 2xx response, the service will try again — often with exponential backoff over hours or even days.
This means your webhook handler needs to be idempotent: processing the same event twice should produce the same result. The simplest way to handle this is to store the event ID and skip duplicates:
const processed = new Set();
app.post("/webhook", (req, res) => {
if (processed.has(req.body.id)) return res.sendStatus(200);
processed.add(req.body.id);
// Handle event...
res.sendStatus(200);
});
In production, swap that Set for a database lookup.
Common Gotchas
A few things that trip up beginners:
- Your endpoint must be publicly reachable.
localhostwon’t work in production. Use ngrok or a similar tunnel for local development, and deploy to a real server for production. - Respond fast, process later. If your handler takes 30 seconds to run, the sender will time out and retry. Acknowledge the request immediately and push work to a background queue.
- Don’t trust the payload blindly. Always verify signatures. Always validate the data shape before acting on it.
- Watch for ordering issues. Webhooks can arrive out of order. A
payment.refundedevent might land beforepayment.succeededif there’s a retry in play. Design your handlers to cope with this. - Log everything. When something goes wrong (and it will), having the raw payloads in your logs makes debugging dramatically easier.
When to Use Webhooks vs. Other Approaches
Webhooks are ideal when you need to react to events from an external service in near real-time. If you’re building something where both sides are under your control and you need bidirectional, low-latency communication, WebSockets or server-sent events might be a better fit. And if you’re comparing data-fetching strategies for your own API, check out the differences between REST and GraphQL.
But for the classic use case — “tell my app when something happens on your platform” — webhooks are the standard. They’re simple, efficient, and supported by virtually every major service.
FAQ
What happens if my webhook endpoint goes down?
Most webhook providers implement automatic retries with exponential backoff. If your endpoint returns a non-2xx status or times out, the provider will retry — often over several hours or days. This is why your handler must be idempotent: processing the same event twice should produce the same result.
Do I need a dedicated server to receive webhooks?
Not necessarily — serverless functions (AWS Lambda, Vercel Functions, Cloudflare Workers) work great for webhook endpoints. They scale automatically and you only pay when events arrive. For local development, tools like ngrok create a public tunnel to your localhost so external services can reach your machine.
How do I debug webhooks during development?
Use a tunnel tool like ngrok to expose your local server, then trigger events from the external service’s dashboard. Log the raw request body and headers for every incoming webhook. Many services (Stripe, GitHub) also offer webhook event replay in their dashboards, letting you re-send past events for testing.
Now go register one and see it fire. There’s nothing quite like watching that first POST hit your server.