Stripe is the gold standard for taking payments online — but a sloppy integration leaks revenue and trust. This guide covers the right way to wire up Stripe: hosted Checkout for safety, webhooks as the only source of truth, and the patterns we use on every project that handles money.
1. The mental model
The single most important Stripe concept: the browser redirect after payment is a convenience, not proof. The real, trustworthy signal that money moved is the webhook Stripe sends to your server. Build around that and you'll never grant access to someone who didn't pay — or deny it to someone who did.
2. Create a Checkout Session
Hosted Checkout means Stripe handles the card form, 3D Secure, wallets and PCI compliance. You never touch raw card numbers — a huge reduction in security scope.
import Stripe from "stripe";const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); export async function POST(req: Request) { const { priceId, orgId } = await req.json(); const session = await stripe.checkout.sessions.create({ mode: "subscription", line_items: [{ price: priceId, quantity: 1 }], metadata: { orgId, priceId }, success_url: process.env.BASE_URL + "/billing?ok=1", cancel_url: process.env.BASE_URL + "/pricing", }); return Response.json({ url: session.url });}3. Handle the webhook (the source of truth)
Always verify the signature — it proves the event really came from Stripe and wasn't forged. Then update your database based on the event type.
export async function POST(req: Request) { const sig = req.headers.get("stripe-signature")!; let event: Stripe.Event; try { event = stripe.webhooks.constructEvent( await req.text(), sig, process.env.STRIPE_WEBHOOK_SECRET! ); } catch { return new Response("Bad signature", { status: 400 }); } switch (event.type) { case "checkout.session.completed": case "customer.subscription.updated": await syncEntitlements(event); // grant/adjust access break; case "customer.subscription.deleted": await revokeEntitlements(event); // downgrade break; } return Response.json({ received: true });}4. Test the whole flow locally
# install + log in oncestripe login # forward webhooks to your local routestripe listen --forward-to localhost:3000/api/stripe/webhook # trigger a test eventstripe trigger checkout.session.completed5. Production checklist
- 1Use restricted API keys, never the secret key on the client
- 2Verify every webhook signature
- 3Make webhook handlers idempotent
- 4Store the Stripe customer & subscription ids on your records
- 5Handle failed payments and dunning (retries)
- 6Reconcile entitlements from the webhook, not the redirect
Why it matters
- Estimated timeline
- Basic Stripe in 3–5 days
- Best for
- SaaS, e-commerce, marketplaces
- Related service
- API Integrations
Need this built? Explore our API Integrations service.
View service →Written by WeBuildCrew Team · Published 12 April 2026 · 10 min read