Home / Fixes / Stripe webhook

If anyone can POST checkout.session.completed, they are paid

Stripe signs every webhook. If your handler never checks the signature — or your React app calls the webhook itself — you are not taking payments. You are taking suggestions.

Symptoms

Why Lovable / Bolt / v0 do this

The generator knows checkout needs a success path. Calling your own webhook from the browser is fewer moving parts than a raw-body server route, a signing secret, and a dashboard URL. It works in preview because you are the only user and you are honest. In production it is a button that says “set is_paid = true.”

The other failure: a real webhook route that JSON-parses the body first. constructEvent then throws every time, so someone comments it out to “unblock launch.”

The actual fix

Stripe’s servers POST to you. Your browser does not.

import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)

export const config = { api: { bodyParser: false } } // Pages router

export async function POST(req: Request) {
  const sig = req.headers.get('stripe-signature')
  const raw = Buffer.from(await req.arrayBuffer())
  let event
  try {
    event = stripe.webhooks.constructEvent(
      raw,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    )
  } catch (err) {
    return new Response('bad signature', { status: 400 })
  }
  if (event.type === 'checkout.session.completed') {
    const session = event.data.object
    // mark paid by session.id / client_reference_id — server only
  }
  return Response.json({ received: true })
}
  1. Put STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in Vercel server env. Not VITE_.
  2. Stripe dashboard → Webhooks → add https://yourdomain.com/api/webhook → copy the whsec_.
  3. Checkout success_url can land on a “thanks, polling…” page. That page reads the order row from your API, which reads the database the webhook updated.
  4. Delete any fetch('/api/webhook') from src/.
Test mode and live mode have different signing secrets. A Production Vercel env pointed at the test whsec_ will 400 forever, which is safer than a handler with no check, and looks like an outage. Match the keys to the mode.

A copy-paste stub is in the kit at stripe/webhook.ts. The $297 triage is this issue, end to end, including the dashboard clicks.

$49 to run this against your tree

Zip or GitHub URL in. Ranked HTML/PDF in 24h. No call. First 10 audits are $49, credited if you continue to triage. Email contact@shipready.local.

Start an audit See a sample report