Add Stripe payment processing to Next.js projects. Implement checkout sessions, payment handling, subscriptions, webhooks, and customer management. Use when adding Stripe to a Next.js project, building payment flows, implementing subscriptions, or integrating payment processing.
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.
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!
When implementing Stripe in a Next.js project:
stripe and @stripe/stripe-jsNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYSTRIPE_SECRET_KEY.env.localunauthenticatedPaths if using auth middleware# .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.
API Route (app/api/checkout/route.ts):
mode: 'payment'// ✅ 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):
session.url directly from responseDifferences from one-time payments:
mode: 'subscription' when creating checkout sessionsKey workflow:
customer.subscription.created webhookCritical security requirements:
payment_intent.succeeded — one-time payment confirmedcustomer.subscription.created — new subscriptioncustomer.subscription.updated — subscription changescustomer.subscription.deleted — cancellationinvoice.payment_succeeded — renewal paymentWebhook endpoint (app/api/webhooks/stripe/route.ts):
stripe.webhooks.constructEvent(body, signature, secret)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.
Enable users to manage subscriptions without custom code:
npm install stripe @stripe/stripe-js
.env.local.env.local to .gitignoreCreate app/api/checkout/route.ts:
Create checkout page:
response.url directlyCreate success page:
session_id query parameterCreate product in Stripe Dashboard (recurring pricing)
Create app/api/subscriptions/list/route.ts:
Create app/api/checkout-subscription/route.ts:
mode: 'subscription'Create subscriptions page:
Create app/api/customer-portal/route.ts:
Create app/api/webhooks/stripe/route.ts:
export const config = { api: { bodyParser: false } }stripe.webhooks.constructEvent(body, signature, webhookSecret)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
NEXT_PUBLIC_ only for publishable keys// Query your database for customer's subscription status
const subscription = await db.subscriptions.findFirst({
where: { userId, status: 'active' }
});
return subscription !== null;
Listen for invoice.payment_failed webhook and:
Stripe handles this automatically when updating subscriptions via the API. Use proration_behavior to control how changes are billed.
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