DEV Community

Cover image for Customer paid in your Lovable or Bolt app but nothing unlocked? Where to look first
WebhookProof
WebhookProof

Posted on AI-assisted

Customer paid in your Lovable or Bolt app but nothing unlocked? Where to look first

A customer writes in: they paid, and they still can't get in. Stripe shows a successful payment. Your app shows a locked screen.

In apps built with Lovable or Bolt, the fix depends on how the builder connected Stripe, because the two take different routes. This post covers how each one decides a customer has paid, where that breaks, and what to paste back into the builder. Every claim links to the doc it comes from (see Sources at the end).

Step 0: find out which setup you have

Lovable offers two routes. Its docs say "most projects should use built-in payments", where "Lovable provisions the Stripe or Paddle account and manages keys, webhooks, and test and live environments for you." The other route is "Connect your own Stripe account", where you supply the API key. A project "can use one or the other, not both at the same time."

Bolt has a single Stripe integration that "requires Supabase as your database provider". When you apply it, Bolt "automatically creates the Supabase edge functions to handle" checkout session creation, "payment webhook processing", and subscription management.

If you're on Lovable built-in payments, Lovable runs the webhook side, so start in its Payments view. The rest of this post is about the setups where the payment wiring sits in your project: Lovable with your own Stripe account, and Bolt.

Lovable with your own Stripe account: no webhooks by default

Lovable's docs are explicit: "By default, the integration does not use webhooks: your app checks payment and subscription status directly with Stripe."

That gives two common failure modes.

1. One-time purchases granted only on the success page

For a one-time purchase, the buyer pays in Stripe Checkout and then "returns to a payment success page in your app". If the code that grants access only runs when that page loads, a buyer who closes the tab never gets access.

Stripe warns about exactly this: "You can't rely on triggering fulfillment only from your checkout landing page, because it's not guaranteed customers visit that page. For example, a customer can pay successfully and then lose their internet connection before your landing page loads." Stripe's fix is a webhook that calls the same fulfillment function, with the landing page as a second, faster path.

Lovable supports that when you ask for it. It "writes the webhook function, and you finish the setup in your Stripe dashboard": create an event destination for the function's URL, choose the events, and add the signing secret when Lovable asks.

Prompt to paste into Lovable:

Add a Stripe webhook function that handles checkout.session.completed and checkout.session.async_payment_succeeded, verifies the Stripe-Signature header against the signing secret, and grants access through the same function the success page uses. Make that function safe to run twice for the same Checkout Session ID.

2. Subscriptions matched by email

For subscriptions, "subscriptions are matched to users by email address. A user's subscription is recognized when the email they sign in with matches the email on their Stripe customer record." If a customer pays as jane@work.com and logs in as jane@gmail.com, the check finds no subscription.

Check: in Stripe, open the customer record and compare its email with the one the user signs in with. If they differ, change one of them, or ask Lovable to link the Stripe customer ID to the user at checkout rather than relying on the email match.

Bolt: the webhook function exists, so check that it runs

Bolt's docs say it "automatically handles webhook endpoint registration and signature verification. You don't need to manually configure webhook endpoints in your Stripe dashboard." With the plumbing generated for you, the useful question is whether events are actually arriving and succeeding.

Check: in Stripe, open Workbench → Webhooks, select the endpoint, and open Event deliveries. Stripe's docs say this tab lists each event as Delivered, Pending or Failed, along with "the HTTP status code of the delivery attempt".

Then work from the status code.

Reading the status code (both builders)

These apply whenever your webhook is a Supabase Edge Function, which is how Bolt builds it, and how Lovable builds it on your own Supabase project.

401 with "Missing authorization header" or "Invalid Token...": Supabase's troubleshooting guide says these messages "come from the legacy auth verification check", which "runs before your code. When it fails, your function never executes." Stripe sends no Supabase token, so the platform rejects the event. Fix: turn off JWT verification for the webhook function only, then verify the Stripe signature inside it.

The Stripe webhook Edge Function returns 401 before it runs. Set verify_jwt = false for that function only in supabase/config.toml, keep Stripe signature verification with constructEventAsync on the raw body (await req.text()), and redeploy.

400 with a signature error: the function runs but the signature doesn't match. The most common cause, per Stripe, is "using the wrong endpoint secret", for example a test secret on the live endpoint (next section).

200, but still no access: the event arrived and your function returned success without granting access. Check that the endpoint subscribes to the event your code handles (checkout.session.completed for one-time payments), and look in the function logs for the database write.

No delivery at all: the live-mode endpoint may not exist, or may not subscribe to that event type.

Test vs live: the go-live trap

Both builders tell you to start with test keys. Bolt: "Use test keys for development. Test keys start with sk_test_." Stripe's docs explain why switching modes takes more than a new key: "Each mode has its own set of API keys, and objects in one mode aren't accessible to the other." Webhook secrets differ too: "If you use the same endpoint for both test and live API keys, the secret is different for each one."

Lovable's go-live steps cover the price side: "Price IDs differ between test and live mode, so ask Lovable to switch your app to the live price IDs." If you added a webhook, also paste in the live endpoint's whsec_ secret. The test one won't verify live events.

Fixing customers who already paid

Once the endpoint works, recover the missed events. Stripe's docs say a Dashboard Resend works "for up to 15 days after the event creation", and the CLI's stripe events resend works for 30. In live mode Stripe also retries failed events "for up to three days". Make sure the grant is safe to run twice before you resend, because Stripe says your fulfillment function "might be called multiple times, possibly concurrently, for the same Checkout Session."

Checklist

  • [ ] Know which route you're on: Lovable built-in, Lovable with your own Stripe, or Bolt
  • [ ] Access is granted from a webhook, not only from the success page
  • [ ] Subscriptions: the login email matches the Stripe customer email (or you match on customer ID)
  • [ ] Event deliveries show 200, not 401 or 400
  • [ ] Live mode: live key, live price IDs, live endpoint, live whsec_
  • [ ] Granting access twice for the same Checkout Session does nothing extra

WebhookProof's free checker sends 3 harmless requests to your webhook URL and tells you whether Stripe can reach it (401s, redirects, login walls) and whether it accepts unsigned events. Each issue comes with a prompt to paste into your builder. More detail in the guide: Stripe webhooks with Supabase, Lovable and Bolt.

WebhookProof is an independent tool, not affiliated with Stripe, Inc., Lovable, Bolt or Supabase, Inc. Quotes are from the linked documentation as read on 2026-10-18.

Sources

Top comments (0)