Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/microck-nextjs-stripe-integration/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 65% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 46% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 66% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 122% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 67% | 0% |
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:
typescript// ❌ 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_KEY and STRIPE_SECRET_KEY to .env.localunauthenticatedPaths if using auth middlewareenv# .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:
typescript// ❌ 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'typescript// ✅ 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:
typescript// 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:
bash npm install stripe @stripe/stripe-js
.env.local.env.local to .gitignoreapp/api/checkout/route.ts:response.url directlysession_id query parameterapp/api/subscriptions/list/route.ts:app/api/checkout-subscription/route.ts:mode: 'subscription'app/api/customer-portal/route.ts:app/api/webhooks/stripe/route.ts:export const config = { api: { bodyParser: false } }stripe.webhooks.constructEvent(body, signature, webhookSecret)bash stripe listen --forward-to localhost:3000/api/webhooks/stripe stripe trigger payment_intent.succeeded
NEXT_PUBLIC_ only for publishable keystypescript// 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| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-14 | pass→pass | 12,841 | 12,336 | -4% | 1 | 1 | 0% | 2,398 | 4,538 | +89% | 0 | 0 | — |
case-15 | pass→pass | 10,695 | 8,691 | -19% | 1 | 1 | 0% | 1,855 | 4,202 | +127% | 0 | 0 | — |
case-08 | pass→pass | 21,006 | 7,732 | -63% | 1 | 1 | 0% | 2,360 | 3,954 | +68% | 0 | 0 | — |
case-01 | fail→pass | 12,714 | 11,690 | -8% | 1 | 1 | 0% | 2,658 | 4,386 | +65% | 0 | 0 | — |
case-02 | fail→pass | 16,722 | 12,224 | -27% | 1 | 1 | 0% | 3,436 | 5,000 | +46% | 0 | 0 | — |
case-03 | fail→pass | 14,899 | 12,835 | -14% | 1 | 1 | 0% | 3,076 | 5,098 | +66% | 0 | 0 | — |
case-04 | pass→pass | 6,956 | 4,787 | -31% | 1 | 1 | 0% | 1,382 | 3,419 | +147% | 0 | 0 | — |
case-05 | fail→pass | 8,390 | 5,091 | -39% | 1 | 1 | 0% | 1,549 | 3,445 | +122% | 0 | 0 | — |
case-06 | fail→pass | 12,485 | 5,895 | -53% | 1 | 1 | 0% | 2,190 | 3,659 | +67% | 0 | 0 | — |
case-07 | fail→fail | 13,756 | 9,003 | -35% | 1 | 1 | 0% | 2,327 | 4,011 | +72% | 0 | 0 | — |
case-09 | fail→pass | 11,418 | 10,077 | -12% | 1 | 1 | 0% | 2,369 | 4,536 | +91% | 0 | 0 | — |
case-10 | pass→pass | 12,103 | 10,159 | -16% | 1 | 1 | 0% | 2,395 | 4,515 | +89% | 0 | 0 | — |
case-11 | fail→pass | 13,906 | 10,033 | -28% | 1 | 1 | 0% | 2,895 | 4,481 | +55% | 0 | 0 | — |
case-12 | fail→pass | 13,991 | 12,197 | -13% | 1 | 1 | 0% | 2,660 | 5,135 | +93% | 0 | 0 | — |
case-13 | pass→pass | 3,114 | 2,893 | -7% | 1 | 1 | 0% | 580 | 2,923 | +404% | 0 | 0 | — |
case-16 | pass→pass | 14,623 | 16,583 | +13% | 1 | 1 | 0% | 2,461 | 5,296 | +115% | 0 | 0 | — |
case-17 | pass→pass | 13,885 | 6,713 | -52% | 1 | 1 | 0% | 2,546 | 3,640 | +43% | 0 | 0 | — |
case-18 | pass→pass | 16,257 | 15,084 | -7% | 1 | 1 | 0% | 3,577 | 5,773 | +61% | 0 | 0 | — |
case-19 | pass→pass | 3,946 | 3,426 | -13% | 1 | 1 | 0% | 753 | 3,161 | +320% | 0 | 0 | — |
case-20 | pass→pass | 11,510 | 11,284 | -2% | 1 | 1 | 0% | 2,351 | 4,790 | +104% | 0 | 0 | — |
case-21 | pass→pass | 22,945 | 26,647 | +16% | 1 | 1 | 0% | 5,115 | 8,278 | +62% | 0 | 0 | — |
case-22 | pass→pass | 10,492 | 9,361 | -11% | 1 | 1 | 0% | 2,035 | 4,408 | +117% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +36 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.