UnbreakMyApp / Fix guides

Stripe payment succeeds, but the paid feature never unlocks

The customer was charged but their account was not upgraded. Usually a webhook problem. Here is how to trace and fix it.

Why it happens

Stripe Checkout takes the payment, but your app only learns about it from a webhook: Stripe calls a URL on your backend (often a Supabase Edge Function) with an event like checkout.session.completed. Your code then marks the user as paid.

Common breaks: the webhook endpoint was never added in Stripe (or only in test mode), the signing secret is missing or wrong, signature verification fails because the body was parsed before verifying, the edge function rejects Stripe because it requires a Supabase login token, or the function cannot find which user paid.

Some apps also try to unlock the feature from the "success" page in the browser. That is unreliable and unsafe.

How to fix it, step by step

  1. In Stripe Dashboard → Developers → Webhooks (Workbench), open your endpoint and check recent deliveries. A failed status code (400, 401, 500) tells you where it breaks. If there is no endpoint, add one pointing at your function URL.
  2. Make sure you are in the right mode: test and live mode have separate endpoints, keys and signing secrets.
  3. Copy the endpoint signing secret (whsec_...) into your backend secrets (e.g., Supabase → Edge Functions → Secrets as STRIPE_WEBHOOK_SECRET).
  4. In the function, verify the signature using the raw request body (await req.text()). In Deno/Supabase use stripe.webhooks.constructEventAsync.
  5. Supabase Edge Functions require a JWT by default. Stripe does not send one, so turn off JWT verification for the webhook function only (verify_jwt = false in config, or deploy with --no-verify-jwt). The Stripe signature check is your security.
  6. Pass the user id when creating the Checkout Session (client_reference_id or metadata) and use it in the webhook to update the right row, using the service role key on the server only.
  7. Resend a failed event from the Stripe dashboard and confirm the user is upgraded.

Paste-ready prompt for Lovable

Users pay with Stripe but their account is not upgraded. Fix the Stripe webhook flow:
1. When creating the Checkout Session, set client_reference_id (and metadata.user_id) to the logged-in user's id.
2. Create or fix a Supabase Edge Function "stripe-webhook" that reads the raw body with await req.text(), verifies it with stripe.webhooks.constructEventAsync using STRIPE_WEBHOOK_SECRET, and handles checkout.session.completed (plus customer.subscription.updated/deleted if we use subscriptions).
3. On success, update the user's plan/paid status in the database using the service role key on the server only.
4. Configure this function with JWT verification disabled (verify_jwt = false), because Stripe does not send a Supabase token.
5. Return 200 quickly and log errors.
Do not unlock features from the success page. List the secrets I need to add.

FAQ

Why not just unlock on the success page?

Anyone can open that URL without paying, and users who close the tab early never get unlocked. The webhook is the source of truth.

Do I need to refund people who were not unlocked?

Usually you just unlock them. Resend the failed webhook events from Stripe after the fix, or upgrade them manually.

Still broken?

Get a free diagnosis by email within 24 hours. Fixed or refunded.

Get a free diagnosis

Or join the waitlist for updates:

Sources