Stripe Webhooks Setup for Indie SaaS in 2026

Production-ready webhook implementation for Stripe. Handle payments, subscriptions, and refunds reliably with error handling patterns.

TL;DR

Set up Stripe webhooks by creating an endpoint that validates signatures, idempotently processes events, and retries gracefully. Use Stripe CLI v1.19+ (verify in official docs) for local testing. Always verify webhook signatures before processing—this prevents forged requests.

---

Why Webhooks Matter for Indie SaaS

Webhooks are how Stripe tells you what happened: payment succeeded, subscription renewed, refund issued. Without them, you'd poll the API constantly (wasteful and slow). Webhooks are event-driven, real-time, and essential for subscription billing.

Real problem: You sell a SaaS subscription, customer pays, but you never grant access because your webhook endpoint crashed silently.

---

Step 1: Create Your Webhook Endpoint

Your endpoint must: 1. Accept POST requests from Stripe 2. Verify the webhook signature (critical security step) 3. Process the event idempotently 4. Return 200 status immediately

Here's a Node.js/Express production-ready example:

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

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

// Important: Use raw body for signature verification app.post( '/api/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => { let event;

// Step 1: Verify the signature try { const sig = req.headers['stripe-signature']; 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}); }

// Step 2: Return 200 immediately to acknowledge receipt res.status(200).json({ received: true });

// Step 3: Process the event asynchronously try { await handleStripeEvent(event); } catch (err) { // Log for debugging; Stripe will retry this webhook console.error( Error processing event ${event.id}:, err.message ); } } );

async function handleStripeEvent(event) { switch (event.type) { case 'customer.subscription.created': await onSubscriptionCreated(event.data.object); break;

case 'customer.subscription.updated': await onSubscriptionUpdated(event.data.object); break;

case 'invoice.payment_succeeded': await onInvoicePaid(event.data.object); break;

case 'charge.refunded': await onChargeRefunded(event.data.object); break;

default: console.log(Unhandled event type: ${event.type}); } }

app.listen(3000, () => { console.log('Webhook server listening on port 3000'); }); ```

---

Step 2: Implement Idempotency (Stripe Retries Events)

Stripe retries failed webhooks. If your code crashes mid-processing, you might receive the same event twice. Always store the event.id to detect duplicates:

```javascript async function onSubscriptionCreated(subscription) { const eventId = subscription.metadata?.event_id; // Check if we've already processed this event const existing = await db.webhookEvents.findOne({ stripe_event_id: eventId }); if (existing) { console.log(Event ${eventId} already processed. Skipping.); return; }

// Process it for the first time await db.users.updateOne( { stripe_customer_id: subscription.customer }, { subscription_active: true, subscription_id: subscription.id } );

// Record that we handled this event await db.webhookEvents.insertOne({ stripe_event_id: eventId, processed_at: new Date(), event_type: 'subscription.created' }); } ```

---

Step 3: Register the Webhook in Stripe Dashboard

1. Go to [Stripe Dashboard → Developers → Webhooks](https://dashboard.stripe.com/webhooks) 2. Click "Add Endpoint" 3. Enter your endpoint URL: https://yourdomain.com/api/webhooks/stripe 4. Select events to listen for (start with: customer.subscription.created, invoice.payment_succeeded, charge.refunded) 5. Copy the Signing Secret → add to .env as STRIPE_WEBHOOK_SECRET

---

Step 4: Local Testing with Stripe CLI

Use [Stripe CLI v1.19+](https://stripe.com/docs/stripe-cli) (verify current version in official docs):

```bash

Install (macOS with Homebrew)

brew install stripe/stripe-cli/stripe

Login to your Stripe account

stripe login

Forward webhook events to your local endpoint

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

In another terminal, trigger a test event

stripe trigger payment_intent.succeeded ```

You'll see: ``` 2026-01-15 10:22:33 ▶ payment_intent.succeeded [evt_test_...] Sent to http://localhost:3000/api/webhooks/stripe Response: 200 OK ```

---

Common Console Errors You'll Hit

Error #1: Webhook signature verification failed ``` Error: No signatures found matching the expected signature for payload. ``` Cause: STRIPE_WEBHOOK_SECRET is wrong or you're using parsed body instead of raw. Fix: Use express.raw({ type: 'application/json' }) and verify your secret key.

Error #2: Port already in use ``` Error: listen EADDRINUSE: address already in use :::3000 ``` Cause: Another process owns port 3000. Fix: lsof -i :3000 then kill -9 <PID> or use a different port.

Error #3: Webhook endpoint returns non-200 status ``` Stripe → POST https://yourdomain.com/api/webhooks/stripe Response: 500 Internal Server Error Retrying in 5 minutes... ``` Cause: Unhandled exception in your webhook handler. Fix: Add try-catch blocks and return 200 even if processing fails. Log errors for async debugging.

---

Production Checklist

  • ✅ Verify webhook signatures (non-negotiable)
  • ✅ Return 200 status immediately before processing
  • ✅ Implement idempotency with event.id storage
  • ✅ Use environment variables for secrets
  • ✅ Add monitoring/alerting for webhook failures
  • ✅ Set up different endpoints for test vs. live mode
  • ✅ Log webhook processing with correlation IDs
  • ✅ Test locally with Stripe CLI before deploying
  • ---

    Related Guides

  • [Billing Automation: Getting Subscriptions Right](/?guide=stripe-subscriptions)
  • [Payment Processing: From First Transaction to Scale](/?guide=payment-processing)
  • ---

    What Am I Missing?

    This guide covers the foundation. But indie SaaS is messy:

  • How do you handle webhooks when your database is down? (Dead letter queues?)
  • Should you use Svix or another webhook service instead? Trade-offs?
  • Best practices for webhook retry logic beyond Stripe's defaults?
  • How to debug webhook issues in production?
  • Testing webhook edge cases (refunds, chargebacks, disputes)?
  • Drop corrections and real scenarios in the comments. If you've lost revenue because webhooks failed silently, we want to hear it.

    ---

    Resources

  • [Official Stripe Webhooks Docs](https://stripe.com/docs/webhooks)
  • [Stripe API Reference (Events)](https://stripe.com/docs/api/events)
  • [Stripe CLI Docs](https://stripe.com/docs/stripe-cli)
  • [Webhook Security Best Practices](https://stripe.com/docs/webhooks/signatures)
  • 🔥 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