Every tutorial shows you the happy path: create a Checkout Session, redirect, done. Nobody shows you what happens when a webhook fails silently, a payment succeeds but your database doesn't update, or a customer refreshes the page mid-checkout.
I built an e-commerce store for a client selling handmade wire bouquets. Here's what the tutorials skip.
The basic setup works great
Stripe Checkout is genuinely well-designed. Creating a session is straightforward:
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
line_items: cartItems.map((item) => ({
price_data: {
currency: "usd",
product_data: { name: item.name },
unit_amount: item.price * 100, // Stripe uses cents
},
quantity: item.quantity,
})),
mode: "payment",
success_url: `${origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/cart`,
});This works. The checkout page looks professional, handles cards, Apple Pay, Google Pay. But this is where the tutorials stop and the real problems begin.
Webhook handling, where things go wrong
The success_url redirect is for the user. The actual order confirmation must come from webhooks. Here's why: users can close the browser tab before the redirect. They can lose internet. The redirect is not reliable.
// This runs on your server, not the browser
export async function POST(req: Request) {
const body = await req.text();
const sig = req.headers.get("stripe-signature")!;
// ALWAYS verify the signature
const event = stripe.webhooks.constructEvent(
body, sig, process.env.STRIPE_WEBHOOK_SECRET!
);
if (event.type === "checkout.session.completed") {
const session = event.data.object;
// Fulfill the order here
await fulfillOrder(session);
}
return new Response(null, { status: 200 });
}Critical: Always return a 200 response quickly. Stripe retries webhooks that don't get a 200, which means you might process the same order twice. Use idempotency keys, check if you've already processed this event ID before fulfilling.
The inventory race condition
Here's a fun one: two people buy the same last item at the same time. Both checkout sessions succeed. Both webhooks fire. You now have two orders for one item.
Solutions:
- Reserve inventory when creating the Checkout Session (set a 30-minute TTL)
- Check inventory again in the webhook handler before confirming
- Use database transactions with row-level locking
Failed payment recovery
Cards get declined. Checkout sessions expire after 24 hours. You need a strategy:
- Store the cart in your database with the session ID
- If the session expires, email the customer with a new checkout link
- Track abandoned checkouts, they're a huge revenue opportunity
The order confirmation email
Stripe doesn't send order emails. You need to:
- Listen for
checkout.session.completed - Generate a nice HTML email with order details
- Send it via your email provider (I use Nodemailer)
Don't skip this. Customers expect confirmation. If they don't get one, they'll think the payment failed and try again.
Security checklist
- Webhook signature verification, never skip this
- Idempotency on order creation, prevent duplicates from retries
- Rate limit your checkout endpoint, prevent abuse
- Never trust client-side prices, always calculate server-side
- Store Stripe IDs, not amounts, use Stripe as the source of truth
That last one is important. Don't store $49.99 in your database. Store the Stripe Price ID and look up the amount from Stripe. This prevents price manipulation.
The parts that took longer than expected
- Shipping integration, calculating rates, tracking numbers, updating order status
- Inventory sync, keeping stock counts accurate across webhooks and admin edits
- Image optimization, product photos are huge. Use Next.js
<Image>with proper sizing. - SEO for products, each product page needs its own metadata, structured data, and OG image
The Stripe integration itself took two days. The rest of the store took three weeks.
Want an e-commerce site that works?
I build online stores with Next.js and Stripe, properly, with webhooks, inventory management, and email confirmations from day one. Let's talk.