Web & SoftwareUpdated 10 min read

Why Your SaaS Payments Work in Test Mode but Fail With Real Customers

Test cards lie. Live Stripe keys, webhook secrets, and 3D Secure are a different system. Here is how to tell whether the money moved, whether the webhook died, and when to hire help.

Founder at a laptop in a small office, checking a phone after a payment notification, warm daylight, no readable screens.
Live Stripe is a different system than a green test checkout.

For SaaS founders, AI SaaS founders, Nontechnical founders, Founders using Lovable or Cursor · Intermediate · Commercial · Solves: Stripe works in test mode but not live, Customer paid but has no access, Webhook signature verification failed, Subscriptions do not activate in production

Key takeaways

  • Sandbox objects and secrets do not work in live mode.
  • A paid invoice is not the same as your app granting a plan.
  • CLI, test, and live endpoints each have their own whsec_.
  • Fix Dashboard evidence before rewriting the app.

If checkout works with a test card and dies the week you flip the live switch, the product did not suddenly forget how to charge a card. You left the sandbox.

Stripe treats sandbox keys and live keys as separate worlds. Objects do not cross. A sandbox Customer is not a live Customer. A sandbox webhook secret is not the live one. Stripe says this directly in its API keys documentation: objects in one mode are not accessible to the other.

That is why founders get a receipt-shaped UI, a happy test card, and then one of these: the live customer is charged and never gets access, the live customer is not charged at all, or the Dashboard shows a paid invoice while the app still treats them as a free user.

The money already moved. Your app did not notice.

Most SaaS billing is asynchronous. The browser finishes Checkout. The bank or card network may still need authentication. Stripe then tells your server what happened. Your server is supposed to mark the user paid, start the subscription state, and unlock the product.

Stripe's subscription webhook guide is blunt about this: a subscription integration needs a destination that handles events, because most subscription activity happens asynchronously. invoice.paid is the event they point to when you should provision access and the subscription is active. invoice.payment_failed and invoice.payment_action_required are how you find out a live card needs a retry, a new method, or a 3D Secure challenge.

AI builders are good at the visible half. They draw a pricing page. They drop in Checkout. They store a stripe_customer_id if you ask. They are weak at the invisible half: a public HTTPS endpoint, the correct live signing secret, raw-body signature checks, and idempotent handling when Stripe delivers the same event twice.

This is the same production gap as a Lovable app that works in preview and breaks with real users. Preview traffic never exercises live webhooks. Real customers do.

Test mode and live mode are two different Stripe accounts in practice

Sandbox keys start with pk_test_ and sk_test_. Live keys start with pk_live_ and sk_live_. Stripe documents the split on the API keys page. Webhook signing secrets are not API keys at all. They live on each endpoint, start with whsec_, and change per endpoint and per mode.

When founders say test works and live fails, they usually mixed one of these:

  • The site still uses pk_test_ in production, so live cards never hit the live account.
  • The site uses pk_live_ but the server still uses sk_test_, so session creation fails or writes to the wrong place.
  • Checkout is live, but the webhook endpoint is still the Stripe CLI secret from stripe listen.
  • The webhook URL still points at localhost, a preview URL, or an old Vercel deployment.
  • The endpoint exists, but it is not subscribed to the events that unlock the product.
Stripe CLI, Dashboard test endpoints, and Dashboard live endpoints each have their own whsec_. Using the wrong one produces the same signature error as a mangled request body.

That last point is official. Stripe's signature troubleshooting page says the most common error is the wrong endpoint secret, and that the CLI secret is not the Dashboard endpoint secret.

The three secrets that get mixed up

1. The publishable key in the browser

Safe to expose. It identifies the account and starts Checkout or Elements. If production still has pk_test_, real cards are talking to a sandbox. Nothing you do in the live Dashboard will match.

2. The secret key on the server

Not safe to expose. It creates Checkout Sessions and reads customers. If this leaks into a Vite or Lovable client bundle, you have a finance incident, not a styling bug. Restricted keys are what Stripe now recommends for new work. Rotate anything that has been in a chat log, a committed .env, or a screenshot.

3. The webhook signing secret

