DevKit Market
  • Home
  • Categories
  • Products
  • Tools
  • Claude skills
  • Blog
  • About
Sign inGet started
DevKit Market
HomeCategoriesProductsToolsClaude skillsBlogAbout
Theme
Sign inGet started
DevKit Market

Production-ready starter kits for developers who want to ship faster, not fiddle with boilerplate.

Products

SaaS Starter ProNext.js Blog KitAuth BoilerplateLanding Page KitAdmin DashboardWaitlist App

Company

Hire meBlogClaude skillsAbout

Support

FAQContact

© 2026 DevKit Market. Built solo with Next.js & Claude.

Sitemap
Blog/Tutorial/How to Add Stripe to Next.js (2026)
Tutorial
April 12, 2026•8 min read

How to Add Stripe to Next.js (2026)

Nikhil Anand
Lead Developer @ DevKit

How to Add Stripe to Next.js (2026): The Ultimate Integration Guide

Integrating Stripe into a Next.js application has changed significantly with the arrival of the App Router and Server Actions. Gone are the days of complex
text
/api/checkout
folders and manual loading state management; today, we leverage Embedded Checkout, Server Actions, and webhook-driven fulfillment to build a bulletproof payment flow in under an hour.
This guide walks you through a production-ready setup for Next.js 15, including the new Embedded Checkout (recommended by Stripe in 2026), webhook signature verification with idempotency, the Customer Portal for self-service subscriptions, and the critical "asynchronous fulfillment" pattern that prevents order loss.
Live demo: stripe-demo.devkitmarket.com GitHub repo: github.com/devkit-market/nextjs-stripe-2026

5-Minute Overview — The Workflow

  1. Environment Setup: Get your API keys and configure
    text
    .env.local
    .
  2. Stripe Singleton: Create a typed Stripe instance you can import anywhere.
  3. Checkout Session: Build a Server Action that returns a
    text
    client_secret
    .
  4. Embedded Checkout UI: Mount Stripe's checkout inside your own page (no redirects).
  5. Webhook Fulfillment: Verify events with the SDK and update your database safely.
  6. Customer Portal: Let users manage subscriptions themselves with one extra route.

Step 1 — API Keys & Dependencies

First, install the Stripe server SDK and the new React-friendly client packages:
bash
npm install stripe @stripe/stripe-js @stripe/react-stripe-js
Next, add your keys to
text
.env.local
. Never expose your Secret Key to the client — only the Publishable Key is safe for the browser.
bash
# .env.local
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_BASE_URL=http://localhost:3000
Get your test keys from Stripe Dashboard → Developers → API keys. The webhook secret is generated in Step 5 — leave it blank for now.

Step 2 — The Stripe Singleton

Create a single typed Stripe instance you can import everywhere on the server. This keeps your API version pinned and avoids re-instantiating the client on every request.
Create
text
lib/stripe.ts
:
typescript
import Stripe from "stripe";

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2025-09-30.clover", // Pin to a known version, never auto-upgrade
  typescript: true,
});
Pinning the API version is critical — Stripe ships breaking changes regularly, and "latest" will silently break your integration the day they release a new one.

Step 3 — Creating a Checkout Session (Server Action)

In Next.js 15, we use Server Actions to create the Checkout Session. This is cleaner than API routes and fully type-safe end to end.
Create
text
app/actions/stripe.ts
:
typescript
"use server";

import { stripe } from "@/lib/stripe";
import { headers } from "next/headers";

export async function createCheckoutSession(priceId: string) {
  // Always look up price on the server — never trust the client
  const price = await stripe.prices.retrieve(priceId);
  if (!price.active) throw new Error("Price is not active");

  const origin = (await headers()).get("origin") || process.env.NEXT_PUBLIC_BASE_URL!;

  const session = await stripe.checkout.sessions.create({
    ui_mode: "embedded", // 2026 default — keeps users on your domain
    line_items: [{ price: priceId, quantity: 1 }],
    mode: price.recurring ? "subscription" : "payment",
    return_url: `${origin}/return?session_id={CHECKOUT_SESSION_ID}`,
    automatic_tax: { enabled: true },
    allow_promotion_codes: true,
  });

  if (!session.client_secret) throw new Error("Could not create session");

  return { clientSecret: session.client_secret };
}
The big shift in 2026:
text
ui_mode: "embedded"
returns a
text
client_secret
instead of a redirect URL
. Your users stay on your domain through the entire checkout, which improves conversion and brand trust.

Step 4 — The Embedded Checkout UI

