Why Use Stripe Webhooks?
Stripe webhooks notify your application about payment events in real-time. Instead of polling the Stripe API, webhooks push updates to your server when:
✓ Payment succeeds or fails
Immediately update order status and send confirmations
✓ Subscription changes
Handle renewals, cancellations, and upgrades
✓ Disputes and refunds
Respond to chargebacks and process refunds
✓ Customer updates
Sync customer data and payment methods
Prerequisites
Step 1: Create a Webhook Endpoint
First, create an endpoint that accepts POST requests. Here's an example using Express.js:
Node.js / Express.js
const express = require('express');
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 payload = req.body;
try {
// We'll add verification in the next step
console.log('Received webhook:', payload);
res.sendStatus(200);
} catch (err) {
console.error('Webhook error:', err.message);
res.sendStatus(400);
}
}
);
app.listen(3000, () => console.log('Server running on port 3000'));Critical: Use Raw Body
You must use express.raw() instead of express.json() for the webhook endpoint. Stripe's signature verification requires the raw request body.
Step 2: Verify Webhook Signatures
Always verify webhook signatures to ensure requests come from Stripe. Install the Stripe SDK:
npm install stripeSignature Verification
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
app.post('/webhooks/stripe',
express.raw({type: 'application/json'}),
async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
// Verify the webhook signature
event = stripe.webhooks.constructEvent(
req.body,
sig,
endpointSecret
);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.sendStatus(400);
}
// Signature verified - process the event
console.log('Verified event:', event.type);
res.sendStatus(200);
}
);Getting Your Webhook Secret
- 1. Go to Stripe Dashboard → Webhooks
- 2. Click "Add endpoint"
- 3. Enter your endpoint URL (e.g.,
https://yourapp.com/webhooks/stripe) - 4. Select events to listen for
- 5. Copy the "Signing secret" (starts with
whsec_) - 6. Add to your environment variables as
STRIPE_WEBHOOK_SECRET
Step 3: Handle Webhook Events
Process different event types based on your business logic:
Event Handling Example
app.post('/webhooks/stripe',
express.raw({type: 'application/json'}),
async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
} catch (err) {
return res.sendStatus(400);
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
await handlePaymentSuccess(paymentIntent);
break;
case 'payment_intent.payment_failed':
const failedPayment = event.data.object;
await handlePaymentFailure(failedPayment);
break;
case 'customer.subscription.created':
const subscription = event.data.object;
await handleNewSubscription(subscription);
break;
case 'customer.subscription.deleted':
const canceledSub = event.data.object;
await handleCanceledSubscription(canceledSub);
break;
case 'invoice.payment_succeeded':
const invoice = event.data.object;
await handleInvoicePayment(invoice);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.sendStatus(200);
}
);
async function handlePaymentSuccess(paymentIntent) {
console.log('Payment succeeded:', paymentIntent.id);
// Update order status in database
await db.orders.update({
stripePaymentIntentId: paymentIntent.id
}, {
status: 'paid',
paidAt: new Date()
});
// Send confirmation email
await sendOrderConfirmation(paymentIntent.metadata.orderId);
}Common Stripe Webhook Events
payment_intent.succeededPayment completedpayment_intent.payment_failedPayment failedcustomer.subscription.createdNew subscriptioncustomer.subscription.updatedSubscription changedcustomer.subscription.deletedSubscription canceledinvoice.payment_succeededInvoice paidView all events: Stripe Event Types
Step 4: Test Webhooks Locally
You have two options for testing Stripe webhooks during development:
Option 1: Stripe CLI
Install Stripe CLI:
brew install stripe/stripe-cli/stripeForward webhooks to localhost:
stripe listen --forward-to localhost:3000/webhooks/stripeTrigger test events:
stripe trigger payment_intent.succeededOption 2: hookVM
Create a hookVM endpoint and use the CLI:
hookvm flow listen --endpoint-id ep_xxx --port 3000Benefits:
- • Inspect webhook payloads in dashboard
- • Replay webhooks for debugging
- • Works with any webhook provider
Step 5: Deploy to Production
1. Deploy Your Endpoint
Deploy your webhook endpoint to a server with HTTPS enabled. Stripe requires HTTPS for production webhooks.
Example URL: https://api.yourapp.com/webhooks/stripe
2. Register in Stripe Dashboard
- • Go to Stripe Dashboard → Webhooks
- • Click "Add endpoint"
- • Enter your production URL
- • Select events to listen for (or select "all events" for testing)
- • Copy the webhook signing secret
- • Add secret to production environment variables
3. Monitor Webhook Deliveries
Monitor webhook deliveries in the Stripe Dashboard. Check for failed deliveries and review error messages. Stripe automatically retries failed webhooks with exponential backoff.
Best Practices
Return 200 quickly
Acknowledge receipt immediately, process asynchronously to avoid timeouts
Make handlers idempotent
Stripe may send the same webhook multiple times - handle duplicates safely
Use metadata for context
Add order IDs or user IDs to Stripe objects to link events to your data
Log all webhook events
Keep audit logs for debugging and compliance
Simplify Stripe Webhook Management
hookVM handles signature verification, retry logic, and provides a dashboard to inspect and replay Stripe webhooks. Focus on your business logic, not infrastructure.