Next.js + Stripe Integration
This Skill teaches Claude how to implement Stripe payment processing in Next.js projects, including one-time payments, subscriptions, webhooks, and customer management. Based on real-world implementation experience with modern Stripe APIs and authentication frameworks.
⚠️ CRITICAL: Breaking Changes in Modern Stripe.js
stripe.redirectToCheckout() is DEPRECATED and no longer works!
Modern Stripe implementations use the checkout session URL directly:
// ❌ OLD (BROKEN) const { error } = await stripe.redirectToCheckout({ sessionId });
// ✅ NEW (CORRECT) const session = await stripe.checkout.sessions.create({...}); window.location.href = session.url; // Use the URL directly!
Quick Start Checklist
When implementing Stripe in a Next.js project:
-
Install dependencies: stripe and @stripe/stripe-js
-
Configure environment: Add NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY and STRIPE_SECRET_KEY to .env.local
-
Access env vars correctly: Load inside functions, NOT at module level (critical for runtime)
-
Create API routes: Build endpoints for checkout sessions, webhooks, and customer portal
-
Build UI: Create checkout forms and payment pages
-
Handle webhooks: Set up secure webhook handlers for payment events
-
Update middleware: Add payment routes to unauthenticatedPaths if using auth middleware
-
Test locally: Use Stripe CLI for webhook testing
Core Implementation Patterns
- Environment Setup & Runtime Loading
.env.local
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... STRIPE_SECRET_KEY=sk_test_... STRIPE_WEBHOOK_SECRET=whsec_...
CRITICAL: Access environment variables inside API route functions, NOT at module initialization:
// ❌ WRONG - Fails at build/startup const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); export async function POST() { ... }
// ✅ CORRECT - Variables loaded at runtime export async function POST(request: NextRequest) { const stripeSecretKey = process.env.STRIPE_SECRET_KEY; if (!stripeSecretKey) { return NextResponse.json({ error: 'API key not configured' }, { status: 500 }); } const stripe = new Stripe(stripeSecretKey); // ... rest of function }
Important: Only use NEXT_PUBLIC_ prefix for publishable keys. Secret keys stay server-side only.
- One-Time Payments (Checkout) - Modern Approach
API Route (app/api/checkout/route.ts ):
-
Load Stripe with secret key inside the function
-
Create a Stripe checkout session with mode: 'payment'
-
Return the full session URL (not just session ID)
-
Verify webhook signatures on payment success
// ✅ CORRECT: Load env vars inside function const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); const session = await stripe.checkout.sessions.create({...}); return NextResponse.json({ url: session.url }); // Return URL directly
Client Side (Simplified):
-
NO need to load Stripe.js for basic checkout
-
Call checkout API route
-
Redirect to session.url directly from response
-
Handle success/cancel redirects via query parameters
- Subscriptions
Differences from one-time payments:
-
Create products in Stripe Dashboard with recurring pricing
-
Use mode: 'subscription' when creating checkout sessions
-
Manage customer subscriptions in database
-
Handle multiple lifecycle events via webhooks
Key workflow:
-
Fetch available subscription tiers from Stripe API
-
Display pricing page with subscription options
-
Create checkout session with subscription mode
-
Handle customer.subscription.created webhook
-
Sync subscription status to your database
- Webhook Handling
Critical security requirements:
-
Verify webhook signatures using Stripe's libraries
-
Use raw request body for signature validation (disable body parsing)
-
Handle these key events:
-
payment_intent.succeeded — one-time payment confirmed
-
customer.subscription.created — new subscription
-
customer.subscription.updated — subscription changes
-
customer.subscription.deleted — cancellation
-
invoice.payment_succeeded — renewal payment
Webhook endpoint (app/api/webhooks/stripe/route.ts ):
-
Accept POST requests from Stripe
-
Verify signature: stripe.webhooks.constructEvent(body, signature, secret)
-
Process event and update database
-
Return 200 status to acknowledge
- Authentication Middleware Configuration
When using WorkOS or similar auth frameworks, explicitly allow payment routes:
// middleware.ts export default authkitMiddleware({ eagerAuth: true, middlewareAuth: { enabled: true, unauthenticatedPaths: [ '/', '/sign-in', '/sign-up', '/api/checkout', // Allow unauthenticated checkout '/api/webhooks/stripe', // Allow webhook delivery '/payment-success', '/payment-cancel', ], }, });
Why: Without this, auth middleware intercepts payment routes, causing CORS errors when the frontend tries to call them.
- Customer Portal
Enable users to manage subscriptions without custom code:
-
Configure Customer Portal in Stripe Dashboard
-
Create API route that generates portal sessions
-
Redirect users to portal for managing subscriptions, payment methods, and invoices
Implementation Guide
Setup Phase
-
Create Next.js project (or use existing)
-
Install Stripe packages: npm install stripe @stripe/stripe-js
-
Get API keys from Stripe Dashboard → Developers → API Keys
-
Add keys to .env.local
-
Add .env.local to .gitignore
Build Checkout Flow (One-Time Payments)
Create app/api/checkout/route.ts :
-
Load Stripe with secret key inside the function
-
Accept POST with amount and metadata
-
Create checkout session
-
Return session.url directly (not just session ID)
-
See API_ROUTES.md for complete code
Create checkout page:
-
Simple button component (no Stripe.js needed for basic flow)
-
Call checkout API route on button click
-
Redirect to response.url directly
-
Handle success/cancel via query parameters
Create success page:
-
Accepts session_id query parameter
-
Retrieves session details from Stripe (optional - for confirmation display)
-
Displays confirmation message
-
Can fetch order details from your database
Build Subscription Flow
Create product in Stripe Dashboard (recurring pricing)
Create app/api/subscriptions/list/route.ts :
-
Fetch products and prices from Stripe API
-
Return formatted subscription tiers
Create app/api/checkout-subscription/route.ts :
-
Similar to checkout flow but use mode: 'subscription'
-
Link to price ID instead of amount
Create subscriptions page:
-
Fetch available tiers from API
-
Display subscription cards with pricing
-
Implement checkout on selection
Create app/api/customer-portal/route.ts :
-
Accept POST request
-
Create portal session with customer ID
-
Return portal URL
Webhook Integration
Create app/api/webhooks/stripe/route.ts :
-
Disable body parsing: export const config = { api: { bodyParser: false } }
-
Extract raw body and signature from headers
-
Verify: stripe.webhooks.constructEvent(body, signature, webhookSecret)
-
Handle subscription and payment events
-
Update database based on event type
Test locally with Stripe CLI:
stripe listen --forward-to localhost:3000/api/webhooks/stripe stripe trigger payment_intent.succeeded
Deploy webhook endpoint to production
Add webhook endpoint URL in Stripe Dashboard → Webhooks
Use production secret key for production webhooks
Best Practices
-
PCI Compliance: Always load Stripe.js from Stripe's CDN, never bundle it
-
Singleton Pattern: Lazy-load Stripe.js only when needed (performance optimization)
-
Environment Variables: Use NEXT_PUBLIC_ only for publishable keys
-
Error Handling: Catch and log errors from Stripe API calls
-
Webhook Security: Always verify signatures; never trust webhook data without verification
-
Database Sync: Store customer IDs, subscription status, and invoice data in your database
-
Testing: Use Stripe test mode keys during development; switch to live keys only in production
-
Customer Portal: Leverage it for subscription management instead of building custom UI
Common Patterns
Check if User has Active Subscription
// Query your database for customer's subscription status const subscription = await db.subscriptions.findFirst({ where: { userId, status: 'active' } }); return subscription !== null;
Handle Failed Payments
Listen for invoice.payment_failed webhook and:
-
Send customer notification email
-
Update UI to show payment issue
-
Offer retry option via customer portal
Prorate Subscription Changes
Stripe handles this automatically when updating subscriptions via the API. Use proration_behavior to control how changes are billed.
Architecture Recommendations
app/ ├── api/ │ ├── checkout/route.ts # One-time payment sessions │ ├── checkout-subscription/route.ts │ ├── subscriptions/ │ │ └── list/route.ts # Get available tiers │ ├── customer-portal/route.ts # Manage subscriptions │ └── webhooks/ │ └── stripe/route.ts # Webhook handler ├── checkout/ │ └── page.tsx # Checkout form ├── success/ │ └── page.tsx # Success page └── subscriptions/ └── page.tsx # Subscription tiers
Deployment Considerations
-
Vercel: Natural fit for Next.js projects; environment variables work seamlessly
-
Environment Variables: Ensure all keys are added to your hosting platform
-
Webhooks: Update webhook endpoint URL in Stripe Dashboard after deployment
-
HTTPS: Required for production (Stripe won't send webhooks to non-HTTPS URLs)
-
Testing: Create webhook endpoints in both test and production modes
References and Resources
-
Vercel Next.js + Stripe Guide
-
Stripe Subscriptions with Next.js
-
Stripe Official Documentation
-
Stripe Sample Applications