This is how you prove the POST came from Stripe. You pass the raw body, the Stripe-Signature header, and the whsec_ for that exact endpoint into constructEvent. Stripe's webhook guide is explicit: do not manipulate the raw body. Next.js, Express json middleware, and pretty-printed logging all break this in the same way. The error looks like a bad secret even when the secret is right.

Checkout succeeding is not the same as access being granted

Here is the sequence a founder feels as one button click, and a production system treats as four different jobs:

  • Create a Checkout Session with live keys and the right price IDs.
  • Collect payment, including any authentication the issuer requires.
  • Receive invoice.paid or checkout.session.completed on a public HTTPS URL.
  • Write plan, period end, and seat limits into your own database, then show the paid UI.

AI-generated apps often stop after step 1. They poll the client after redirect and hope the session looks paid. That works in sandbox when everything is instant and nobody challenges the card. It fails when the customer completes 3D Secure on another screen, when the webhook is 400ing in silence, or when two events arrive and the handler creates duplicate users.

Stripe also tells you not to assume event order. Distinct events can share a timestamp. Track event.id so retries do not double-provision. Live mode retries failed deliveries for up to three days with exponential backoff. That is in the webhook delivery docs. A handler that is slow, or that returns 500 while it emails invoices, will look haunted for days.

The live-only failures test cards hide

Sandbox cards are obedient. Live cards are not. The product can be fine in test and still fail for reasons that are not a missing env var.

  • Strong Customer Authentication. invoice.payment_action_required means the customer has to authenticate. If you never send them back to Stripe, you keep an incomplete subscription and a confused buyer.
  • Radar and issuer declines. Live fraud rules and banks decline cards that test numbers never simulate.
  • Wrong price or product IDs. Live products are a different set of objects. A hardcoded sandbox price ID fails or charges the wrong thing.
  • HTTPS and TLS. Live destinations must be public HTTPS. Stripe validates TLS 1.2 or 1.3 before it will send. A bad certificate looks like Stripe never called you.
  • Preview URLs. A webhook registered to a Vercel preview host dies the next deploy. Register the production host.

None of this is exotic. It is why 'we will add billing later' turns into refund tickets the first weekend you run ads.

What to check in the Stripe Dashboard before you hire anyone

You can diagnose a lot without reading the repo. Do this in live mode, not sandbox.

  • Payments or Invoices: did the money actually move? If no, the checkout or PaymentIntent is the bug. If yes, the webhook or entitlement write is the bug.
  • Workbench, Event destinations: is there a live HTTPS endpoint on your real domain? Open a recent event. Was the response 2xx or 400?
  • Reveal the signing secret on that same endpoint. Confirm the production host has that value, not the CLI value.
  • Enabled events: for subscriptions you want at least invoice.paid, invoice.payment_failed, invoice.payment_action_required, and customer.subscription.updated / deleted. checkout.session.completed if you fulfill one-time Checkout.
  • API keys: production browser traffic should show pk_live_. Server logs should never print a secret key.

If events are 400 with a signature error, you have a secret or raw-body problem. If events never appear, the URL is wrong, the host is down, or TLS is failing. If events are 200 and the user is still locked out, your handler acknowledges Stripe and then does not write the plan. That last one is an application bug. Stripe will not fix it for you.

When a vibe-coded Stripe integration is actually done

Treat billing as done only when all of this is true on a live test purchase you are willing to refund:

  • A real card (or your own card) completes Checkout, including any authentication prompt.
  • The live event destination shows 2xx for that payment.
  • The user can sign out, sign back in, and still see the paid plan.
  • A second user cannot see the first user's invoices or customer portal session.
  • Cancel and failed-payment paths lock the product or send the customer a recovery email.
  • The service role or secret key is not in the browser bundle.

If you have not tested a second user, you also have not tested Supabase RLS. Paid users leaking into each other's billing records is worse than a broken webhook.

What I would do if this were my SaaS

I would not start with a rewrite. I would start with the Dashboard. Money moved or it did not. Events are 2xx or they are not. The env on the production host matches the live endpoint or it does not.

Then I would make the webhook boring. One route. Raw body. constructEvent. Persist event.id. Grant access from invoice.paid when the subscription is active. Return 2xx fast. Put slow email and CRM work after the ack, not before it.

