Store › Next.js + Stripe Starter
A working payment path: Checkout, webhook signature verification, event idempotency, order state machine, email delivery. 17 files, no abstraction layer — you can read all of it before lunch.
By purchasing you agree to the Terms of Sale. These terms cover this store's digital goods only.
A tutorial tells you how to write it. This gives you the part that's already correct, so you only change the product and the delivery. Build and startup verified on Next.js 15.5 + Node 22 — not "should work".
req.json()
Stripe signs the raw bytes. Re-serialize them — even just key ordering — and
constructEvent throws No signatures found matching the expected signature.
The fix is await req.text().
Stripe delivery is at-least-once. A response slower than 20 seconds, any non-2xx, or a network blip triggers redelivery — for up to 3 days. Without deduplication you ship the same order twice and send the same email twice.
Event dedup alone isn't enough. Different events (checkout.session.completed and
async_payment_succeeded) can point at the same session. Delivery needs an atomic
claim on order state: only paid can become delivered.
Return 200 after an exception and Stripe considers it delivered — no retry, order stuck at
"paid but never delivered" forever. Return 500 so it retries, and mark the order
failed so a human can pick it up.
stripe-starter/ ├── app/ │ ├── page.tsx product page │ ├── buy-button.tsx client component │ ├── success/page.tsx polls delivery status after redirect │ ├── cancel/page.tsx │ └── api/ │ ├── checkout/route.ts creates the Checkout Session │ └── webhook/route.ts verifies and processes events ├── lib/ │ ├── stripe.ts client + product config │ ├── orders.ts storage, idempotency, state machine │ └── email.ts delivery email ├── .env.example └── package.json
PRODUCT_NAME=My Template PRODUCT_PRICE_CENTS=4900 # cents, so 4900 = $49.00 PRODUCT_CURRENCY=usd
/api/checkoutNEXT_PUBLIC_SITE_URL at your domainline_items for moremode: "payment")charge.refunded)stripe listen --forward-to localhost:3000/api/webhook, paste the whsec_... into env, restart.Full terms: Terms of Sale.