Guides
How to Integrate Stripe With Next.js
This covers the two core pieces of a Stripe integration: creating a Checkout session from a Route Handler, and handling the webhook that confirms what actually happened with the payment.
Prerequisites
- arrow_rightA Stripe account with API keys
- arrow_rightThe stripe npm package installed
- arrow_rightA Next.js App Router project
Step 1: Create a Checkout Session
app/api/checkout/route.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const { priceId } = await request.json();
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_URL}/success`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/cancel`,
});
return Response.json({ url: session.url });
}Step 2: Handle Webhooks
app/api/webhooks/stripe/route.ts
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get('stripe-signature')!;
const event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
if (event.type === 'checkout.session.completed') {
// fulfill the order in your database
}
return new Response('ok');
}The webhook, not the success_url redirect, is the reliable source of truth for whether a payment actually completed — a customer could close the tab before the redirect fires, but the webhook still arrives.
Step 3: Test With the Stripe CLI
stripe listen --forward-to localhost:3000/api/webhooks/stripeThis forwards real Stripe events to your local dev server so you can test the webhook handler without deploying first.
Common Errors
- arrow_right"Webhook signature verification failed" — the raw request body must be read as text before parsing; parsing it as JSON first breaks signature verification
- arrow_rightOrder marked complete before payment actually confirmed — relying on the success_url redirect instead of the webhook as the source of truth
- arrow_rightDuplicate order fulfillment — webhooks can be delivered more than once; use the event ID or an idempotency check before processing
Security Considerations
- arrow_rightAlways verify the webhook signature — without it, anyone could POST a fake 'payment succeeded' event to your endpoint
- arrow_rightNever expose the Stripe secret key to the client — only the publishable key belongs in frontend code
- arrow_rightStore the webhook secret in environment variables, separate per environment (test vs. live)
Frequently Asked Questions
Should I use Stripe Checkout or Payment Elements?add
Checkout (Stripe's hosted page) is faster to implement and reduces PCI compliance scope. Elements gives full control over the payment form's appearance if it needs to stay fully on your domain.
Why do webhooks matter more than the redirect?add
The redirect only fires if the customer's browser successfully completes it; the webhook is delivered by Stripe directly to your server regardless, making it the reliable signal for whether payment actually succeeded.
How do I test payments without real money?add
Stripe's test mode provides test card numbers (like 4242 4242 4242 4242) that simulate successful and failed payments without moving real money.