Stripe webhooks are how your SaaS knows when payments succeed, subscriptions change, and customers cancel. Get this wrong and your app will charge people who shouldn't be charged, or give access to people who haven't paid. Here's how to implement webhooks correctly.
What Are Webhooks?
A webhook is an HTTP POST request that Stripe sends to your server when something happens — a payment succeeds, a subscription is cancelled, a charge fails. Your server receives the event and updates your database accordingly.
Always Verify Webhook Signatures
Anyone can send a fake POST request to your webhook endpoint. Stripe signs every webhook with a secret key. Your server must verify this signature before processing the event. In code:
Use stripe.webhooks.constructEvent(payload, signature, webhookSecret). If this throws, reject the request. This prevents attackers from spoofing payment success events to grant themselves free access.
Idempotency Is Critical
Stripe may send the same event multiple times (network failures, retries). Your webhook handler must be idempotent — processing the same event twice should have the same result as processing it once.
Implementation: store processed Stripe event IDs in your database. When a webhook arrives, check if that event ID has already been processed. If yes, return 200 without doing anything. If no, process and store the ID.
The Events You Must Handle
checkout.session.completed— activate the user's subscriptioncustomer.subscription.created— log new subscription startcustomer.subscription.updated— handle plan upgrades, downgrades, renewalscustomer.subscription.deleted— revoke access, send cancellation confirmationinvoice.payment_succeeded— extend subscription access, send receiptinvoice.payment_failed— send payment failure email, begin dunning process
Need Webhooks Implemented Correctly?
I take 2 clients per month. Ship your SaaS in 2–4 weeks with a developer who has done it 350+ times.
Start on Fiverr →Testing Webhooks Locally
Use the Stripe CLI to forward events to your local development server: stripe listen --forward-to localhost:3000/api/webhooks/stripe. Then trigger test events with stripe trigger payment_intent.succeeded.
Making Webhooks Reliable in Production
Webhook handlers must be idempotent — if Stripe delivers the same event twice (which happens more often than you would expect), your handler must produce the same result both times without duplicating side effects. Store processed webhook IDs in your database and check for duplicates before acting on any event. Use a job queue (like BullMQ or Inngest) for any webhook processing that involves sending emails or making external API calls — this decouples webhook receipt from processing and survives application restarts without data loss.