What it means
Stripe took the money, but your app never found out, so the customer's account still looks unpaid. It's one of the most common complaints about vibe-coded apps, and the fastest way to get refund requests.
Why AI tools cause it
- The upgrade only happens on the "success" page. If the customer closes the tab, or their connection drops, the code never runs. Anyone who finds that URL can also upgrade themselves for free.
- No webhook, or a webhook still pointing at
localhostor a test-mode endpoint. - The webhook can't tell which user paid, because no user id or email was attached to the checkout.
- The webhook crashes on a signature error or a database permission error, and nobody looks.
The fix
- Grant access from a Stripe webhook listening for
checkout.session.completed, but only when the session'spayment_statusispaid. Also listen forcheckout.session.async_payment_succeededto catch slower payment methods like bank debits. For subscriptions, remove access oncustomer.subscription.deleted. Never grant access from the success page alone. - When creating the checkout, pass the user's id as
client_reference_id(or inmetadata) so the webhook knows who to upgrade. - In Stripe, open Workbench → Webhooks (in live mode), click your endpoint, check its URL is your live site, and open Event deliveries. Failed deliveries show the error, and you can click Resend.
- The webhook writes to your database as a server, so use a server-side key there (for example Supabase's secret / service_role key), not the user's session. Keep that key only in server environment variables, never with a
NEXT_PUBLIC_orVITE_prefix. - For anyone already stuck, upgrade them by hand and send a short apology.
Paste this into your AI
Customers pay with Stripe but sometimes don't get access. Move the "grant access" logic into a Stripe webhook for checkout.session.completed (only when payment_status is paid) and checkout.session.async_payment_succeeded that verifies the signature, finds the user from client_reference_id, and updates their account. Make it safe to receive the same event twice. The success page should only show a message, never grant access.
Still broken?
If the webhook shows "No signatures found", see our fix for that error. If it shows a 307 or 308 redirect, your endpoint URL is missing or adding www or a trailing slash. Copy the exact final URL into Stripe.