Now mount Stripe's checkout component inside your own page. Create
text
app/checkout/page.tsx
:
tsx
"use client";

import { useCallback } from "react";
import { loadStripe } from "@stripe/stripe-js";
import {
  EmbeddedCheckoutProvider,
  EmbeddedCheckout,
} from "@stripe/react-stripe-js";
import { createCheckoutSession } from "@/actions/stripe";

const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

export default function CheckoutPage({
  searchParams,
}: {
  searchParams: { priceId: string };
}) {
  const fetchClientSecret = useCallback(async () => {
    const { clientSecret } = await createCheckoutSession(searchParams.priceId);
    return clientSecret;
  }, [searchParams.priceId]);

  return (
    <div id="checkout" className="min-h-screen p-8">
      <EmbeddedCheckoutProvider
        stripe={stripePromise}
        options={{ fetchClientSecret }}
      >
        <EmbeddedCheckout />
      </EmbeddedCheckoutProvider>
    </div>
  );
}
That's the entire checkout UI. Stripe handles cards, Apple Pay, Google Pay, Klarna, Link, and 25+ other payment methods automatically based on the customer's location.
If you prefer the classic redirect flow (Stripe-hosted page), swap
text
ui_mode: "embedded"
for
text
ui_mode: "hosted"
in the Server Action and use
text
redirect(session.url!)
instead of returning the client secret.

Step 5 — Webhook Implementation (Critical)

The browser redirect is not 100% reliable. If the user closes their tab between paying and the return URL firing, you'll never know they paid. To ensure you never lose a payment, you must use webhooks as the source of truth for fulfillment.
Create
text
app/api/webhook/stripe/route.ts
:
typescript
import { headers } from "next/headers";
import { NextResponse } from "next/server";
import type Stripe from "stripe";
import { stripe } from "@/lib/stripe";

export async function POST(req: Request) {
  const body = await req.text(); // raw body — do NOT use req.json()
  const signature = (await headers()).get("stripe-signature");

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature!,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err: any) {
    return NextResponse.json(
      { error: `Webhook Error: ${err.message}` },
      { status: 400 }
    );
  }

  // Idempotency check — Stripe may retry the same event
  const alreadyProcessed = await db.webhookEvent.findUnique({
    where: { stripeId: event.id },
  });
  if (alreadyProcessed) {
    return NextResponse.json({ received: true, duplicate: true });
  }

  // Handle the event
  switch (event.type) {
    case "checkout.session.completed":
      const session = event.data.object as Stripe.Checkout.Session;
      await fulfillOrder(session); // grant access, send email, etc.
      break;
    case "invoice.paid":
      await extendSubscription(event.data.object as Stripe.Invoice);
      break;
    case "customer.subscription.deleted":
      await revokeAccess(event.data.object as Stripe.Subscription);
      break;
    case "invoice.payment_failed":
      await notifyDunning(event.data.object as Stripe.Invoice);
      break;
    default:
      console.log(`Unhandled event type ${event.type}`);
  }

  // Mark as processed only AFTER fulfillment succeeds
  await db.webhookEvent.create({ data: { stripeId: event.id } });

  return NextResponse.json({ received: true });
}
To get your webhook secret: install the Stripe CLI, run
text
stripe login
, then
text
stripe listen --forward-to localhost:3000/api/webhook/stripe
. The CLI prints the
text
whsec_...
secret — paste it into
text
.env.local
. For production, register the endpoint in Stripe Dashboard → Developers → Webhooks and copy the live signing secret.
The idempotency table is the part most tutorials skip and the part that breaks production. Without it, a retried webhook doubles every order.

Step 6 — The Customer Portal

For subscriptions, you need a way for customers to update their card, cancel, switch plans, or download invoices. Building this yourself is weeks of work. Stripe gives it to you in one route.
Create
text
app/actions/portal.ts
:
typescript
"use server";

import { stripe } from "@/lib/stripe";
import { auth } from "@/lib/auth"; // your auth provider
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export async function openCustomerPortal() {
  const { userId } = await auth();
  if (!userId) throw new Error("Unauthorized");

  const user = await db.user.findUnique({ where: { id: userId } });
  if (!user?.stripeCustomerId) throw new Error("No Stripe customer");

  const origin = (await headers()).get("origin")!;

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: user.stripeCustomerId,
    return_url: `${origin}/dashboard`,
  });

  redirect(portalSession.url);
}
Then add a button anywhere in your dashboard:
tsx
<form action={openCustomerPortal}>
  <button className="px-4 py-2 bg-slate-900 text-white rounded-lg">
    Manage Subscription
  </button>
