Lovable Stripe Works After Deploy but Fails in Preview
Your Stripe button works perfectly once the app is deployed, but in the Lovable preview the checkout fails, the webhook never fires, or you see a test/live key mismatch. The preview sandbox cannot receive Stripe webhooks and often runs different keys than production. Here is why preview cannot complete a Stripe round-trip, and how to test it correctly.
By Founder Name · Last verified: 2026-06-25
Why does Stripe only work after I deploy, not in preview?
The preview is a sandbox, and your Stripe configuration is split across two worlds. Stripe Checkout itself loads fine in preview because that is just a redirect to Stripe's hosted page. What fails is everything that depends on server state: the webhook that confirms the payment, the env-managed secret keys, and the redirect back to a stable URL. Preview and the deployed build run as separate environments, so a flow that needs both halves only completes after deploy.
Think of a Stripe purchase as a round-trip, not a single click. The browser redirects to Checkout (this works in preview), the card is charged at Stripe, and then Stripe must call back to a fixed server URL to tell your app the payment succeeded. That callback — the webhook — is the half that the preview environment cannot receive. So in preview the money can move but the activation never runs, which makes the integration look broken when it is really only half-connected.
There are five recurring reasons a Lovable Stripe flow behaves this way, and only one of them is an actual code bug. The rest are environment and configuration gaps that disappear the moment you point Stripe at a stable deployed URL with the right keys.
| Preview Symptom | Root Cause | Fixable How |
|---|---|---|
| Checkout redirect works, account never upgrades | Webhook is delivered to the production endpoint, not the preview URL | Test via Stripe CLI forwarding, or test on the deployed build |
| Webhook log shows 400 on every delivery | Test/live key and signing-secret mismatch in the environment | Match sk_ and whsec_ to the same Stripe mode |
| Works in preview, fails live (or vice versa) | Different keys in editor sandbox vs deployed host | Set the same mode's keys in the production host and redeploy |
| Stripe cannot reach the endpoint at all | Preview URL is ephemeral and may sit behind auth | Register your stable deployed domain as the endpoint |
| Account upgrades on success page but not reliably | Activation runs client-side on success_url, not in the webhook | Move activation into a signature-verified webhook |
| SubtleCryptoProvider cannot be used in a synchronous context | Synchronous constructEvent used inside a Deno edge function | Switch to constructEventAsync |
Related: if the webhook is configured but still not firing, start here · if the charge clears but the subscription never activates
Why can't the Lovable preview receive a Stripe webhook?
A Stripe webhook is an HTTP POST that Stripe sends to a fixed, public URL on your server after a payment. The Lovable preview is an ephemeral sandbox: its URL changes, it may sit behind auth, and it is not the endpoint you registered in Stripe. So Stripe delivers checkout.session.completed to your production endpoint, never to the preview, and the preview-side database never sees the upgrade. There is no preview webhook to fire.
Stripe only knows about the endpoint URL you registered under Developers, then Webhooks. That URL has to be stable and publicly reachable, because Stripe POSTs to it from its own servers and retries on failure. A preview URL on lovable.app is none of those things — it is generated for an editor session, it can change, and it is not the address Stripe holds. So even a perfectly written handler will never be invoked while you sit in preview.
This is why the flow appears to work the instant you deploy: the deployed build has a stable domain that matches the registered endpoint, so Stripe's callback finally lands somewhere your app is listening. Nothing about your code changed — the delivery target did.
Is a test key vs live key mismatch why my payments fail?
Yes, and this is the most common reason live payments fail after a clean test. Stripe has two parallel modes with separate keys: sk_test_ and a test whsec_ for test mode, sk_live_ and a live whsec_ for live mode. Events from one mode are signed with that mode's secret. If production runs test keys, every real live event fails signature verification with a 400, and nothing activates even though the card was charged.
The trap is that each half of the pair must match the same mode. A common Lovable misconfiguration is a live secret key paired with a test webhook secret, or vice versa — the charge succeeds but the webhook returns 400 because the signing secret belongs to the other mode. The Stripe dashboard webhook log will show the 400, which is your fastest confirmation that this is a mode mismatch and not a code bug.
Match them deliberately: in test mode use sk_test_ together with the signing secret from your test-mode endpoint; in live mode use sk_live_ together with the signing secret from your live-mode endpoint. Set both values in the same environment, then redeploy. Validate test mode first, and only promote to live keys once the test-mode round-trip is clean.
How do I test the Stripe flow correctly without deploying every time?
Use Stripe test-mode keys, point the webhook at your deployed build's stable URL, and use the Stripe CLI to forward events to a local or preview run when you need fast iteration. Preview is for UI; the deployed test environment is for the full charge-to-activation round-trip. Never validate a payment flow with live charges — run Stripe's test card against test keys until the round-trip is clean, then promote to live.
- Set test-mode keys in your environment: sk_test_ for STRIPE_SECRET_KEY and the test endpoint's whsec_ for STRIPE_WEBHOOK_SECRET.
- Register your deployed build's stable domain as the webhook endpoint in Stripe under Developers, then Webhooks — never the lovable.app preview URL.
- Run a real checkout using Stripe test card 4242 4242 4242 4242, any future expiry, and any CVC.
- In Stripe, open Developers, then Webhooks, and confirm the checkout.session.completed delivery returned 200.
- Open your database and confirm the user's plan row actually flipped to paid with the customer and subscription IDs stored.
- Only after a clean test-mode round-trip, swap to sk_live_ and the live endpoint's whsec_ in production and redeploy.
How do I watch the webhook hit my handler without redeploying?
The Stripe CLI lets you forward live webhook deliveries to any local URL, so you can watch checkout.session.completed hit your handler in real time without deploying. Run stripe listen, point it at your function, and trigger a test checkout. It prints each event, the signing secret to use, and the HTTP status your handler returns — turning the invisible preview gap into a live, readable log on your own machine.
The forwarding command looks like this:
# Forward Stripe events to a locally running edge function stripe login stripe listen --forward-to http://localhost:54321/functions/v1/stripe-webhook # In a second terminal, fire a real test event stripe trigger checkout.session.completed
The crucial detail: stripe listen prints its own webhook signing secret, for example whsec_1a2b3c..., and that is the secret your handler must verify against while the CLI is forwarding. It is NOT the same as your dashboard endpoint's whsec_. Set STRIPE_WEBHOOK_SECRET to the value the CLI prints for local testing, then switch back to the dashboard endpoint's secret when you deploy. Mixing these two is a frequent cause of a 400 that looks like a code bug.
Why do my Stripe keys exist in preview but vanish after deploy?
Because the keys and the webhook secret live in environment variables, and preview, the deployed build, and your local machine are three separate environments. A key set in the Lovable editor sandbox does not automatically exist in the deployed host, and a webhook secret printed by the Stripe CLI is different from your dashboard endpoint's secret. This is the Vanishing Env-Var pattern: the value exists in one environment and is silently absent in the next.
When Lovable generates a Stripe integration, the secrets you paste into the editor are scoped to that sandbox. The deployed build reads from its own host environment, and your local CLI run reads from yet another. Each one needs STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET set explicitly. Miss one and the symptom is exactly the confusing split this article describes: it works here, it fails there, and nothing in the code is wrong.
The fix is mechanical but easy to skip: set both Stripe secrets in every environment that runs the handler — the deployed host's settings panel for production, and the CLI's printed secret for local forwarding — then redeploy. After that, verify in the deployed build's logs rather than assuming the preview reflects production.
Related: why env vars disappear when you deploy a Lovable app · keep Stripe secrets out of the browser entirely
When should I hand the Stripe integration to a senior engineer?
Hand it over once the Stripe dashboard shows successful payments but your accounts still do not provision, or once you have spent more than a couple of prompts re-generating the same checkout handler. Payment bugs are the one place where guessing costs real money — every failed activation is a customer who paid and got nothing. A senior engineer traces the event from Stripe delivery to the exact database write.
A specialist confirms the endpoint URL, matches the keys and signing secret to the correct mode, verifies the signature with the async crypto path on Deno, and proves the activation write lands in the right user row — then runs a full test-mode round-trip plus the Stripe CLI to leave you with a readable, reproducible flow. We also resend any failed events so customers who paid during the broken window get provisioned without manual database edits.
Do not iterate on payment code by re-prompting Lovable. Re-prompting a charge handler risks stacking a second silent failure on top of the first — the Bug Doom Loop applied to the one part of your app where a mistake costs real money.
Frequently asked questions
Why does my Stripe checkout work in Lovable preview but the account never upgrades?
Why can't the Lovable preview receive a Stripe webhook at all?
How does a test key vs live key mismatch break my Stripe payments?
How do I test a Stripe webhook without deploying on every change?
Can I test the payment flow with a real card to make sure it works?
Why do my Stripe keys work in preview but disappear after I deploy?
Can I make Stripe fully work inside the Lovable preview itself?
Should I activate the subscription on the success page so it works in preview?
How fast can you fix a Lovable Stripe integration that only works after deploy?
The checkout redirect works but the webhook never fires in preview — what is happening?
Talk to a senior engineer — not a salesperson.
Book a free 30-minute audit call. We'll diagnose what's wrong and tell you exactly what it costs to fix.