Back to Blog
stripenextjspaymentsecommercetutorial

How to Add Stripe Checkout to Your Next.js E-Commerce Store

ShopySeed Team·

Stripe Checkout is the fastest way to add payment processing to your Next.js e-commerce store. Instead of building a custom payment form, you redirect customers to a Stripe-hosted page that handles card input, validation, 3D Secure, and receipt emails — all PCI-compliant out of the box.

In this guide, we walk through integrating Stripe Checkout with Next.js, from installing the SDK to handling webhooks and creating orders.

Prerequisites

Before you begin, you need:

  • A Next.js project (App Router recommended)
  • A Stripe account (free to create)
  • Your Stripe API keys (found in the Stripe Dashboard under Developers → API keys)

Step 1: Install Stripe Dependencies

Install the Stripe Node.js SDK for server-side operations:

npm install stripe

You don't need the @stripe/stripe-js client library for Stripe Checkout — since customers are redirected to Stripe's hosted page, there's no client-side Stripe code required.

Step 2: Set Up Environment Variables

Add your Stripe keys to your .env.local file:

STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_APP_URL=http://localhost:3000

Never expose your STRIPE_SECRET_KEY to the client. The NEXT_PUBLIC_ prefix is only for the app URL, which is safe to expose.

Step 3: Create a Stripe Instance

Create a shared Stripe instance that you can import across your server-side code:

// lib/stripe.ts
import Stripe from "stripe";

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2025-12-18.acacia",
  typescript: true,
});

Step 4: Create a Checkout Session Endpoint

The checkout flow starts when the user clicks a "Checkout" button. Your backend creates a Stripe Checkout Session and returns the URL:

// app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";

interface CartItem {
  name: string;
  price: number; // in cents
  quantity: number;
}

export async function POST(req: NextRequest) {
  const { items }: { items: CartItem[] } = await req.json();

  const session = await stripe.checkout.sessions.create({
    mode: "payment",
    line_items: items.map((item) => ({
      price_data: {
        currency: "eur",
        product_data: {
          name: item.name,
        },
        unit_amount: item.price,
      },
      quantity: item.quantity,
    })),
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/order/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/cart`,
  });

  return NextResponse.json({ url: session.url });
}

Key things to note:

  • mode: "payment" — for one-time payments. Use "subscription" for recurring billing.
  • unit_amount — prices are in the smallest currency unit (cents for EUR/USD).
  • {CHECKOUT_SESSION_ID} — Stripe replaces this placeholder with the actual session ID.

Step 5: Redirect to Checkout

On the frontend, call your API endpoint and redirect the user:

// components/CheckoutButton.tsx
"use client";

import { useState } from "react";

export function CheckoutButton({ items }: { items: CartItem[] }) {
  const [loading, setLoading] = useState(false);

  const handleCheckout = async () => {
    setLoading(true);
    const res = await fetch("/api/checkout", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ items }),
    });
    const { url } = await res.json();
    window.location.href = url;
  };

  return (
    <button onClick={handleCheckout} disabled={loading}>
      {loading ? "Redirecting..." : "Proceed to Checkout"}
    </button>
  );
}

When the user clicks the button, they're redirected to Stripe's hosted checkout page. Stripe handles card input, validation, and 3D Secure authentication.

Step 6: Handle Webhooks

This is the most important step. Don't rely on the success redirect to confirm payment — users can close their browser, and redirects can fail. Instead, use Stripe webhooks as the source of truth.

// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import Stripe from "stripe";

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

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!,
    );
  } catch (err) {
    console.error("Webhook signature verification failed");
    return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
  }

  if (event.type === "checkout.session.completed") {
    const session = event.data.object as Stripe.Checkout.Session;
    await handleSuccessfulPayment(session);
  }

  return NextResponse.json({ received: true });
}

async function handleSuccessfulPayment(session: Stripe.Checkout.Session) {
  // Create order in your database
  // Send confirmation email
  // Update inventory
  console.log("Payment successful for session:", session.id);
}

Critical details about webhooks:

  • Always verify the signature — this prevents attackers from sending fake events to your endpoint.
  • Use req.text() to get the raw body — req.json() would parse it and break signature verification.
  • Make the handler idempotent — Stripe may retry webhooks, so your code should handle duplicate events gracefully.

Step 7: Set Up the Webhook in Stripe

For local development, use the Stripe CLI to forward webhooks:

stripe listen --forward-to localhost:3000/api/webhooks/stripe

The CLI will output a webhook signing secret (whsec_...) — use this as your STRIPE_WEBHOOK_SECRET in development.

For production, add the webhook endpoint in the Stripe Dashboard:

  1. Go to Developers → Webhooks
  2. Click "Add endpoint"
  3. Enter your URL: https://yourdomain.com/api/webhooks/stripe
  4. Select events: checkout.session.completed

Step 8: Create the Success Page

After successful payment, show the customer their order confirmation:

// app/order/success/page.tsx
import { stripe } from "@/lib/stripe";

export default async function SuccessPage({
  searchParams,
}: {
  searchParams: Promise<{ session_id: string }>;
}) {
  const { session_id } = await searchParams;
  const session = await stripe.checkout.sessions.retrieve(session_id);

  return (
    <div className="max-w-lg mx-auto py-16 text-center">
      <h1 className="text-2xl font-bold mb-4">Thank you for your order!</h1>
      <p className="text-gray-600">
        A confirmation email has been sent to {session.customer_details?.email}.
      </p>
    </div>
  );
}

Common Patterns and Tips

Adding Product Images

Include images in Stripe Checkout to improve conversion:

price_data: {
  currency: "eur",
  product_data: {
    name: item.name,
    images: [item.imageUrl], // Stripe displays this in checkout
  },
  unit_amount: item.price,
},

Collecting Shipping Addresses

Enable shipping address collection in your checkout session:

const session = await stripe.checkout.sessions.create({
  // ...
  shipping_address_collection: {
    allowed_countries: ["US", "CA", "GB", "DE", "FR"],
  },
});

Applying Discount Codes

Create a coupon in Stripe Dashboard or via the API, then allow customers to enter codes:

const session = await stripe.checkout.sessions.create({
  // ...
  allow_promotion_codes: true,
});

Handling Taxes

Stripe Tax can automatically calculate and collect taxes:

const session = await stripe.checkout.sessions.create({
  // ...
  automatic_tax: { enabled: true },
});

Testing Your Integration

Stripe provides test card numbers for development:

Card NumberResult
4242 4242 4242 4242Successful payment
4000 0000 0000 32203D Secure required
4000 0000 0000 0002Declined

Use any future expiration date and any 3-digit CVC.

Skip the Integration Work

Setting up Stripe Checkout correctly — with webhook handling, error recovery, order creation, and inventory management — involves a lot of moving pieces. If you want a production-ready implementation that's been tested across hundreds of scenarios, check out ShopySeed.

ShopySeed includes a complete Stripe Checkout integration with:

  • Checkout session creation with product data
  • Webhook handling with signature verification
  • Order creation and inventory updates
  • Success and cancellation flows
  • Comprehensive test coverage for all payment scenarios

Get ShopySeed and skip the boilerplate →