</form>
That single form gives every paying customer a complete self-service billing UI. Configure what they can do (cancel, switch plans, update payment methods) in Stripe Dashboard → Settings → Customer Portal.

Step 7 — Production Checklist

Before you flip to Live Mode, ensure you've ticked these boxes:
  • Switch to Live Keys: Replace
    text
    sk_test_
    and
    text
    pk_test_
    with
    text
    sk_live_
    and
    text
    pk_live_
    in your production environment (Vercel, Netlify, etc). Never commit live keys to git.
  • Register Production Webhook: Add a new endpoint pointing to your production URL in Dashboard → Webhooks, and use that secret — test and live webhooks have different secrets.
  • Webhook Idempotency: Confirm your DB has the
    text
    webhookEvent
    table or equivalent. A retried webhook without idempotency = duplicate orders.
  • Server-Side Price Validation: Always retrieve the price from Stripe or your DB inside the Server Action. Never trust an amount the client sends.
  • Enable Stripe Tax: Turn it on in Dashboard → Tax to handle VAT, GST, and US sales tax automatically. One toggle saves a tax accountant.
  • Set Up Radar Rules: Stripe's fraud tool is on by default but customize the rules for your risk tolerance under Dashboard → Radar.
  • Branding: Update logo, brand color, and accent in Dashboard → Settings → Branding so Embedded Checkout matches your site.
  • Test the Full Failure Path: Use card
    text
    4000 0000 0000 0341
    (succeeds then disputes) and
    text
    4000 0000 0000 9995
    (insufficient funds) before going live.

Conclusion

Stripe is the gold standard for online payments for a reason — it handles cards, wallets, BNPL, subscriptions, tax, and fraud out of the box with a single integration. By combining it with Next.js Server Actions and Embedded Checkout, you reduce the surface area for bugs, keep customers on your domain for the entire flow, and ship a polished payment experience in a single afternoon.
Need a pre-built template with this already configured? Check out our SaaS Starter Pro which includes Stripe, Subscriptions, the Customer Portal, and Webhook Idempotency out of the box.

Skip the setup and start shipping

Love this guide? All these patterns are pre-configured in our **SaaS Starter Pro** kit. Save 40+ hours of development.

Explore the Kit

Related Articles

Selected insights to level up your development workflow.

View all
Tutorial
12 min

How to Add Razorpay to Next.js (2026): Complete Guide with Code

Step-by-step guide to integrate Razorpay payment gateway in Next.js 15 with App Router, TypeScript, webhooks, and refunds.

Read more
Tutorial
8 min

Next.js + Prisma + Stripe Tutorial

Learn how to build a subscription-based SaaS using the powerhouse trio of Next.js, Prisma, and Stripe.

Read more
Tutorial
10 min

Next.js Authentication with Clerk — Complete Guide

Why Clerk is our top choice for SaaS authentication and how to set it up in under 15 minutes.

Read more
Browse all articles
Free for everyoneno signup · no credit card

Keep building with free resources

Production-ready starter kits and zero-friction developer tools — the same ones we use to ship our own products.

4 kits
9 tools

Starter Kits

clone · ship
FreeFeatured

Next.js Blog Kit

MDX-powered blog with full SEO, dark mode, RSS feed, reading time, and syntax highlighting. Deploy to Vercel in one click.

Next.jsMDXTailwind
Get kit

Landing Page Kit

Free

Conversion-optimised landing page with hero, pricing, testimonials, FAQ, waitlist form, and analytics integration built in.

Waitlist App

Free

Viral referral waitlist with position tracking, email confirmation, social share, and a live Supabase backend. Zero to launch in an hour.

Developer Tools

instant · in-browser
12k+
usage / mo

Shadcn/UI Component Previewer

Live preview of shadcn/ui components with instant copy-paste code. Browse rendered components and grab snippets.

Productivity
Open tool

Next.js Project Structure Generator

8.5k

Select your stack and instantly get a production-ready folder structure. Copy the entire scaffold in one click.

.env File Generator

24k

Pick your tech stack and get a complete, commented .env boilerplate file. Never forget an environment variable.

Tailwind CSS Color Palette Generator

15k+

Enter a brand color and generate a complete Tailwind-compatible shade scale with config snippets.

Looking for something specific?

Browse the full library — 7+ kits across 4+ categories.

Browse all resources
Back to blog
Share article