DEV Community

Cover image for The Ultimate Guide to Stripe + Next.js (2026 Edition)
Sameer Saleem
Sameer Saleem

Posted on

The Ultimate Guide to Stripe + Next.js (2026 Edition)

In 2026, the "best way" to handle payments in Next.js has shifted. With the maturity of Server Actions, Embedded Checkout, and the React 19 integration in Next.js 15+, the friction of building a secure, high-conversion payment flow is at an all-time low.

This guide skips the legacy API routes and focuses on the high-performance, Server-Action-first architecture recommended for 2026.


1. The 2026 Lifecycle: Embedded vs. Hosted

Stripe now strongly pushes Embedded Checkout. Unlike the old redirect, it uses an iframe or a web component that lives inside your Next.js page, keeping your user on your domain while offloading all PCI compliance to Stripe.

2. Modern Project Setup

We’ll initialize a Next.js project with the App Router and the latest Stripe SDKs.

npx create-next-app@latest my-store-2026 --typescript --tailwind --app
cd my-store-2026
npm install stripe @stripe/stripe-js @stripe/react-stripe-js

Enter fullscreen mode Exit fullscreen mode

3. Secure Environment Variables

Never expose your STRIPE_SECRET_KEY. In 2026, Next.js environment variables are strictly enforced.

# .env.local
STRIPE_SECRET_KEY="sk_test_51..."
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_51..."
STRIPE_WEBHOOK_SECRET="whsec_..."

Enter fullscreen mode Exit fullscreen mode

4. The Server Action Pattern

In the 2026 docs, Server Actions are the standard for creating Checkout Sessions. This eliminates the need for /api/checkout folders.

Create src/app/actions/stripe.ts:

"use server";

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

export async function createCheckoutSession(priceId: string) {
  const origin = (await headers()).get("origin");

  const session = await stripe.checkout.sessions.create({
    ui_mode: 'embedded', // This enables the 2026 Embedded UI
    line_items: [{ price: priceId, quantity: 1 }],
    mode: 'subscription',
    return_url: `${origin}/return?session_id={CHECKOUT_SESSION_ID}`,
  });

  return { clientSecret: session.client_secret };
}

Enter fullscreen mode Exit fullscreen mode

5. Implementing Embedded Checkout

The EmbeddedCheckout component from @stripe/react-stripe-js provides a seamless "no-redirect" experience.

Create src/components/CheckoutForm.tsx:

"use client";

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

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

export default function CheckoutForm({ priceId }: { priceId: string }) {
  // We fetch the clientSecret via the Server Action
  const fetchClientSecret = async () => {
    const { clientSecret } = await createCheckoutSession(priceId);
    return clientSecret as string;
  };

  return (
    <div id="checkout">
      <EmbeddedCheckoutProvider stripe={stripePromise} options={{ fetchClientSecret }}>
        <EmbeddedCheckout />
      </EmbeddedCheckoutProvider>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

6. Critical: The 2026 Webhook Handler

Even with Server Actions, Route Handlers are still required for Webhooks because Stripe needs a static URL to "ping" when a payment succeeds.

Create src/app/api/webhook/route.ts:

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

export async function POST(req: Request) {
  const body = await req.text();
  const signature = (await headers()).get("stripe-signature")!;

  let event;

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

  // Handle the event
  if (event.type === "checkout.session.completed") {
    const session = event.data.object;
    // 2026 Practice: Trigger a background sync or email service
    console.log(`💰 Payment confirmed for ${session.id}`);
  }

  return new Response(null, { status: 200 });
}

Enter fullscreen mode Exit fullscreen mode

🚀 What's New in the 2026 Stripe Docs?

  • Link Authentication Element: Automatically detects if a user has a "Link" account (Stripe's 1-click checkout) and pre-fills their details, increasing conversion by up to 10%.
  • Adaptive Pricing: Now natively supported in Checkout Sessions—Stripe automatically shows the price in the user's local currency based on their IP.
  • Enhanced Tax ID Collection: You can now toggle tax_id_collection: { enabled: true } directly in the session creation to handle global B2B tax compliance without extra logic.

Pro-Tip: For local testing, use the Stripe CLI. Run stripe listen --forward-to localhost:3000/api/webhook to simulate successful payments on your local machine.


Top comments (0)