Webhooks Explained: Real-Time Event Notifications in 2025
How webhooks push events to your app, how to verify signatures and handle retries, and how to design reliable endpoints.
In modern application architecture, real-time communication between services is essential. While REST APIs are great for request-response patterns, they fall short when you need instant notifications about events. Enter webhooks—a powerful mechanism for pushing data from one application to another in real-time. From payment processing to CI/CD pipelines, webhooks power the integrations that make modern software work.
What Are Webhooks?
A webhook is an HTTP callback—an HTTP POST request that occurs when something happens. Instead of your application polling an API repeatedly asking "did anything change?", the external service sends a POST request to your server when an event occurs. This is often called "reverse API" or "HTTP push."
Think of it like this: APIs are like calling someone—you initiate the conversation. Webhooks are like someone calling you—they notify you when something happens.
Webhooks vs Polling
The traditional alternative to webhooks is polling—repeatedly asking an API "has anything changed?" Let's compare:
Polling (Inefficient)
// Every 5 seconds, check for updates
setInterval(async () => {
const response = await fetch('https://api.example.com/events');
const events = await response.json();
if (events.length > 0) {
// Process events
}
}, 5000);
// Problems:
// - Wastes bandwidth (99% of requests return no data)
// - Delayed notifications (up to 5 seconds)
// - Server load from constant requests
Webhooks (Efficient)
// Register webhook URL
POST https://api.example.com/webhooks
{
"url": "https://yourapp.com/webhooks/payment",
"events": ["payment.completed", "payment.failed"]
}
// When event occurs, service calls your endpoint
POST https://yourapp.com/webhooks/payment
{
"event": "payment.completed",
"data": { "id": "pay_123", "amount": 1000 }
}
// Benefits:
// - Instant notifications
// - No wasted requests
// - Efficient resource usage
Common Webhook Use Cases
Payment Processing
Payment providers like Stripe, PayPal, and Square use webhooks to notify your application when payments succeed, fail, or are refunded. This is critical because payment processing is asynchronous—you can't wait for the response.
// Stripe webhook example
POST /webhooks/stripe
{
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_123",
"amount": 2000,
"currency": "usd"
}
}
}
CI/CD Pipelines
GitHub, GitLab, and Bitbucket send webhooks when code is pushed, pull requests are opened, or builds complete. This triggers automated deployments, notifications, and integrations.
Communication Platforms
Slack, Discord, and Microsoft Teams use webhooks to send messages to channels. This enables bots, notifications, and integrations.
E-commerce Platforms
Shopify, WooCommerce, and other e-commerce platforms send webhooks for order creation, inventory updates, and customer changes.
Webhook Security: Critical Considerations
Webhooks are HTTP requests from external services to your server. This creates security challenges—how do you verify the request is legitimate?
1. Signature Verification
Most webhook providers include a signature in the request headers. This signature is computed using a secret key and the request body. Always verify signatures before processing webhooks.
// Stripe signature verification
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
// Event is verified, process it
} catch (err) {
// Invalid signature, reject request
return res.status(400).send('Invalid signature');
}
2. HTTPS Only
Always use HTTPS for webhook endpoints. Webhook payloads often contain sensitive data, and HTTP exposes this data to anyone on the network.
3. IP Whitelisting
Some providers publish their webhook IP ranges. You can whitelist these IPs at the firewall level for additional security. However, IP ranges can change, so signature verification is still essential.
4. Idempotency Keys
Webhooks can be delivered multiple times (due to retries). Use idempotency keys to ensure you don't process the same event twice. Store processed event IDs and check before processing.
// Idempotency check
const eventId = req.body.id;
if (await db.webhookEvents.exists(eventId)) {
// Already processed, return success
return res.status(200).json({ received: true });
}
// Process event
await processWebhookEvent(req.body);
// Mark as processed
await db.webhookEvents.create({ id: eventId });
Webhook Retry Strategies
Webhook delivery is not guaranteed. Network issues, server downtime, or timeouts can cause delivery failures. Providers implement retry strategies:
- Exponential backoff: Retry after 1s, 2s, 4s, 8s, etc.
- Maximum retries: Usually 3-5 attempts over 24-48 hours
- Dead letter queue: Failed webhooks stored for manual review
Best Practices for Handling Retries
- Return 200 quickly: Acknowledge receipt immediately, process asynchronously
- Handle timeouts: Webhook processing should complete within 5-10 seconds
- Log everything: Track all webhook attempts for debugging
- Monitor failures: Alert on repeated webhook failures
Implementing Webhook Endpoints
Express.js Example
const express = require('express');
const crypto = require('crypto');
const app = express();
// Middleware to capture raw body for signature verification
app.use('/webhooks', express.raw({ type: 'application/json' }));
app.post('/webhooks/stripe', async (req, res) => {
// 1. Verify signature
const sig = req.headers['stripe-signature'];
const secret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, secret);
} catch (err) {
return res.status(400).send('Invalid signature');
}
// 2. Check idempotency
if (await isEventProcessed(event.id)) {
return res.status(200).json({ received: true });
}
// 3. Process asynchronously
processWebhookEvent(event).catch(console.error);
// 4. Acknowledge immediately
res.status(200).json({ received: true });
});
async function processWebhookEvent(event) {
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object);
break;
case 'payment_intent.failed':
await handlePaymentFailure(event.data.object);
break;
default:
console.log('Unhandled event type:', event.type);
}
}
Next.js API Route Example
// pages/api/webhooks/stripe.ts
import { NextApiRequest, NextApiResponse } from 'next';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
export const config = {
api: {
bodyParser: false, // Need raw body for signature verification
},
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const sig = req.headers['stripe-signature']!;
const buffer = await getRawBody(req);
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
buffer,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return res.status(400).send('Invalid signature');
}
// Process event
await handleWebhookEvent(event);
res.status(200).json({ received: true });
}
Testing Webhooks
Testing webhooks can be challenging because they require external services to send requests to your local development server. Here are strategies:
1. Webhook Testing Tools
Services like ngrok, webhook.site, or RequestBin provide public URLs that forward requests to your local server. This allows you to test webhooks during development.
2. Mock Webhook Servers
Create a mock server that sends webhook requests matching your provider's format. This is useful for integration tests.
3. Provider Test Modes
Most providers offer test/sandbox modes with test webhook endpoints. Use these for development and staging environments.
Webhook Best Practices
- Always verify signatures: Never trust webhook requests without verification
- Process asynchronously: Acknowledge receipt immediately, process in background
- Implement idempotency: Handle duplicate deliveries gracefully
- Log everything: Webhook debugging is difficult without comprehensive logs
- Monitor failures: Set up alerts for webhook processing failures
- Use HTTPS: Always use encrypted connections
- Version your endpoints: Include version in URL path for future changes
- Document expected payloads: Make it clear what events you handle
Common Webhook Patterns
Event Sourcing
Store all webhook events in a database. This creates an audit trail and allows you to replay events for debugging or recovery.
Webhook Queues
Use message queues (RabbitMQ, SQS, etc.) to handle webhook processing. This provides reliability, scalability, and retry mechanisms.
Fan-Out Pattern
When a webhook arrives, fan it out to multiple handlers. This allows different parts of your system to react to the same event independently.
Conclusion
Webhooks are essential for building modern, real-time applications. They enable efficient event-driven architectures and eliminate the need for constant polling. However, they require careful attention to security, reliability, and error handling.
When implementing webhooks, prioritize security (signature verification), reliability (idempotency, retries), and observability (logging, monitoring). With these practices in place, webhooks become a powerful tool for building scalable, responsive applications.
Try these tools
Use these tools alongside this guide
Part of the ThenCatch blog. Learn more about us or browse more guides.