Stripe Integration for SaaS
Setup
Environment Variables
STRIPE_SECRET_KEY=sk_... STRIPE_PUBLISHABLE_KEY=pk_... STRIPE_WEBHOOK_SECRET=whsec_...
Install
pnpm add stripe @stripe/stripe-js
Server Client
// lib/stripe.ts import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2024-11-20.acacia', typescript: true, });
Checkout Session
// app/api/checkout/route.ts import { stripe } from '@/lib/stripe'; import { auth } from '@/lib/auth';
export async function POST(request: Request) { const user = await auth(); if (!user) return new Response('Unauthorized', { status: 401 });
const { priceId } = await request.json();
// Validate price ID against allowed list const allowedPrices = ['price_xxx', 'price_yyy']; if (!allowedPrices.includes(priceId)) { return new Response('Invalid price', { status: 400 }); }
const session = await stripe.checkout.sessions.create({
customer: user.stripeCustomerId,
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: ${process.env.NEXT_PUBLIC_URL}/dashboard?success=true,
cancel_url: ${process.env.NEXT_PUBLIC_URL}/pricing,
subscription_data: {
metadata: { userId: user.id },
},
});
return Response.json({ url: session.url }); }
Webhook Handler (CRITICAL)
See templates/webhook_handler.ts for complete implementation.
Security Requirements
-
✅ Verify signature with stripe.webhooks.constructEvent()
-
✅ Use raw body (not parsed JSON)
-
✅ Return 200 quickly, process async
-
✅ Handle idempotency (check if already processed)
-
✅ Log webhook events for debugging
Event Types to Handle
-
checkout.session.completed
-
Initial purchase
-
customer.subscription.created
-
New subscription
-
customer.subscription.updated
-
Plan change
-
customer.subscription.deleted
-
Cancellation
-
invoice.payment_failed
-
Failed payment
-
invoice.paid
-
Successful payment
Testing
Forward webhooks to local
stripe listen --forward-to localhost:3000/api/webhooks/stripe
Trigger test events
stripe trigger checkout.session.completed stripe trigger customer.subscription.updated stripe trigger invoice.payment_failed
Common Errors
"No signatures found matching"
-
Check STRIPE_WEBHOOK_SECRET is correct
-
Ensure using raw body: await request.text()
"Webhook timeout"
-
Process heavy work async
-
Return 200 immediately, use queue for processing