Introduction to Webhooks
Imagine you are waiting for a package delivery. You could walk to the front door every 5 minutes to check if it arrived (Polling), or you could relax inside and wait for the delivery driver to ring your doorbell (Webhooks).
Webhooks are the doorbell of the modern internet. Instead of your server constantly asking another service for updates, the other service notifies your server immediately when something happens.
1. Polling vs. Webhooks (Push vs. Pull)
| Feature | Polling (Client Pull) | Webhooks (Server Push) |
|---|---|---|
| How it works | Client repeatedly sends GET requests on a timer. | Server sends an HTTP POST request only when an event occurs. |
| Latency | Delayed by the polling interval (e.g., up to 60s). | Near real-time (milliseconds). |
| API & Server Load | High. 95%+ of requests return empty responses. | Minimal. Requests are sent only when data changes. |
| Network Efficiency | Wasteful bandwidth and compute. | Highly efficient. |
The Numbers: Polling vs. Webhooks
Suppose you check for new orders once every minute:
- Polling: $60 \times 24 = 1,440$ API calls per day per customer. If you have 30 customers, that's 43,200 requests/day—even if only 100 orders occurred!
- Webhooks: Only 100 calls per day (triggered solely when real orders happen). That is over 400x more efficient.
2. How Webhooks Work in Practice
Let's look at what happens during a real payment with Stripe:
3. Anatomy of a Webhook Request
A webhook is simply an incoming HTTP POST request carrying a JSON event payload and signature headers:
POST /webhooks/stripe HTTP/1.1
Host: api.yourdomain.com
Content-Type: application/json
Stripe-Signature: t=1699999999,v1=9f83a48e71c6...
{
"id": "evt_1092837465",
"type": "payment_intent.succeeded",
"created": 1699999999,
"data": {
"object": {
"id": "pi_3MtwBwLkdIwHu7ix0snN00fn",
"amount": 9900,
"currency": "usd",
"customer": "cus_9x8a7b6c",
"status": "succeeded"
}
}
}The 4 Key Components of Any Webhook:
- 🔗 Endpoint URL: The public route exposed on your backend (e.g.,
https://api.myapp.com/webhooks/stripe). - 📋 Event Type: The unique string identifier describing what happened (e.g.,
payment_intent.succeeded,user.deleted). - 📦 Payload: A structured JSON object containing the modified resource and its details.
- 🔐 Cryptographic Signature: A header hash used by your backend to verify that the request truly came from the provider and wasn't forged by an attacker.
4. Real-World Webhook Examples
A. GitHub Webhooks
When you push commits or open pull requests on GitHub, webhooks notify your CI/CD pipelines, Discord servers, or Jira boards:
{
"ref": "refs/heads/main",
"repository": {
"full_name": "developer/e-commerce-api"
},
"pusher": {
"name": "alex-dev",
"email": "alex@example.com"
},
"commits": [
{
"id": "c1a2b3d4",
"message": "fix: update stripe checkout tax calculation",
"added": ["src/tax.js"],
"modified": ["package.json"]
}
]
}| Event Name | Typical Developer Action |
|---|---|
push | Trigger automated CI/CD builds & deploy preview environments. |
pull_request | Run unit tests, execute linter checks, and update issue tracker. |
release | Publish packages to npm/Docker and send release notes. |
B. Stripe Payment Webhooks
Payment processing is inherently asynchronous (e.g., credit card 3D Secure authentication, bank transfers taking days, or refunds). Webhooks keep your database in sync:
payment_intent.succeeded: Unlock premium access, generate invoice, and ship order.invoice.payment_failed: Notify the user to update their expired payment card.customer.subscription.deleted: Downgrade user permissions to the free tier.charge.dispute.created: Alert the finance team of a chargeback.
5. Basic Webhook Receiver in Express.js
Here is a minimal Express webhook receiver endpoint:
import express from "express";
const app = express();
// Parse JSON request bodies
app.use(express.json());
app.post("/webhooks/stripe", (req, res) => {
const event = req.body;
// Process specific event types
switch (event.type) {
case "payment_intent.succeeded": {
const paymentIntent = event.data.object;
console.log(`💰 Payment succeeded for ID: ${paymentIntent.id}`);
// Run business logic: fulfill order, send invoice...
break;
}
case "payment_intent.payment_failed": {
const failedPayment = event.data.object;
console.warn(`❌ Payment failed: ${failedPayment.last_payment_error?.message}`);
// Notify customer...
break;
}
default:
console.log(`ℹ️ Unhandled event type: ${event.type}`);
}
// Always respond quickly with 200 OK so the provider knows you received it
res.status(200).json({ received: true });
});
app.listen(3000, () => {
console.log("⚡ Webhook server running on port 3000");
});6. When to Use Webhooks vs. Polling
💡 Summary
- Webhooks are HTTP push notifications sent from an event source to your server.
- Webhooks eliminate polling waste by only executing network calls when an event actually happens.
- Always return HTTP 200 quickly to prevent the sender from treating the webhook as failed.
- Security is critical: Because webhook endpoints are public URLs, you must verify cryptographic signatures before trusting incoming payloads.
👉 Next Step: Learn how to protect your webhook endpoints against forgery and replay attacks in Webhook Security & Signature Verification.