A webhook is an HTTP callback that sends data from one application to another when a specific event occurs. Instead of constantly checking for updates (polling), webhooks push information the moment something happens, giving your application real-time data without wasting resources on empty requests.
How Webhooks Work
A webhook follows a simple flow: an event happens, and a server sends an HTTP POST request to a URL you provide. Here is what that looks like step by step.
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ │ │ │ │ │
│ Event │────────▶│ Source System │────────▶│ Your Server │
│ (payment, │ triggers│ (Stripe, GitHub) │ HTTP │ (webhook │
│ push, PR) │ │ │ POST │ endpoint) │
│ │ │ │ │ │
└──────────────┘ └──────────────────┘ └──────┬───────┘
│
▼
Process event
Return 200 OK
- You register a URL. Tell the source system where to send events. This URL is your webhook endpoint.
- An event occurs. A customer pays an invoice, a developer opens a pull request, or a message arrives.
- The source sends a POST request. The request body contains a JSON payload with event details.
- Your server processes the event. Parse the payload, run your logic, then return a
200 OKstatus code. - The source confirms delivery. If your server does not respond with a 2xx status, most providers retry the request.
That is the entire flow. No polling, no wasted requests, no delays.
Webhooks vs Polling
Polling means your application asks "anything new?" on a loop. Webhooks flip this around — the source tells you when something happens.
| Polling | Webhooks | |
|---|---|---|
| Direction | Your app asks the server | Server tells your app |
| Timing | Periodic (every 30s, 1m, 5m) | Instant when event occurs |
| Wasted requests | Most requests return nothing | Only fires when there is data |
| Server load | High (constant requests) | Low (event-driven) |
| Complexity | Simple to implement | Requires a public endpoint |
| Best for | Small datasets, infrequent checks | Real-time updates, high volume |
Use polling when you need data on a schedule or the source does not support webhooks. Use webhooks for everything else.
Anatomy of a Webhook Payload
When a webhook fires, the source sends an HTTP POST request to your URL. Here is what a real Stripe webhook payload looks like:
{
"id": "evt_1SXJAxIvz5j75Cdbc84ixIMX",
"object": "event",
"type": "invoice.payment_failed",
"created": 1764065326,
"data": {
"object": {
"id": "in_1SXJAtIvz5j75CdbK1yFGg0d",
"customer": "cus_TUHkz63gW41i4G",
"amount_due": 2000,
"currency": "usd",
"status": "open",
"attempt_count": 1
}
}
}
Every webhook payload shares the same basic structure:
- Event type — What happened (
invoice.payment_failed,push,pull_request.opened). - Timestamp — When it happened.
- Data — The details. For a payment, this includes amount, currency, and customer ID. For a pull request, it includes the title, author, and diff URL.
The request also includes HTTP headers. These carry metadata like content type, delivery IDs, and — critically — signature headers for verification. GitHub uses X-Hub-Signature-256. Stripe uses Stripe-Signature. These headers prove the request came from the real source and was not tampered with.
Build a Webhook Receiver
A webhook receiver is a server endpoint that accepts POST requests from external services. Here is a minimal receiver in Node.js using Express:
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks', (req, res) => {
const event = req.body;
console.log(`Received event: ${event.type}`);
switch (event.type) {
case 'payment.completed':
// Send a receipt email
console.log(`Payment received: $${event.data.amount / 100}`);
break;
case 'user.created':
// Set up the new account
console.log(`New user: ${event.data.email}`);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.sendStatus(200);
});
app.listen(3000, () => {
console.log('Webhook receiver running on port 3000');
});
This covers the basics: accept POST requests, parse the JSON body, route by event type, return a 200 status. For a hands-on walkthrough with more examples, see the webhooks tutorial.
Test Locally with cURL
You can test your receiver without a real webhook provider. Send a fake event with cURL:
curl -X POST http://localhost:3000/webhooks \
-H "Content-Type: application/json" \
-d '{
"type": "payment.completed",
"data": {
"amount": 4999,
"currency": "usd",
"customer_id": "cus_abc123"
}
}'
Your server logs should print Payment received: $49.99.
Expose Your Local Server
Webhook providers need a public URL to send requests to. During development, use ngrok to expose your local server:
ngrok http 3000
Copy the generated HTTPS URL (like https://a1b2c3.ngrok.io) and paste it into the webhook settings of your provider. Add /webhooks to the end.
Verify Webhook Signatures
Anyone can send a POST request to your webhook URL. Without verification, an attacker could send fake events and trick your application into processing them. This is why webhook providers include a signature with every request.
The most common pattern is HMAC-SHA256. The provider signs the request body with a shared secret, and you verify the signature on your end.
Here is how signature verification works in Node.js:
const crypto = require('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));
}
app.post('/webhooks', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const rawBody = JSON.stringify(req.body);
if (!verifySignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Signature is valid — process the event
res.sendStatus(200);
});
Key points for verification:
- Use the raw request body, not a re-serialized version. Re-serializing can change whitespace or key order, which breaks the signature.
- Use timing-safe comparison (
crypto.timingSafeEqual). Regular string comparison leaks information through timing differences. - Check timestamps when available. Stripe includes a timestamp in its signature header. Reject events older than 5 minutes to prevent replay attacks.
Different providers use different header names and formats. Here are the most common ones:
| Provider | Signature Header | Algorithm |
|---|---|---|
| Stripe | Stripe-Signature |
HMAC-SHA256 with timestamp |
| GitHub | X-Hub-Signature-256 |
HMAC-SHA256 |
| Twilio | X-Twilio-Signature |
HMAC-SHA1 |
| Shopify | X-Shopify-Hmac-Sha256 |
HMAC-SHA256 (Base64) |
For provider-specific guides, see how to verify Stripe webhooks and GitHub webhooks.
Handle Retries and Failures
Webhook providers expect a fast response. If your server does not return a 2xx status within a few seconds, the provider assumes delivery failed and retries.
Best practices for handling retries:
- Return 200 immediately. Process the event asynchronously if it takes more than a few seconds. Acknowledge receipt first, then handle the business logic in a background job.
- Make your handler idempotent. Retries mean your server might receive the same event twice. Use the event ID to track what you have already processed.
- Log failed deliveries. If your server is down, you will miss events. Most providers have a dashboard where you can see failed deliveries and manually retry them.
const processedEvents = new Set();
app.post('/webhooks', (req, res) => {
const eventId = req.body.id;
// Skip if already processed
if (processedEvents.has(eventId)) {
return res.sendStatus(200);
}
processedEvents.add(eventId);
// Process event...
res.sendStatus(200);
});
In production, store processed event IDs in a database instead of an in-memory set.
Common Webhook Providers
Most services you work with support webhooks. Here are the most popular ones and what you can do with them:
- Stripe — Payment events like charges, refunds, subscription changes, and invoice failures. Powers payment notifications and failed payment alerts.
- GitHub — Repository events like pushes, pull requests, issues, and CI/CD completions. Supports 73+ event types.
- Slack — Incoming webhooks let you post messages to channels from external services.
- AWS SNS — Cloud infrastructure events like alarms, deployment status, and service health changes.
- Twilio — SMS delivery receipts, incoming messages, and call status updates.
For tools that help manage webhook infrastructure at scale, see the guide to open-source webhook services.
Webhooks vs APIs
Webhooks and APIs solve different problems. An API lets you request data on demand. A webhook delivers data when something happens. Many systems use both together.
API (Pull):
Your App ──── "Any new orders?" ────▶ Server
Your App ◀──── "No." ──────────────── Server
Your App ──── "Any new orders?" ────▶ Server
Your App ◀──── "Yes, here's one." ─── Server
Webhook (Push):
Server ──── "New order just placed" ────▶ Your App
Use APIs when you need data on demand (loading a user profile, searching products). Use webhooks when you need to react to events in real time (payment received, code pushed, message sent). Most production systems combine both: webhooks trigger actions, APIs fetch additional data when needed.
Best Practices
Follow these rules to build reliable webhook integrations:
- Always use HTTPS. Webhook payloads often contain sensitive data like customer information or payment details. Never accept webhooks over plain HTTP.
- Verify every request. Check the HMAC signature before processing any event. Reject unsigned or incorrectly signed requests.
- Respond fast. Return a 200 status within 3 seconds. Move heavy processing to a background queue.
- Handle duplicates. Store processed event IDs and skip events you have already handled.
- Log everything. Record the event type, timestamp, and processing result for every webhook. This makes debugging much easier when something goes wrong.
- Set up monitoring. Track your webhook endpoint's response time, error rate, and queue depth. Alert on failures so you can fix issues before they affect users.
- Use a dedicated endpoint. Do not mix webhook handling with your regular API routes. A separate
/webhookspath makes it easier to apply middleware, rate limiting, and monitoring.
Next Steps
Now that you understand how webhooks work, dive deeper with these guides:
- Webhooks tutorial — Build a complete webhook integration from scratch.
- Stripe webhooks guide — Handle payment events with signature verification.
- GitHub webhooks guide — Set up repository event notifications.
- Create a Slack webhook URL — Send messages to Slack channels from any service.
- DynamoDB Streams real-time notifications guide — Trigger notifications from database changes using AWS Lambda.
To send webhook-triggered notifications across Slack, email, push, and in-app from a single API, check out MagicBell.
