How to configure Stripe webhooks
A step-by-step guide to setting up Stripe webhooks for your FounderKit app — locally and in production.
Stripe webhooks are how Stripe tells your app that something happened — a payment succeeded, a subscription was cancelled, a payment failed. If you don't set them up correctly, your app won't update user plans.
Here's how to do it right with FounderKit.
What events you need
FounderKit listens for four Stripe events:
| Event | What it does |
|-------|-------------|
| checkout.session.completed | User subscribed → update plan to PRO/ENTERPRISE |
| customer.subscription.updated | Plan changed → update stripePriceId and period end |
| customer.subscription.deleted | Subscription cancelled → reset to FREE |
| invoice.payment_failed | Payment failed → send failure email |
Local development
Install the Stripe CLI:
brew install stripe/stripe-cli/stripe
stripe login
Forward events to your local server:
stripe listen --forward-to localhost:3000/api/webhooks/stripe
Copy the webhook signing secret it shows you and add it to .env.local:
STRIPE_WEBHOOK_SECRET=whsec_...
Production setup
- Go to dashboard.stripe.com/webhooks
- Click "Add endpoint"
- Set the URL to
https://your-domain.com/api/webhooks/stripe - Select these events:
checkout.session.completedcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_failed
- Copy the signing secret and add it to your Vercel env vars
Testing locally
Run your dev server and the Stripe CLI listener in separate terminals:
# Terminal 1
npm run dev
# Terminal 2
stripe listen --forward-to localhost:3000/api/webhooks/stripe
Then trigger a test event:
stripe trigger checkout.session.completed
You should see the webhook handled in your server logs.
Common mistakes
Wrong signing secret — the local whsec_ from stripe listen is different from the production one in the Stripe dashboard. Use the right one for each environment.
Missing export const dynamic = "force-dynamic" — without this, Next.js may cache the webhook route. It's already set in FounderKit's route.ts.
Not handling invoice.payment_failed — users whose cards decline don't always cancel immediately. Stripe retries the charge. Handle this event to send a "please update your payment method" email.
If you run into issues, check the Stripe CLI logs — they show the exact payload and response code for every event.