How to Receive Stripe Webhooks Reliably
A comprehensive guide to setting up, verifying, and handling Stripe webhooks in production. Learn best practices for idempotency, event ordering, and building reliable webhook endpoints with Node.js and Python examples.

How to Receive Stripe Webhooks Reliably
Stripe webhooks notify your application of important events—successful payments, subscription changes, failed invoices. Receiving them reliably in production is harder than it looks.
This guide covers everything from initial setup to production-ready best practices: signature verification, handling duplicates, processing asynchronously, testing, and hardening for failures.
Why Webhooks Matter for Stripe
Webhooks push critical events to your application in real-time. This matters because:
- Asynchronous payments: Bank transfers, SEPA don't complete instantly
- Subscription lifecycle: Know when subscriptions renew, cancel, or fail
- Compliance: Capture every payment event for accurate records
- User experience: Real-time UI updates on payment status
Missing a webhook means lost revenue, confused customers, or compliance issues.
Setting Up Your Stripe Webhook Endpoint
Step 1: Create Your Endpoint
Here's a basic HTTP endpoint in Node.js and Python that Stripe can POST events to.
Node.js with Express:
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();
// Important: Use raw body for signature verification
app.post('/webhooks/stripe',
express.raw({ type: 'application/json' }),
async (req, res) => {
const sig = req.headers['stripe-signature'];
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
} catch (err) {
console.error(`Signature verification failed: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event
await handleStripeEvent(event);
res.json({ received: true });
}
);
app.listen(3000, () => console.log('Webhook server running on port 3000'));Python with Flask:
import os
import stripe
from flask import Flask, request, jsonify
app = Flask(__name__)
stripe.api_key = os.environ.get('STRIPE_SECRET_KEY')
endpoint_secret = os.environ.get('STRIPE_WEBHOOK_SECRET')
@app.route('/webhooks/stripe', methods=['POST'])
def stripe_webhook():
payload = request.get_data()
sig_header = request.headers.get('Stripe-Signature')
try:
event = stripe.Webhook.construct_event(
payload, sig_header, endpoint_secret
)
except ValueError as e:
# Invalid payload
return jsonify({'error': 'Invalid payload'}), 400
except stripe.error.SignatureVerificationError as e:
# Invalid signature
return jsonify({'error': 'Invalid signature'}), 400
# Handle the event
handle_stripe_event(event)
return jsonify({'received': True}), 200
if __name__ == '__main__':
app.run(port=3000)Step 2: Register Your Endpoint with Stripe
In your Stripe Dashboard, navigate to Developers > Webhooks and click Add endpoint. Enter your endpoint URL and select the events you want to receive.
For most applications, these event types are essential:
| Event Type | When It Fires |
|---|---|
payment_intent.succeeded | Payment completed successfully |
payment_intent.payment_failed | Payment attempt failed |
customer.subscription.created | New subscription started |
customer.subscription.updated | Subscription plan or status changed |
customer.subscription.deleted | Subscription cancelled |
invoice.paid | Invoice payment succeeded |
invoice.payment_failed | Invoice payment failed |
charge.refunded | Refund processed |
charge.dispute.created | Customer disputed a charge |
After saving, Stripe will generate a signing secret (starts with whsec_). Store this securely—you'll need it for signature verification.
Signature Verification: Your First Line of Defense
Never process a webhook without verifying its signature. This ensures the request came from Stripe and wasn't tampered with. See our HMAC-SHA256 webhook signature verification guide for cryptographic details.
Stripe signs every webhook with a timestamp and your endpoint's secret:
- Stripe combines timestamp and payload into a signed string
- It generates an HMAC-SHA256 signature using your webhook secret
- Your server recreates and compares the signature
The code examples above handle this automatically via Stripe's SDK. Never skip this—attackers easily forge payloads without signature verification.
Handling Events: A Production-Ready Approach
Node.js:
async function handleStripeEvent(event) {
const { type, data } = event;
const object = data.object;
switch (type) {
case 'payment_intent.succeeded':
await handleSuccessfulPayment(object);
break;
case 'payment_intent.payment_failed':
await handleFailedPayment(object);
break;
case 'customer.subscription.created':
case 'customer.subscription.updated':
await syncSubscription(object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCancelled(object);
break;
case 'invoice.paid':
await recordInvoicePayment(object);
break;
case 'invoice.payment_failed':
await handleFailedInvoice(object);
break;
case 'charge.dispute.created':
await alertDisputeTeam(object);
break;
default:
console.log(`Unhandled event type: ${type}`);
}
}
async function handleSuccessfulPayment(paymentIntent) {
const { id, amount, currency, customer, metadata } = paymentIntent;
// Check for duplicate processing
const existing = await db.payments.findByStripeId(id);
if (existing) {
console.log(`Payment ${id} already processed, skipping`);
return;
}
// Record the payment
await db.payments.create({
stripeId: id,
customerId: customer,
amount: amount,
currency: currency,
metadata: metadata,
processedAt: new Date()
});
// Trigger downstream actions
await fulfillOrder(metadata.orderId);
await sendReceiptEmail(customer, paymentIntent);
}Python:
def handle_stripe_event(event):
event_type = event['type']
data_object = event['data']['object']
handlers = {
'payment_intent.succeeded': handle_successful_payment,
'payment_intent.payment_failed': handle_failed_payment,
'customer.subscription.created': sync_subscription,
'customer.subscription.updated': sync_subscription,
'customer.subscription.deleted': handle_subscription_cancelled,
'invoice.paid': record_invoice_payment,
'invoice.payment_failed': handle_failed_invoice,
'charge.dispute.created': alert_dispute_team,
}
handler = handlers.get(event_type)
if handler:
handler(data_object)
else:
print(f'Unhandled event type: {event_type}')
def handle_successful_payment(payment_intent):
payment_id = payment_intent['id']
# Idempotency check
existing = db.payments.find_by_stripe_id(payment_id)
if existing:
print(f'Payment {payment_id} already processed, skipping')
return
# Record and fulfill
db.payments.create(
stripe_id=payment_id,
customer_id=payment_intent['customer'],
amount=payment_intent['amount'],
currency=payment_intent['currency'],
processed_at=datetime.utcnow()
)
fulfill_order(payment_intent['metadata'].get('order_id'))Common Challenges and Solutions
Challenge 1: Duplicate Events
Stripe may send the same event multiple times. Network issues, retries, and edge cases all contribute to duplicates. Your code must be idempotent—processing the same event twice should produce the same result. Our comprehensive webhook idempotency guide covers patterns for handling this correctly in payment systems.
Solution: Track processed event IDs or use natural idempotency keys:
async function processEventIdempotently(event) {
const lockKey = `stripe_event:${event.id}`;
// Try to acquire a lock
const acquired = await redis.set(lockKey, '1', 'NX', 'EX', 3600);
if (!acquired) {
console.log(`Event ${event.id} already being processed`);
return;
}
try {
await handleStripeEvent(event);
} finally {
// Keep the key to prevent reprocessing
}
}Challenge 2: Event Ordering
Events can arrive out of order. A subscription.updated event might arrive before subscription.created. Don't assume sequential delivery.
Solution: Use timestamps and fetch current state when needed:
async function syncSubscription(subscription) {
const existing = await db.subscriptions.findByStripeId(subscription.id);
// Only update if this event is newer
if (existing && existing.stripeUpdatedAt >= subscription.created) {
console.log('Received outdated subscription event, skipping');
return;
}
// For critical operations, fetch the latest state from Stripe
const current = await stripe.subscriptions.retrieve(subscription.id);
await db.subscriptions.upsert(current);
}Challenge 3: Slow Processing
Stripe expects a response within 20 seconds. Long-running tasks will cause timeouts and retries. Understanding webhook retry strategies helps you design systems that handle these retries gracefully.
Solution: Acknowledge immediately, process asynchronously:
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
// Verify signature first
const event = verifyAndParseEvent(req);
// Queue for async processing
eventQueue.add('stripe-webhook', { event });
// Respond immediately
res.json({ received: true });
});Testing with Stripe CLI
Install Stripe CLI and test locally:
# Login to your Stripe account
stripe login
# Forward webhooks to your local server
stripe listen --forward-to localhost:3000/webhooks/stripe
# In another terminal, trigger test events
stripe trigger payment_intent.succeeded
stripe trigger customer.subscription.createdThe CLI displays your webhook secret for local testing. Use this temporary secret in your development environment.
Best Practices Checklist
- Verifies signatures on every request
- Responds quickly (under 5 seconds, ideally under 1 second)
- Handles duplicates with idempotency checks
- Processes asynchronously for long-running tasks
- Logs everything for debugging and auditing
- Monitors failures with alerts for repeated errors
- Handles unknown events gracefully (don't crash on new event types)
Adding a Reliability Layer with Hook Mesh
Even with perfect code, production webhook delivery faces challenges: server downtime, deployment windows, traffic spikes, and network issues. A single missed webhook can cascade into bigger problems.
Hook Mesh acts as a reliability layer between Stripe and your application. Instead of pointing Stripe directly at your server, you route webhooks through Hook Mesh, which provides:
- Automatic retries with exponential backoff when your server is unavailable
- Event buffering during deployments or outages
- Delivery guarantees so you never miss critical payment events
- Real-time monitoring with alerts when deliveries fail
- Debug tools to inspect and replay any webhook
Setting it up takes minutes: create a Hook Mesh endpoint, update your Stripe webhook URL, and add your destination. Your existing webhook code stays exactly the same—Hook Mesh just ensures every event reaches it.
For startups and SMBs processing real payments, the cost of missed webhooks (failed fulfillment, angry customers, revenue leakage) far outweighs the cost of a reliability layer. Hook Mesh handles the infrastructure complexity so you can focus on your product.
Conclusion
Stripe webhooks require careful implementation: verify signatures, handle duplicates, respond quickly, and build for failure.
Test thoroughly with the Stripe CLI and consider Hook Mesh as your reliability layer for production. For handling multiple payment providers, check out our PayPal webhooks guide. For a complete overview of payment integrations, visit our platform integrations hub.
Related Posts
PayPal Webhooks: IPN vs Webhooks, Implementation Guide
Complete guide to PayPal webhooks implementation. Learn the differences between IPN and modern webhooks, set up event listeners, verify signatures, and migrate from legacy IPN to webhooks for reliable payment notifications.
HMAC-SHA256 Webhook Signatures: Implementation Guide
Learn how to implement secure webhook signature verification using HMAC-SHA256. Complete guide with code examples for signing and verifying webhook payloads in Node.js and Python.
Webhook Idempotency: Why It Matters and How to Implement It
A comprehensive technical guide to implementing idempotency for webhooks. Learn about idempotency keys, deduplication strategies, and implementation patterns with Node.js and Python code examples.
Webhook Retry Strategies: Linear vs Exponential Backoff
A technical deep-dive into webhook retry strategies, comparing linear and exponential backoff approaches, with code examples and best practices for building reliable webhook delivery systems.
Webhook Integration Guides: Connect with Any Platform
Comprehensive guides for integrating webhooks from Stripe, GitHub, Shopify, Twilio, and 10+ other platforms. Learn best practices for reliable webhook handling across your entire stack.