Stripe webhooks deliver real-time event notifications to your server when things happen in your Stripe account — payments succeed, subscriptions renew, disputes are created. This guide covers setting up a webhook endpoint, verifying signatures with stripe.webhooks.constructEvent and raw body parsing, handling key events, and implementing retry logic for reliable event processing.
Complete Guide to Stripe Webhook Integration
Webhooks are the backbone of reliable Stripe integrations. Instead of polling the API, Stripe pushes event notifications to your server in real time. Critical flows like order fulfillment, subscription management, and fraud alerts depend on webhooks. The most important part is signature verification — every webhook request includes a Stripe-Signature header that you must validate using your webhook endpoint secret and the raw request body.
Prerequisites
- Stripe account (test mode)
- Node.js 18 or later installed
- Stripe Node.js SDK: npm install stripe
- Express.js: npm install express
- A publicly accessible URL (or Stripe CLI for local testing)
Step-by-step guide
Create the webhook endpoint with signature verification
Create the webhook endpoint with signature verification
The critical detail: you must pass the raw request body (not parsed JSON) to constructEvent. Use express.raw() middleware on the webhook route.
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);2const express = require('express');3const app = express();45// CRITICAL: webhook route must use express.raw(), not express.json()6app.post('/webhook',7 express.raw({ type: 'application/json' }),8 (req, res) => {9 const sig = req.headers['stripe-signature'];10 let event;1112 try {13 event = stripe.webhooks.constructEvent(14 req.body, // Raw body buffer15 sig, // Stripe-Signature header16 process.env.STRIPE_WEBHOOK_SECRET // whsec_ from Dashboard17 );18 } catch (err) {19 console.error('Webhook signature verification failed:', err.message);20 return res.status(400).send(`Webhook Error: ${err.message}`);21 }2223 console.log('Verified event:', event.type, event.id);24 res.json({ received: true });25 }26);2728// Other routes can use express.json() normally29app.use(express.json());3031app.listen(3000, () => console.log('Webhook server on port 3000'));Expected result: Webhook requests are verified against Stripe's signature. Invalid or spoofed requests are rejected with a 400 status.
Handle key Stripe events
Handle key Stripe events
Route different event types to appropriate handlers. Focus on the events that matter for your business logic.
1function handleEvent(event) {2 switch (event.type) {3 // Payment events4 case 'payment_intent.succeeded': {5 const pi = event.data.object;6 console.log(`Payment ${pi.id}: $${pi.amount / 100} ${pi.currency}`);7 // Fulfill the order8 break;9 }10 case 'payment_intent.payment_failed': {11 const pi = event.data.object;12 console.log(`Payment failed: ${pi.id}`, pi.last_payment_error?.message);13 // Notify the customer14 break;15 }1617 // Subscription events18 case 'customer.subscription.created':19 console.log('New subscription:', event.data.object.id);20 break;21 case 'customer.subscription.deleted':22 console.log('Subscription canceled:', event.data.object.id);23 // Revoke access24 break;25 case 'invoice.payment_failed':26 console.log('Invoice payment failed:', event.data.object.id);27 // Send dunning email28 break;2930 // Dispute events31 case 'charge.dispute.created':32 console.log('Dispute opened:', event.data.object.id);33 // Alert your team34 break;3536 // Connect events37 case 'account.updated': {38 const acct = event.data.object;39 console.log(`Account ${acct.id}: charges=${acct.charges_enabled}`);40 break;41 }4243 default:44 console.log(`Unhandled event type: ${event.type}`);45 }46}Expected result: Each event type is routed to the appropriate handler for business logic processing.
Implement idempotent event processing
Implement idempotent event processing
Stripe may send the same event multiple times (retries). Use the event ID to ensure you process each event only once.
1// Simple in-memory set for deduplication (use Redis or DB in production)2const processedEvents = new Set();34function processEventIdempotently(event) {5 // Check if we have already processed this event6 if (processedEvents.has(event.id)) {7 console.log(`Event ${event.id} already processed, skipping`);8 return;9 }1011 // Process the event12 handleEvent(event);1314 // Mark as processed15 processedEvents.add(event.id);1617 // Clean up old events (keep last 10,000)18 if (processedEvents.size > 10000) {19 const iterator = processedEvents.values();20 processedEvents.delete(iterator.next().value);21 }22}2324// In production, use a database:25// INSERT INTO processed_events (event_id, processed_at)26// VALUES ($1, NOW())27// ON CONFLICT (event_id) DO NOTHING28// RETURNING event_idExpected result: Duplicate webhook events are detected and skipped, preventing double-processing.
Register the webhook endpoint in Stripe Dashboard
Register the webhook endpoint in Stripe Dashboard
Go to Dashboard → Developers → Webhooks → Add endpoint. Enter your URL and select which events to receive. Copy the signing secret (whsec_) to your environment variables.
1// After registering in the Dashboard, you receive a signing secret:2// whsec_abc123...3//4// Store it as an environment variable:5// STRIPE_WEBHOOK_SECRET=whsec_abc123...6//7// Common events to subscribe to:8// - payment_intent.succeeded9// - payment_intent.payment_failed10// - customer.subscription.created11// - customer.subscription.updated12// - customer.subscription.deleted13// - invoice.payment_failed14// - invoice.paid15// - charge.dispute.created16// - charge.refunded17// - account.updated (for Connect)18// - checkout.session.completed1920console.log('Register your endpoint at:');21console.log('Dashboard → Developers → Webhooks → Add endpoint');22console.log('URL: https://yoursite.com/webhook');23console.log('Copy the signing secret to STRIPE_WEBHOOK_SECRET env var');Expected result: Webhook endpoint is registered in Stripe and will receive events for the selected types.
Handle errors and respond quickly
Handle errors and respond quickly
Stripe expects a 2xx response within 20 seconds. If your handler takes longer, return 200 immediately and process the event asynchronously.
1app.post('/webhook',2 express.raw({ type: 'application/json' }),3 async (req, res) => {4 const sig = req.headers['stripe-signature'];5 let event;67 try {8 event = stripe.webhooks.constructEvent(9 req.body, sig, process.env.STRIPE_WEBHOOK_SECRET10 );11 } catch (err) {12 return res.status(400).send(`Webhook Error: ${err.message}`);13 }1415 // Respond immediately — Stripe retries if no 2xx within 20 seconds16 res.json({ received: true });1718 // Process the event asynchronously19 try {20 await processEventAsync(event);21 } catch (err) {22 console.error(`Error processing ${event.type}:`, err.message);23 // Log to error tracking (Sentry, etc.)24 // The event will be retried by Stripe if needed25 }26 }27);2829async function processEventAsync(event) {30 // Your business logic here31 // Can take as long as needed since we already responded 20032 handleEvent(event);33}Expected result: Webhook responds immediately with 200, then processes the event asynchronously without risking Stripe timeouts.
Complete working example
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);2const express = require('express');3const app = express();45const processedEvents = new Set();67// Webhook endpoint — MUST use raw body8app.post('/webhook',9 express.raw({ type: 'application/json' }),10 (req, res) => {11 const sig = req.headers['stripe-signature'];12 let event;13 try {14 event = stripe.webhooks.constructEvent(15 req.body, sig, process.env.STRIPE_WEBHOOK_SECRET16 );17 } catch (err) {18 console.error('Signature verification failed:', err.message);19 return res.status(400).send(`Webhook Error: ${err.message}`);20 }2122 // Respond immediately23 res.json({ received: true });2425 // Idempotency check26 if (processedEvents.has(event.id)) return;27 processedEvents.add(event.id);2829 // Route events30 switch (event.type) {31 case 'payment_intent.succeeded': {32 const pi = event.data.object;33 console.log(`Payment succeeded: ${pi.id} ($${pi.amount / 100})`);34 break;35 }36 case 'payment_intent.payment_failed': {37 const pi = event.data.object;38 console.log(`Payment failed: ${pi.id}`);39 break;40 }41 case 'customer.subscription.deleted': {42 console.log('Sub canceled:', event.data.object.id);43 break;44 }45 case 'invoice.payment_failed': {46 console.log('Invoice failed:', event.data.object.id);47 break;48 }49 case 'charge.dispute.created': {50 console.log('Dispute:', event.data.object.id);51 break;52 }53 case 'account.updated': {54 const acct = event.data.object;55 console.log(`Account ${acct.id} updated`);56 break;57 }58 default:59 console.log(`Unhandled: ${event.type}`);60 }61 }62);6364// Non-webhook routes use JSON parsing65app.use(express.json());6667app.get('/health', (req, res) => res.json({ status: 'ok' }));6869app.listen(3000, () => console.log('Webhook server on port 3000'));Common mistakes when handling webhooks in Stripe
Why it's a problem: Using express.json() on the webhook route which parses the raw body
How to avoid: Use express.raw({ type: 'application/json' }) for the webhook route. Place it BEFORE any global express.json() middleware.
Why it's a problem: Not verifying the webhook signature
How to avoid: Always call stripe.webhooks.constructEvent with the raw body, signature header, and webhook secret. Never skip verification.
Why it's a problem: Taking too long to respond, causing Stripe to retry
How to avoid: Return res.json({ received: true }) immediately, then process the event asynchronously.
Why it's a problem: Not handling duplicate events (retries)
How to avoid: Store processed event IDs and skip duplicates. Use a database with UNIQUE constraint on event_id in production.
Best practices
- Always verify webhook signatures with stripe.webhooks.constructEvent and raw body
- Respond with 200 within 20 seconds — process events asynchronously if needed
- Implement idempotent event processing to handle retries safely
- Subscribe only to the events you need to reduce noise and processing load
- Log all received events for debugging and auditing
- Use the Stripe CLI (stripe listen) for local development testing
- Set up monitoring alerts if your webhook endpoint starts returning errors
- For production webhook infrastructure, RapidDev can help build reliable event processing pipelines
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
How do I set up Stripe webhooks in Node.js? I need signature verification with constructEvent, handling for payment and subscription events, and idempotent processing. Show me the complete Express.js setup.
Build a production-ready Stripe webhook handler for my Node.js Express app. I need signature verification with raw body parsing, handlers for payment_intent.succeeded, subscription events, and dispute events, plus idempotent processing with event deduplication.
Frequently asked questions
What happens if my webhook endpoint is down?
Stripe retries failed webhook deliveries over 3 days with exponential backoff. After all retries are exhausted, the event is marked as failed. You can view missed events in Dashboard → Developers → Webhooks.
Can I receive webhooks for events that happened before I registered the endpoint?
No. Webhooks only deliver events that occur after the endpoint is registered. For historical events, use the Events API: stripe.events.list().
Why does signature verification fail?
The most common cause is parsing the request body as JSON before passing it to constructEvent. The raw body bytes must match exactly. Use express.raw() on the webhook route.
How many events can Stripe send per second?
Stripe can send hundreds of events per second during high traffic. Ensure your endpoint can handle the load. Use a message queue (SQS, Redis) for heavy processing.
Can I have multiple webhook endpoints?
Yes. You can create multiple endpoints, each subscribed to different events. This is useful for routing payment events to one service and subscription events to another.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation