How to Add Stripe Checkout to Your Next.js E-Commerce Store
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 stripeYou 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:3000Never 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/stripeThe 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:
- Go to Developers → Webhooks
- Click "Add endpoint"
- Enter your URL:
https://yourdomain.com/api/webhooks/stripe - 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 Number | Result |
|---|---|
4242 4242 4242 4242 | Successful payment |
4000 0000 0000 3220 | 3D Secure required |
4000 0000 0000 0002 | Declined |
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