I would not ask an AI builder to 'just make Stripe work' again without pasting the live event ID, the HTTP status Stripe recorded, and the current endpoint URL. The model cannot see Workbench. You can.

If the app was assembled in Lovable, Bolt, or v0, I would also assume the fulfillment write is coupled to the UI. Move it server-side. Keep the publishable key in the client. Keep the secret key and webhook secret out of it.

When to hire someone instead of prompting the builder

Hire help when any of these are true:

  • Real customers have been charged and cannot get in. That is a support fire, not a learning exercise.
  • You cannot tell whether the failure is Checkout, webhooks, or your own database.
  • The secret key or webhook secret has been in a frontend bundle, a public repo, or a shared chat.
  • You need subscriptions, trials, seat changes, and failed-payment recovery, not a single test Checkout.

A useful first engagement is a production billing review, not a greenfield rebuild. Scope it like a real product SOW: live keys, event list, entitlement model, and who owns the Stripe account after the work.

If you only have sandbox traffic and no paying users yet, stay in test mode and finish the webhook path there with stripe listen, then register a live HTTPS destination before the first real charge. Do not learn this on a customer's card.

Frequently asked questions

Did my customer actually get charged?

Open live mode Payments or Invoices. If there is no paid object, Checkout or the PaymentIntent failed. If there is a paid object and they have no access, the webhook or your database write failed.

Why does Stripe say webhook signature verification failed?

Wrong whsec_ for that endpoint, or the framework parsed the body before constructEvent. Stripe documents both. The CLI secret is not the Dashboard live secret.

Which Stripe events should a SaaS subscription handle?

At minimum invoice.paid for access, invoice.payment_failed and invoice.payment_action_required for recovery, plus customer.subscription.updated and deleted. Stripe lists these in the subscription webhook guide.

Should I rebuild the app to fix live Stripe?

Usually no. Match live keys and the live destination, keep the raw body, persist event.id, and grant access from invoice.paid. Rebuild only if the secret key is in the client or billing is glued to the UI with no server path.

Can I stay in Stripe test mode until I have paying users?

Yes, and you should finish the webhook path there first. Register a live HTTPS destination before the first real charge. Do not learn billing on a customer's card.

Implementation table

FixProblemWhat to changeMetricTool
Swap to pk_live_ / sk_live_ and live price IDsTest checkout works, live does not startReplace sandbox keys and objects on the production hostLive Checkout Session creates without auth errorsStripe API keys
Repair the live webhook and entitlement writeCustomer paid, no accessRegister HTTPS destination, verify 2xx, grant on invoice.paidLive event shows 2xx and user sees the plan after refreshStripe Workbench
Match the endpoint secret and raw bodySignature verification failedReveal live whsec_, stop JSON-parsing before constructEventSame event ID verifies and stores onceStripe signature docs
Handle invoice.payment_action_requiredLive card needs authenticationSend the customer back to Stripe to complete SCAIncomplete subscriptions drop after the customer returnsSubscription webhooks
Related services

Work this article points to.

Website redesign

Website redesign

A redesign fails when it starts in Figma. Website redesign services should start with website strategy, the pages that create pipeline, then website design and website development. I run that sequence so a custom website redesign does not erase the rankings you already have.

View service

Sources & references

Related links

Zlatko Marjanovic — founder of ZedNova Studios

Zlatko Marjanovic

Founder, ZedNova Studios

I am Zlatko Marjanovic, founder of ZedNova Studios and an AI product engineer. I take over Next.js, Supabase, and Stripe codebases, fix what is actually broken, and keep shipping.

On GitHub I work in public with Cursor, Claude Code, Next.js, and Supabase. On Upwork I help founders who already have a product, often one built fast with AI tools, and now need someone to stabilize auth, billing, and deploys.

I have been doing this for 7+ years and have shipped 120+ projects for US and EU teams. The work I care about is the layer after the demo: RLS, webhooks, Vercel, and the next version.

If you want help with a build, a messy repo, or a site that should rank and convert, email me at zlatkomarjanovic.zm@gmail.com.

LinkedInX / TwitterGitHubWebsiteUpwork
Older articleGPT-6 Astra Is Live. What OpenAI's New Model Changes for Founders and AgenciesNewer articleYour MedSpa Website Gets Traffic but No Bookings. Here's What Is Usually Wrong

Related