Payments45-60 min
Stripe Webhooks Implementation
This guide explains raw body handling, signature verification, idempotency, event routing, local Stripe CLI testing, and production webhook monitoring.
StripeNode.jsExpress
Prerequisites
- A Stripe account with test mode access.
- An Express API or serverless endpoint.
- A database table for payments, subscriptions, or processed webhook event IDs.
- Stripe CLI installed for local testing.
1
Install Stripe
Implementation snippet
npm install stripe2
Use raw body for the webhook route
Stripe signature verification needs the exact raw request body. Apply JSON parsing after the webhook route or exclude this route from JSON parsing.
Implementation snippet
app.post("/api/stripe/webhook", express.raw({ type: "application/json" }), stripeWebhookHandler);
app.use(express.json());Warning: If `express.json()` runs before the webhook route, signature verification can fail even when the secret is correct.
3
Verify the signature
Implementation snippet
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
async function stripeWebhookHandler(req, res) {
const signature = req.headers["stripe-signature"];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!,
);
} catch (error) {
return res.status(400).send(`Webhook signature failed: ${error.message}`);
}
await handleStripeEvent(event);
res.json({ received: true });
}4
Route important events
- Use `checkout.session.completed` to activate purchases or start onboarding.
- Use `customer.subscription.updated` to sync plan, status, trial, and renewal date.
- Use `customer.subscription.deleted` to remove paid access.
- Use `invoice.payment_failed` to mark accounts as past_due and notify users.
Implementation snippet
async function handleStripeEvent(event: Stripe.Event) {
switch (event.type) {
case "checkout.session.completed":
return syncCheckoutSession(event.data.object as Stripe.Checkout.Session);
case "customer.subscription.updated":
case "customer.subscription.deleted":
return syncSubscription(event.data.object as Stripe.Subscription);
case "invoice.payment_failed":
return markPaymentFailed(event.data.object as Stripe.Invoice);
default:
return;
}
}5
Make webhook handling idempotent
- Create a `stripe_webhook_events` table with a unique `event_id` column.
- Insert the event ID before processing business changes.
- If the insert fails because the event already exists, return success without reprocessing.
- Wrap event recording and business updates in a transaction when possible.
Note: Stripe may retry events. Idempotency prevents duplicate invoices, duplicate emails, and repeated entitlement changes.
6
Test locally with Stripe CLI
Implementation snippet
stripe login
stripe listen --forward-to localhost:3000/api/stripe/webhook
stripe trigger checkout.session.completed7
Production checklist
Checklist
- Create a live webhook endpoint in Stripe Dashboard.
- Store the live webhook signing secret separately from the test secret.
- Monitor failed webhook deliveries for the first release week.
- Log event ID, event type, customer ID, and processing result.
- Return a 2xx response only after the event is safely processed or safely ignored.