How to Set Up Stripe Payments in Next.js 2026: Checkout, Webhooks, and Subscriptions
Complete guide to integrating Stripe payments in Next.js with App Router. Set up Stripe Checkout, handle webhooks, add subscription billing, manage customer portals, and test with the Stripe CLI. Updated for Next.js 15 and Stripe API 2026.
On this page
Adding payments to a Next.js app is one of those tasks that sounds simple but involves multiple moving parts: server-side API routes, client-side redirects, webhook verification, subscription lifecycle management, and handling edge cases like failed payments and duplicate charges. I integrated Stripe into a SaaS platform serving over 10,000 businesses and ran into most of these issues firsthand. This guide covers everything from initial setup to production-ready subscription billing using Next.js App Router and the latest Stripe API.
What You Will Build
- Stripe Checkout for one-time payments
- Recurring subscription billing with multiple plans
- Webhook handler to sync payment events with your database
- Customer portal for managing subscriptions
- Server-side payment verification
- Testing the full flow with Stripe CLI
Prerequisites
- Next.js 14 or 15 with App Router
- Node.js 18 or above
- A Stripe account (free to create at stripe.com)
- A database for storing user and subscription data (PostgreSQL with Prisma recommended)
- Basic familiarity with TypeScript and React Server Components
Step 1: Install Stripe Packages
You need two packages: `stripe` for server-side API calls and `@stripe/stripe-js` for the client-side Stripe.js loader. The server-side package handles creating checkout sessions, managing subscriptions, and verifying webhooks. The client-side package loads Stripe.js for redirecting users to Checkout.
Step 2: Set Up Environment Variables
Go to the Stripe Dashboard, navigate to Developers and then API keys. Copy your publishable key and secret key. For local development use the test mode keys. Never expose your secret key to the client.
The STRIPE_WEBHOOK_SECRET is generated when you set up the webhook endpoint. We will get this value in a later step.
Step 3: Create the Stripe Server Client
Create a shared Stripe instance that you import wherever you need to call the Stripe API on the server. This keeps your Stripe version and config consistent across all API routes and server actions.
Step 4: Create the Client-Side Stripe Loader
For client-side operations like redirecting to Stripe Checkout, you need to load Stripe.js. Use the `loadStripe` function from @stripe/stripe-js and wrap it so it only loads once.
Step 5: Create a Checkout Session API Route
This is the core of Stripe integration. When a user clicks a buy or subscribe button, your frontend calls this API route. The route creates a Stripe Checkout Session on the server and returns the session URL. The frontend then redirects the user to Stripe hosted checkout page where they enter payment details.
Important details: the `metadata` field lets you attach your internal userId to the Stripe session. You will need this in the webhook to link the Stripe subscription back to your user in the database. The `mode` can be `subscription` for recurring billing or `payment` for one-time charges.
Step 6: Create the Pricing Page with Checkout Button
Create a pricing page that displays your plans and handles the checkout redirect. When the user clicks a plan, the component calls your checkout API route, gets back the Stripe Checkout URL, and redirects the browser.
Step 7: Handle Webhooks
This is the most critical part of Stripe integration and the part most tutorials skip or get wrong. Webhooks are how Stripe tells your app about payment events: successful payments, failed charges, subscription renewals, cancellations, and refunds. You must never rely on the success_url redirect alone because users can close the browser before reaching it. The webhook is the only reliable way to know a payment succeeded.
Key points about this webhook handler: always verify the signature before processing any event. The raw body must be passed to constructEvent, not the parsed JSON. Handle at least these five events for subscriptions: checkout.session.completed, invoice.payment_succeeded, invoice.payment_failed, customer.subscription.deleted, and customer.subscription.updated. Always return a 200 response quickly, even if your database write takes time, to prevent Stripe from retrying.
Step 8: Add the Customer Portal
Stripe provides a hosted customer portal where users can update their payment method, switch plans, cancel their subscription, and view invoice history. You do not need to build any of this UI yourself. Create an API route that generates a portal session URL and redirect the user.
Before using the customer portal, enable it in the Stripe Dashboard under Settings then Billing then Customer portal. Configure which features you want to allow: plan switching, cancellation, payment method updates, and invoice history.
Step 9: Create Products and Prices in Stripe Dashboard
Before testing, you need to create products and prices in the Stripe Dashboard. Go to Products, click Add Product, enter a name and description, then add a recurring price. You can also create prices using the Stripe API or CLI.
Copy the price IDs (starting with price_) and use them in your pricing page component. For one-time payments, change the checkout session mode from subscription to payment.
Step 10: Test with Stripe CLI
The Stripe CLI lets you test webhooks locally by forwarding Stripe events to your localhost. Install the CLI, log in, and start forwarding events to your webhook route.
The CLI will print a webhook signing secret starting with whsec_. Copy this value and add it to your .env.local as STRIPE_WEBHOOK_SECRET. This secret is different from your production webhook secret.
Test Card Numbers
Trigger Test Events
Step 11: One-Time Payments Instead of Subscriptions
If your app sells one-time products instead of subscriptions, change the checkout session mode to payment and use a one-time price ID instead of a recurring one. The webhook event to listen for is checkout.session.completed with mode payment instead of subscription.
Step 12: Deploy Webhook to Production
When deploying to production on Vercel, Netlify, or AWS, you need to register a production webhook endpoint in the Stripe Dashboard.
- Go to Stripe Dashboard then Developers then Webhooks
- Click Add endpoint
- Enter your production URL: https://yourdomain.com/api/webhooks/stripe
- Select events to listen to: checkout.session.completed, invoice.payment_succeeded, invoice.payment_failed, customer.subscription.deleted, customer.subscription.updated
- Copy the signing secret (whsec_) and add it to your production environment variables as STRIPE_WEBHOOK_SECRET
- Switch your STRIPE_SECRET_KEY from sk_test to sk_live in production environment
Common Mistakes to Avoid
- Never rely on the success_url redirect to confirm payment. Users can close the browser. Always use webhooks.
- Never parse the request body as JSON before passing it to constructEvent. Stripe needs the raw body string for signature verification.
- Never expose your STRIPE_SECRET_KEY on the client side. Only the NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY should be public.
- Always handle invoice.payment_failed in your webhook. Subscriptions can fail silently without this.
- Always return a 200 response from your webhook handler quickly. If Stripe does not get a 200 within a few seconds, it will retry the event up to 3 days.
- Do not use the same webhook secret for local testing and production. The Stripe CLI generates a different secret than your dashboard endpoint.
- Store the Stripe customer ID in your database. Creating a new customer for every checkout leads to duplicate customers.
Full Project Structure
Quick Start Checklist
- Install stripe and @stripe/stripe-js
- Add STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, and STRIPE_WEBHOOK_SECRET to .env.local
- Create lib/stripe.ts for server-side and lib/stripe-client.ts for client-side
- Create the checkout API route at app/api/checkout/route.ts
- Create the webhook handler at app/api/webhooks/stripe/route.ts
- Create the customer portal route at app/api/portal/route.ts
- Create products and prices in Stripe Dashboard
- Test locally with stripe listen --forward-to localhost:3000/api/webhooks/stripe
- Use test card 4242 4242 4242 4242 for successful payments
- Register a production webhook endpoint before going live
- Switch from test to live API keys in production
Official-Style Summary
Integrating Stripe with Next.js App Router involves four main pieces: a checkout API route that creates Stripe Checkout Sessions, a webhook handler that receives and verifies payment events from Stripe, a customer portal route for subscription management, and client-side code that redirects users to the Stripe-hosted checkout page. The webhook handler is the most important part because it is the only reliable way to know a payment succeeded. Always verify the webhook signature using the raw request body, handle at least five subscription events (checkout.session.completed, invoice.payment_succeeded, invoice.payment_failed, customer.subscription.deleted, customer.subscription.updated), and return a 200 response immediately. Use the Stripe CLI for local testing, create separate webhook secrets for development and production, and never expose your secret key on the client side.
FAQ
Do I need @stripe/react-stripe-js for Next.js?
Not necessarily. For Stripe Checkout (the hosted payment page), you only need the stripe server package and @stripe/stripe-js for the redirect. The @stripe/react-stripe-js package is only needed if you want to build a custom payment form with Stripe Elements embedded directly in your page instead of using the hosted Checkout.
Can I use Stripe with Next.js Server Actions instead of API routes?
Yes. You can create Stripe Checkout sessions inside a Server Action instead of an API route. However, webhook handlers must remain as API routes because Stripe sends POST requests to a URL endpoint. Server Actions are triggered by form submissions from the client, not external HTTP requests.
Why is my Stripe webhook returning 400 or signature verification failed?
The most common cause is parsing the request body as JSON before passing it to constructEvent. Stripe needs the raw body string, not parsed JSON. In Next.js App Router, use req.text() instead of req.json() to get the raw body. Also make sure your STRIPE_WEBHOOK_SECRET matches the endpoint: the Stripe CLI generates a different secret than your production dashboard webhook.
How do I handle Stripe in a Next.js middleware or edge runtime?
The Stripe Node.js SDK does not work in edge runtimes because it depends on Node.js APIs. Your webhook and checkout routes must use the Node.js runtime. Add export const runtime = 'nodejs' at the top of your route file if your Next.js project defaults to edge.
Should I use Stripe Checkout or Stripe Elements?
Use Stripe Checkout (the hosted page) for most apps. It handles payment UI, 3D Secure authentication, address collection, tax calculation, and PCI compliance automatically. Stripe Elements gives you more design control by embedding payment fields directly in your page, but you have to handle more logic yourself. Checkout is faster to implement and covers more edge cases out of the box.
How do I test failed payments and subscription cancellations locally?
Use the Stripe CLI to trigger specific events with stripe trigger invoice.payment_failed or stripe trigger customer.subscription.deleted. For testing failed card payments through the actual checkout flow, use the test card number 4000 0000 0000 0002 which always declines.
Shahmeer Rizwan
Full-Stack Developer