The short version
A customer paid and the app still says free. Someone cancelled in Stripe last week and still has access. Checkout worked perfectly with the 4242 test card and does nothing at all with a real one. Or the webhook function has been answering 401 since the day you published. None of those is a Lovable bug. Each one is the same missing piece: a Lovable app cannot confirm a payment by itself. Something on a server has to hear from Stripe or Paddle and write to your database, and every failure on this page is that listener missing, unverified, pointed at the wrong mode, or trusted in the wrong order.
Here is the short version. The app does not need rebuilding. Payments in a Lovable app are one product decision, one secret in the right place, one mode cutover, and one webhook handler that does four boring things correctly. All of it can be fixed in place on a live app, without a single customer noticing. This page goes through it in the order that stops money leaking first.
One thing before the detail. If you sell one digital product, have under a hundred customers, pay nobody out, and are still on Lovable Cloud, read the section on who does not need to touch any of this and probably stop there. We would rather say that now than three sections in.
Does Lovable have its own payments product, or do you still need Stripe?
It has one, since April 2026, and it is built on Stripe or Paddle underneath. So you still need a payment provider. What changed is who sets it up and who the merchant is.
Lovable Payments is the built-in feature. You ask for payments in chat, Lovable creates a Paddle or Stripe account for you, wires the checkout, the customer portal, the database tables and the webhook endpoints, and hands you a Payments tab with revenue in it. Lovable's own documentation now marks its older Stripe integration guide as deprecated and says most projects should use the built-in feature instead. That older path still exists and still matters, because a lot of live Lovable apps were wired up on it, and because the built-in feature has one requirement that rules out most apps that have already been hardened. More on that in the next section.
The two providers are not interchangeable. Paddle is a merchant of record: Paddle takes on the legal and operational responsibility for the transaction, and handles VAT, sales tax and GST in the countries it sells into. Stripe, by default, makes you the merchant. Stripe's own merchant-of-record product is called Managed Payments, it covers digital products only, and under Lovable it applies per transaction rather than automatically. If you have ever wondered why a founder in Riyadh or Singapore picks Paddle for a small SaaS, that sentence is the whole reason. Registering for tax in every country you sell into is not a feature you want to build.
One disambiguation, because it shows up in the search suggestions. Stripe Atlas is a company incorporation service. It has nothing to do with Lovable Payments, and you do not need it to take a payment.
Underneath either path is the same shape. Your app, in the browser, never touches a secret and never decides that money moved. A function running on Supabase, which is what Lovable Cloud is, holds the secret key, creates the checkout, and receives an event from the provider when the payment actually happens. Our Lovable Cloud vs Supabase guide covers who owns that backend. Keep the shape in mind, because every section below is about one of its four parts going wrong.
What does Lovable Payments decide for you, and which of it can you undo?
It decides more than the dialog lets on. Most of it is right for a small digital product. Two of the decisions you cannot undo, and one of them is made before you ever see the dialog.
The one made in advance: the built-in feature needs Lovable Cloud. Lovable's docs say built-in payments use the built-in backend for webhooks and subscription data and are not available for projects connected to your own Supabase project. If you followed a hardening guide, including ours, and moved the app onto a Supabase project you own, the built-in feature is simply not offered to you. You are on the manual Stripe path whether the docs call it deprecated or not. That is the fact the launch coverage never mentions, and it is the reason this page covers both paths.
The ones you make by accepting the defaults. Lovable creates the provider account, so on Paddle you complete their verification and domain review before selling, and on Stripe you click a link in the Payments tab to claim the account Lovable created, or link one you already have. Products and prices you create in test sync to live when you publish, discount codes do not, so a launch coupon that worked in preview has to be recreated by hand. One active subscription per user per environment by default. Lovable registers two webhook endpoints per environment on the provider account, one that feeds your app and one that sends Lovable a copy so it can watch delivery, and the docs are blunt that you must not delete or disable either of them. Test checkout accepts only test cards. A Pro plan or higher.
The ones you cannot undo. A project with payments enabled cannot be remixed, so the fork button goes away. And disconnecting is permanent: Lovable deletes your API keys, webhook secrets, environment variables and integration data from the project. A project runs one provider at a time. So a founder who starts on Paddle, grows into a marketplace and needs Stripe Connect is not flipping a switch. They are running two providers side by side while the old subscriptions lapse.
Whether any of this is a problem depends entirely on what you sell. For one digital product to consumers, Paddle as merchant of record is the best default a solo founder can get. For a company that already has a Stripe account with revenue in it, link that account and keep the history. For anything that pays a third party, read the Connect section before you accept the dialog.
Why did a customer pay and the app still say free?
Because the app upgraded them on the success page, and the customer never reached the success page.
This is the single most common payments bug we see in AI-built apps and it is not the AI's fault. It is the obvious way to write it. Stripe redirects the customer to your success URL, the page reads the session id, the page flips a flag in the database. Stripe's own fulfilment guide says exactly why that is not enough: you cannot rely on triggering fulfilment only from your checkout landing page, because it is not guaranteed customers visit that page. They pay, their phone locks, the tab dies, the train goes into a tunnel. Stripe has the money. Your database has nothing. For subscriptions and for any payment method that confirms later, Stripe says webhooks are required, not recommended.
Lovable's deprecated Stripe guide makes this worse in a quiet way. It says webhooks are opt-in and that Lovable relies on edge-function polling unless you ask for them. Polling means the app asks Stripe on page load whether this user has paid. It works right up until the customer who paid never loads the page again, or the poll fails for a second, or two tabs poll at once. Under the built-in Payments feature Lovable registers the webhook for you. Under the manual path, if you never asked for one, you do not have one.
The fix is a webhook handler that does four things and nothing clever. Verify the request came from Stripe. Return a success status immediately, before any real work, because Stripe times out slow handlers and retries. Fulfil once per Checkout Session, by retrieving the session, checking its payment status is not unpaid, and recording that this session id has been handled, because Stripe's guide says the same function might be called multiple times, possibly concurrently, for the same session. Then act on the events that matter: checkout session completed, and for delayed methods, async payment succeeded or failed. Keep the success page too. Stripe waits up to 10 seconds for your handler before redirecting, so in the normal case the page already shows the upgrade. The page is the nice path. The webhook is the truth.
What it costs to leave it. A customer who paid and got nothing is a support ticket at best, and at worst a dispute, which is the card issuer reversing the payment and debiting your balance for the amount plus a network fee while you gather evidence. You will not see the error, because there is no error. There is a customer who stops replying.
Who does not need to touch any of this?
More people than the payments vendors would like.
If you sell one digital product, take subscriptions or one-time payments from customers who are all in the same app, pay nobody out, have fewer than a hundred paying users, and your app still runs on Lovable Cloud, use Lovable Payments and pick Paddle. Run the readiness check, which looks for a real privacy policy, terms and a refund policy on the live site. Complete Paddle's verification. Then go back to building the product, because the merchant of record just took tax, invoicing, receipts, fraud and chargebacks off your desk, and there is nothing on this page that makes that trade worse.
Do three checks first, all of them in the provider dashboard rather than in Lovable. Both webhook endpoints Lovable registered are present and enabled. Your live products and prices exist, not just the test ones. And your discount codes exist in live, because those did not sync.
Come back when one of five things happens. You connect your own Supabase project. You need to pay a third party, a seller, a coach, a driver, anything. A customer needs two subscriptions at once. Someone reports they paid and were not upgraded, or cancelled and were not downgraded. Or you have a Stripe account with history in it and Lovable's default would leave that history behind. Those are the triggers for the rest of this page, and until one fires the rest is premature.
Why does checkout work with 4242 and do nothing with a real card?
Because test mode and live mode are two different Stripe accounts wearing the same login, and the thing you tested does not exist in the one you published.
Stripe's keys page states it plainly: each mode has its own set of API keys, and objects in one mode are not accessible to the other. A product, a price, a coupon created while you were testing with the 4242 card lives in the sandbox. Swap the keys to live and your checkout code asks Stripe for a price id that, in live mode, has never been created. The call fails, the app shows a button that does nothing, and the customer with a real card leaves. Stripe's go-live checklist has a line for this: objects created in a sandbox are not usable in live mode, and when you recreate them you should reuse the same ids so the code keeps working.
The webhook has the same split. If you registered one endpoint URL and used it in both modes, Stripe's docs say the signing secret is different for each. Register the live endpoint separately and store its own secret, or every live event fails verification and your handler correctly rejects your own customers' payments. Under the built-in feature Lovable manages the two environments and syncs products and prices when you publish, which is exactly the problem it exists to remove. Discounts still do not sync, and Lovable's older Stripe guide notes that the integration does not run in preview at all, so the first time the manual path is really exercised is after publishing.
The cutover, in order, for the manual path. Recreate products and prices in live with the same ids. Register the live webhook endpoint and copy its secret into the function's secrets. Swap the publishable key in the browser to the live one, which is the only key allowed there. Swap the secret or restricted key in the function's secrets to live. Then pay yourself with a real card and refund it. Stripe keeps both keys working for up to seven days when you rotate, which is enough room to do this without an outage.
One thing that is not a bug. The test-mode banner Lovable shows in preview is hidden on the published site by design. If you see it on your live domain, you are looking at the preview, not the launch.
Why does the webhook function return 401, and where must the secret key live?
Because Supabase Edge Functions check for a Supabase login token by default, and Stripe does not have one.
This is the failure that looks most like a security setting doing its job, because it is. Supabase's docs say to keep JWT verification on so the platform validates the token before your handler runs, and then say in the same breath that external providers like Stripe or GitHub do not send Supabase credentials. They sign the request body with their own shared secret. So a webhook function deployed with the default setting rejects every Stripe delivery with 401, Stripe records a failed delivery, retries for up to three days with exponential backoff in live mode, and then gives up. Your customers paid the whole time.
The fix is two halves and you need both. Deploy the webhook function with JWT verification switched off. Then verify Stripe's signature inside the handler, because Supabase's own warning is that disabling the platform check makes your handler fully responsible for authenticating the caller. Stripe signs every event with an HMAC over a timestamp and the raw body, sends it in the Stripe-Signature header, and the official libraries check it against your signing secret with a default tolerance of five minutes. Two traps from Stripe's troubleshooting notes. The body has to be the raw bytes, so a framework that parses JSON before your code runs breaks verification. And the tolerance must never be set to zero, because zero disables the recency check and reopens replay. Supabase's example handler reads the request as text, calls the async construct-event method with a subtle-crypto provider, and carries the comment that the function should be deployed with verification off because Stripe verifies the request through its signature.
Why verification is not optional. Stripe's words: without it, an attacker could send fake webhook events to your endpoint to trigger actions like fulfilling orders, granting account access or modifying records. A webhook URL is not a secret. It is in your Stripe dashboard, your logs, and any screenshot you ever posted asking for help.
Now the key. The secret key, sk or the restricted rk form Stripe now recommends, goes in the function's secrets and nowhere else. Lovable's guide says never to paste it in chat and to use the Add API Key form, which stores it as a Supabase secret the function reads at runtime. The browser gets the publishable key, which Stripe says can identify your account and create payment methods but cannot charge anyone. We have pulled Stripe secret keys out of shipped frontends before, which our AI code audit guide describes, and a secret key in a bundle is someone else's checkout running on your account. If you are not sure where yours ended up, search the built JavaScript for the sk prefix before you read another word of this page.
Why do subscription statuses in your app stop matching Stripe?
Because the app wrote the status once, at checkout, and Stripe has changed it four times since.
A subscription is not a purchase. It is a state machine that Stripe runs on its own schedule, and Stripe tells you about each transition through events. The app that copied 'active' into a profile row on day one and never listened again will be wrong the first time a card expires. Stripe's subscription docs list the statuses: trialing, active, incomplete, incomplete expired, past due, canceled, unpaid, paused. They also say what to do with them. Provision access on invoice paid when the status is active. Ask for new card details on past due. Revoke on canceled or unpaid. An incomplete subscription has 23 hours to make its first payment before it expires and never bills. A trial ending fires an event three days out.
The pattern Stripe recommends is less code than the one that breaks. Store an access-until timestamp on the user, not a boolean. Every time invoice paid arrives, push the timestamp forward one billing period plus a day or two of grace. When the user logs in, compare the timestamp to now. Cancelled, past due and unpaid all take care of themselves, because the timestamp simply stops moving. The Lovable authentication guide covers the other half, which is that the subscription has to hang off a user id your app can trust.
Three properties of Stripe events break naive handlers, and Stripe documents all three. Events arrive out of order: creating a subscription can fire subscription created, invoice created, invoice paid and charge created in any sequence, and Stripe says not to use the created timestamp to order them because events can share a second. Events arrive more than once, so record processed event ids and skip repeats. And events keep arriving after you thought you were done, because in live mode Stripe retries a failed delivery for up to three days and lets you resend manually for 15. A handler that is safe to run twice is the only kind worth having.
The same idea protects the calls you make to Stripe, not just the ones you receive. Stripe's API accepts an idempotency key on every POST request, up to 255 characters, a random UUID is fine, and it stores the result for at least 24 hours so a retried request returns the original outcome instead of creating a second subscription. The generated code almost never sets one. A flaky connection during checkout creation plus a retry is two subscriptions for one customer, and the customer will find out before you do.
Refunds and disputes are events too. A refund issued in the Stripe dashboard fires charge refunded with the charge inside it, and a dispute fires its own event the moment the issuer reverses the payment. If your app only ever listens for the happy path, the refunded customer keeps the product and the disputed one keeps it while you lose the evidence window. Under Paddle as merchant of record, refunds and chargebacks are Paddle's to handle, which is the honest reason to choose it when you have no one to handle them.
When do you need Stripe Connect instead of one Stripe account?
The moment any of the money is not yours.
One Stripe account can take a payment from a customer and put it in your balance. It cannot split that payment with a seller, a tutor, a driver, a clinic or a creator. Stripe's Connect docs describe it as building a platform, marketplace or other business that manages payments and moves money between multiple parties, and the marketplace pattern specifically as collecting from customers and automatically paying out a portion to sellers or service providers. That is a different product with connected accounts, onboarding, and its own webhook scope for events from those accounts.
Neither easy path gets you there. Lovable Payments is a single-merchant setup. Stripe's Managed Payments, the merchant-of-record option, lists Connect explicitly among the integrations it does not support. So a founder who built a marketplace in Lovable and accepted the Payments dialog has a checkout that works and a payout model that does not exist. Sometimes the workaround is honest: take the full amount, pay the sellers by bank transfer at month end, and call it a service business for a while. Sometimes it is a marketplace pretending not to be one, and the sellers notice at tax time.
If you are paying anyone out, the decision is not Lovable Payments or your own Stripe. It is your own Stripe with Connect, from the start, on a backend you control, with the webhook listener registered for connected accounts as well as your own. That is a week of careful work, not a rebuild, and it is the one section of this page where the answer really is to plan before you accept a default.
When it is worth handing this to someone
The moment someone else's money moves through code nobody has read.
If the disqualifier section described you, pick Paddle, run the readiness check, do the three dashboard checks and go. If one of the five triggers has fired, the work on this page is a few days of careful configuration and testing for someone who has done it before, and weeks of guesswork for someone who has not, because none of it fails loudly. A handler with no signature check accepts everything. A live checkout pointing at a test price shows a button. A subscription that never listens again stays 'active' forever. The only test that proves any of it is to pay yourself with a real card, cancel, wait for the event, and watch the database.
That is the work we do at Geminate Solutions. We take apps built in Lovable, Bolt, v0 and Replit and get them to the point where real customers can be charged through them, and we do not sell rebuilds. Your Lovable app stays your Lovable app. We have shipped 50+ products, run an EdTech platform at 250,000+ daily users and an exam system absorbing 10 million requests a minute, and hold Top Rated Plus on Upwork at 4.9. You own the code from the first commit. Our AI builder to production service lays out how an engagement runs. If the webhook is fine and it is the rest of the backend that is creaking, the Lovable backend solutions guide is the sibling to this one, and the production checklist for Lovable apps is the hub both of them hang off.
The first step is a written review of what your payments setup will and will not hold. Which path you are actually on. Where the secret key lives. Whether the live products and the live endpoint exist. Whether the handler verifies and deduplicates. And what happens to a user the day Stripe says they stopped paying. We send it back within 48 hours and it stays yours whether or not we ever talk again.
Frequently Asked Questions
Does Lovable Payments cost more than using Stripe or Paddle directly?
No. Lovable's documentation says using Stripe or Paddle through Lovable costs the same as setting up the provider directly, with no markup from Lovable. You pay the provider's own rate either way. What differs is who is the merchant of record and who owns the account, not the fee.
Can I move from Lovable Payments to my own Stripe account later?
Partly. On the Stripe path you claim the account Lovable created, so it is already yours, or you link an existing one. On the Paddle path Paddle is the merchant of record, so the sales are legally Paddle's and your subscribers stay with Paddle. Disconnecting is permanent: Lovable deletes the keys, webhook secrets and integration data from the project, and a project runs one provider at a time. Plan a switch as a period of running both, not a cutover.
Can a Lovable app take payments without user accounts?
One-time payments, yes. Subscriptions, not sensibly. A subscription has to belong to someone your app can recognise next time they log in, which is why Lovable recommends adding authentication before payments, and why its built-in feature allows one active subscription per user per environment by default.
Will resending a Stripe event or fixing the webhook double-fulfil an order?
Only if the handler is not idempotent. Stripe delivers events more than once and out of order by design, so the handler must record processed event ids and fulfil once per Checkout Session id. Once that is in place you can resend any event from the Dashboard, which Stripe allows for up to 15 days, and nothing doubles.
Do I still need to verify the webhook signature if Lovable set everything up?
Yes, and you should check rather than assume. Ask Lovable to show you the line in the handler that verifies the Stripe-Signature header against the signing secret. If it is not there, that is the first fix. Without it anyone who finds the endpoint URL can post a fake payment event and grant themselves access. Either way, never delete the endpoints Lovable registered in the provider dashboard.








