Vibe Code Stripe Checkout (And Get the Webhook Right)

Add payments to an AI-built app — the prompts that work, and the webhook mistake that lets people pay without getting access.

C
CodeIllusion Team
#vibe-coding #stripe #payments #saas #tutorial
Vibe Code Stripe Checkout (And Get the Webhook Right)

Payments are the one part of a vibe-coded app where being wrong costs money in both directions — customers who pay and get nothing, or get everything without paying.

The good news: Stripe Checkout is extremely well documented, so AI reproduces it accurately. The bad news is entirely in the webhook, and that’s where every real failure lives.

Read this part of your code. It’s about eighty lines.

The Model

Three pieces:

  1. Checkout Session — you create one server-side, redirect the customer to Stripe, they pay on Stripe’s page. You never touch card details.
  2. Webhook — Stripe calls your server to say what happened. This is the source of truth.
  3. Your database — records what each user is entitled to.

The rule that prevents almost every problem: grant access in the webhook, never on the redirect.

Checkout

Add Stripe Checkout for a subscription with monthly and annual tiers. Create the Checkout Session in a server-side route. Pass the authenticated user’s ID as client_reference_id and store their Stripe customer ID on their user record. Redirect to /success on completion and /pricing on cancel. Use the official stripe Node library.

client_reference_id matters — it’s how the webhook knows which user paid. Without it you’re guessing from email addresses, which breaks the moment someone pays with a different one.

The Webhook — The Part That Goes Wrong

Here’s what AI typically writes:

export async function POST(req: Request) {
  const body = await req.json()          // ← no signature verification
  if (body.type === 'checkout.session.completed') {
    await grantAccess(body.data.object.client_reference_id)
  }
  return new Response('ok')
}

Two serious problems.

No signature verification. Anyone who finds this URL can POST fake JSON and grant themselves a subscription. It’s an unauthenticated endpoint that hands out paid access.

No idempotency. Stripe retries webhooks. The same event will arrive more than once, and this code processes it every time.

The correct version:

Rewrite the Stripe webhook properly. Verify the signature with stripe.webhooks.constructEvent using the raw request body and STRIPE_WEBHOOK_SECRET — do not parse JSON before verifying. Return 400 on verification failure. Store processed event IDs and skip duplicates. Handle checkout.session.completed, customer.subscription.updated, and customer.subscription.deleted. Return 200 quickly and do slow work after.

The raw body detail is not pedantry. Signature verification hashes the exact bytes Stripe sent. If your framework parses JSON first, verification fails even when the request is genuine — and the usual “fix” is to disable verification.

In Next.js App Router:

const body = await req.text()                     // raw, not .json()
const sig = req.headers.get('stripe-signature')!
const event = stripe.webhooks.constructEvent(
  body, sig, process.env.STRIPE_WEBHOOK_SECRET!
)

Never Trust the Redirect

This is the mistake that actually loses money:

// app/success/page.tsx  — WRONG
await grantAccess(userId)   // runs whenever someone loads /success

Anyone can visit /success. And a customer who closes the tab after paying never loads it — they paid and got nothing.

The success page confirms; the webhook grants.

The success page must only read subscription status from our database. It must never grant access. If the webhook hasn’t arrived yet, show a brief pending state that polls.

Subscriptions Aren’t One Event

A subscription has a lifecycle, and AI implementations usually handle only the first step.

EventWhat it means
checkout.session.completedFirst payment succeeded — grant access
customer.subscription.updatedPlan changed, or renewed — sync status
customer.subscription.deletedCancelled — revoke at period end
invoice.payment_failedCard declined — warn, start dunning

Handle the full subscription lifecycle. Store status, current_period_end, and plan on the user record. On deletion, revoke access at period end rather than immediately — they paid for the remainder.

Testing

Install the Stripe CLI and forward webhooks locally:

stripe listen --forward-to localhost:3000/api/webhooks/stripe

It prints a signing secret for local use. Then:

stripe trigger checkout.session.completed

Test cards: 4242 4242 4242 4242 succeeds, 4000 0000 0000 0341 attaches then fails on charge, 4000 0025 0000 3155 requires 3D Secure.

Walk the whole path before going live: subscribe, confirm access, cancel, confirm access persists to period end, confirm it’s revoked after.

Before You Go Live

  • Webhook verifies signatures using the raw body
  • Duplicate events skipped via stored event IDs
  • Access granted only in the webhook, never on redirect
  • Success page reads state, doesn’t grant it
  • All four lifecycle events handled
  • Cancellation revokes at period end
  • Failed payments handled
  • Live webhook endpoint registered in the Stripe dashboard
  • Live keys in production env vars, never NEXT_PUBLIC_
  • Secret key not in Git history
  • Amounts in the smallest currency unit (cents), verified server-side
  • Prices read from Stripe, not from client input

That last one matters: if the client sends the price, someone will send a different one.

git log -p | grep -E "sk_live|sk_test|whsec_" | head

Should return nothing. If it doesn’t, roll the keys.

The Rule

Vibe code the pricing page, the upgrade flow, the billing UI, the emails. Read the webhook.

It’s the smallest file in your payment integration and the only one where a mistake is both silent and expensive.

Next: Vibe code a pricing page for the front end of this, or vibe code a SaaS MVP to see where payments fit in the whole build.

Related reading:

Frequently Asked Questions

Can AI integrate Stripe correctly? +

It gets Checkout Sessions right almost always — that part is well documented and heavily repeated. Webhooks are where it fails: unverified signatures, granting access on the wrong event, and no idempotency handling are all common.

What is the most common Stripe mistake in AI-generated code? +

Granting access on the checkout redirect instead of the webhook. The redirect can be faked by visiting the success URL directly, and it never fires for users who close the tab after paying. Access must be granted by a signature-verified webhook.

Do I need a webhook for Stripe? +

Yes, for anything beyond a one-off payment you manually fulfil. Subscriptions, renewals, cancellations, and failed payments only reach your app through webhooks. Without one, your access state drifts out of sync with reality.

How do I test Stripe payments safely? +

Use test mode and the Stripe CLI to forward webhooks to localhost. Card 4242 4242 4242 4242 succeeds; 4000 0000 0000 0341 fails after attaching. Test the full lifecycle including cancellation and renewal before going live.

Tagged:

#vibe-coding #stripe #payments #saas #tutorial

Enjoyed this article?

Get more AI tool picks, coding tutorials, and no-code automation guides every week. No spam, ever.

Found this useful? Share it:

More in AI Coding Tools