Stripe Webhooks Setup for SaaS in 2026

Production-ready webhook patterns for Stripe events. Error handling, idempotency, and security best practices for indie SaaS.

TL;DR

Stripe webhooks let your SaaS react to payment events in real-time. Set up a HTTPS endpoint, verify signatures using your webhook secret, implement idempotent handlers, and test with the Stripe CLI. Always validate webhook signatures—this is non-negotiable.

---

Why Webhooks Matter for SaaS

Polling Stripe's API every 30 seconds to check payment status doesn't scale. Webhooks push events to your server instantly when customers upgrade, downgrade, or churn. For indie SaaS, reliable webhook handling directly impacts:

  • Subscription state accuracy - Your DB reflects reality when Stripe's does
  • Revenue recognition - No double-charging or missed payments
  • Customer experience - Instant feature access after payment
  • Getting this wrong means silent failures, angry customers, and revenue gaps.

    ---

    Step 1: Create a Webhook Endpoint

    Your webhook endpoint must: 1. Accept POST requests over HTTPS (never HTTP) 2. Return 200 OK within 300 seconds 3. Verify the Stripe signature 4. Process idempotently

    Here's a Node.js example using Express.js (v4.18+) and stripe package (v14.8.0+—verify in official docs):

    ```javascript const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

    const app = express(); const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;

    // Raw body for signature verification 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, webhookSecret ); } catch (err) { console.error(⚠️ Webhook signature verification failed: ${err.message}); return res.status(400).send(Webhook Error: ${err.message}); }

    // Handle event type switch (event.type) { case 'customer.subscription.updated': await handleSubscriptionUpdate(event.data.object); break; case 'invoice.payment_succeeded': await handlePaymentSuccess(event.data.object); break; case 'customer.subscription.deleted': await handleSubscriptionCanceled(event.data.object); break; default: console.log(Unhandled event type: ${event.type}); }

    res.json({received: true}); });

    app.listen(3000, () => console.log('Webhook server running')); ```

    Critical: Use express.raw() for the webhook route. Using express.json() globally will break signature verification because the signature is computed on the raw request body.

    ---

    Step 2: Implement Idempotent Handlers

    Webhooks can fire multiple times (network issues, Stripe retries). Your handlers must be idempotent—processing the same event twice produces the same result.

    Pattern: Store event IDs in your database:

    ```javascript const handlePaymentSuccess = async (invoice) => { const eventId = invoice.id; // Use Stripe object ID as idempotency key // Check if already processed const existing = await db.query( 'SELECT id FROM processed_events WHERE stripe_id = $1', [eventId] ); if (existing.rows.length > 0) { console.log(Event ${eventId} already processed, skipping); return; // Idempotent exit }

    // Process payment const customerId = invoice.customer; await db.query( 'UPDATE subscriptions SET status = $1, processed_at = NOW() WHERE customer_id = $2', ['active', customerId] );

    // Mark event as processed await db.query( 'INSERT INTO processed_events (stripe_id, type) VALUES ($1, $2)', [eventId, 'invoice.payment_succeeded'] ); }; ```

    ---

    Step 3: Register Webhook in Stripe Dashboard

    1. Log into [Stripe Dashboard](https://dashboard.stripe.com) 2. Navigate: DevelopersWebhooks 3. Click Add endpoint 4. Enter your endpoint URL: https://yourdomain.com/webhooks/stripe 5. Select events: - customer.subscription.created - customer.subscription.updated - customer.subscription.deleted - invoice.payment_succeeded - invoice.payment_failed 6. Copy the Signing Secret and set as STRIPE_WEBHOOK_SECRET environment variable

    For testing: You'll get a separate webhook secret for test mode vs. live mode.

    ---

    Step 4: Test with Stripe CLI

    The Stripe CLI (v1.19.1+—verify in official docs) lets you test webhooks locally:

    ```bash

    Install Stripe CLI

    brew install stripe/stripe-cli/stripe # macOS

    or download from https://github.com/stripe/stripe-cli/releases

    Login

    stripe login

    Forward webhooks to localhost

    stripe listen --forward-to localhost:3000/webhooks/stripe

    In another terminal, trigger test events

    stripe trigger customer.subscription.updated ```

    ---

    Common Errors You'll See

    Error 1: Invalid Signature

    ``` WebhookSignatureVerificationError: No signatures found matching the expected signature for payload. ```

    Cause: Wrong webhook secret or raw body not passed to stripe.webhooks.constructEvent(). Fix: Ensure express.raw() is used only on the webhook route, not globally.

    Error 2: Timeout (300s exceeded)

    ``` {"error": {"message": "Webhook endpoint did not return a 2xx or 3xx status code; we will retry sending this webhook."}} ```

    Cause: Database query or external API call took too long. Fix: Queue heavy work (email notifications, analytics) to a background job processor like Bull or pg-boss. Return 200 immediately.

    Error 3: Duplicate Processing

    ``` Duplicate key value violates unique constraint "subscriptions_customer_id_key" ```

    Cause: Webhook fired twice, handler processed both. Fix: Implement idempotency check as shown in Step 2.

    ---

    Production Checklist

  • [ ] Webhook endpoint returns 200 within 300 seconds
  • [ ] Signature verification enabled and tested
  • [ ] Idempotency keys stored in database
  • [ ] Heavy operations queued to background jobs
  • [ ] Error logs sent to Sentry/monitoring service
  • [ ] Both test and live webhook secrets in environment
  • [ ] Rate limiting on webhook endpoint (optional but recommended)
  • [ ] Monitoring: alert on failed webhook deliveries
  • See [Stripe's official webhook guide](https://stripe.com/docs/webhooks) for the complete reference.

    For deeper subscription logic, check [SaaS billing fundamentals](/?guide=saas-billing) and [Handling failed payments](/?guide=failed-payments).

    ---

    What am I missing?

    This guide covers the happy path. Leave a comment if you've encountered:

  • Webhook-specific edge cases in production
  • Better idempotency patterns than event ID tracking
  • Monitoring/alerting strategies you recommend
  • Version updates or API changes from official Stripe docs
  • Build reliable, your users depend on it.

    🔥 0d
    LIVE
    PlanetScale rage spiking Vercel pricing complaints Railway gaining fast Supabase happiness rising Resend loved by devs PlanetScale rage spiking Vercel pricing complaints Railway gaining fast Supabase happiness rising
    DEVELOPER PAIN RADAR // Loading...

    Developers complain.
    Opportunities appear.

    We track what developers are struggling with today — and what opportunities that creates.

    guides today
    avg happiness
    🔥 Pain
    📖 Guides
    🔭 Explore
    👤 Mine
    🔥 Pain Radar — rage scores today
    ↗ share
    💡 Opportunity Feed — pain = market gap
    📈 Tool Momentum
    all scores →
    📖 Latest Guide
    all guides →
    📖 All Guides
    📊 Tool Scores
    + Submit
    📰 Hacker News
    ➕ Submit a Tool
    ← back