Your Live Site Is in Stripe Test Mode (and Nobody Told You)
Checkout submits, the card is accepted, a success page renders — and no money ever arrives. Here is how a live site ends up wired to a payment provider’s test environment, how to check yours in thirty seconds, and how to fix it properly.
By ShipReady
If your production JavaScript contains a pk_test_ publishable key, your live site is talking to your payment provider’s test environment. Checkout will look completely normal — the form submits, the card is accepted, a success page renders — but no charge is created, no money moves, and nothing appears in your live dashboard. Open your site, view source, and search for pk_test_. If it is there, every sale since launch is a test transaction.
This is the least dramatic and most expensive bug in this entire blog. It is not a security vulnerability. Nothing is exposed, nobody is attacking you, and there is no incident to disclose. It is simply that the thing your business runs on has been quietly disconnected, and every signal you would normally rely on says it is working.
It survives launch because the demo passes. You tested checkout — of course you did — and it worked. It worked because you were in test mode, which is exactly what test mode is for. Then you shipped, and the part that was supposed to change did not.
How to check, in about thirty seconds
You do not need to attempt a transaction, and you should not — the whole point is that attempting one tells you very little. The key is in the page.
- Open your live site in a normal browser tab — not a preview deployment, not localhost. The production URL your customers use.
- Open developer tools and search all loaded sources for
pk_test_. In Chrome that is the Sources panel, then Search (Cmd/Ctrl + Shift + F). - Do the same for
pk_live_.
| What you find | What it means |
|---|---|
pk_live_ only | Correct. Your site is pointed at the live environment. |
pk_test_ only | Your live site is in test mode. No payment made on it has ever reached your account. |
| Both | Worse than either. Which one gets used depends on a runtime condition — an environment variable, a hostname check, a feature flag — so some visitors may be charged and others silently are not. |
| Neither | Either your checkout is fully server-side or hosted by your provider, or the SDK is loaded on a route this search did not cover. Check the actual checkout page. |
A publishable key is meant to be public, so finding one in your bundle is not itself a problem — that is where it belongs. The prefix is the whole signal. And note that pk_test_ is not unique to Stripe: Paystack uses the same prefix, which is why ShipReady resolves the provider from what the page actually loads rather than guessing from the prefix alone.
Confirm it from the dashboard side
Stripe keeps test and live data completely separate, and the dashboard has a toggle between them. This is the second confirmation, and the one that tells you how long it has been going on.
- Switch the dashboard to test mode and look at recent payments. If your customers’ real transactions are sitting there, that is your answer.
- Switch to live mode. If it is empty since launch while your analytics show completed checkouts, the two facts explain each other.
- Check your webhook endpoints in both modes. Test and live have separate endpoints and separate signing secrets, so an integration that works in one can be entirely absent in the other.
Why it happens
Almost always one of four things, and none of them are careless:
- The production environment variable was never set. You set
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYlocally in.env.local, and in your hosting provider you set it for Preview but not Production — or set it after the last build, which for a build-time-inlined variable means it is not in the bundle you are serving. - A fallback swallowed the failure. Code of the shape
process.env.NEXT_PUBLIC_STRIPE_KEY || "pk_test_..."is extremely common in AI-scaffolded projects, because it makes the app run without configuration. It also makes a missing production variable completely silent. - The environment check does not fire in production.
process.env.NODE_ENV === "development" ? testKey : liveKeylooks right and usually is, but a static export, a misconfigured build command, or a preview environment promoted to production can leaveNODE_ENVsomewhere you did not expect. - The key changed but the build did not. Client-side environment variables in Next.js and Vite are inlined at build time. Updating the value in your dashboard does nothing until you redeploy.
The common thread is that a build-time value was expected to change between environments, and the mechanism that was supposed to change it failed quietly. That is a configuration failure, not a coding mistake — which is why code review does not catch it and why looking at the deployed site does.
Fixing it
- Set the live publishable key in your hosting provider’s Production environment scope specifically. Keep the test key for local development and preview deployments — that separation is the point, and you want it.
- Remove any hardcoded fallback. If the variable is missing, the checkout should fail loudly at build or at startup. A payment integration that runs without configuration is a payment integration that can run unconfigured in production.
- Redeploy. For any
NEXT_PUBLIC_- orVITE_-prefixed variable, the value is baked into the bundle at build time, so changing it in a dashboard without rebuilding changes nothing. - Re-verify from the live site with the same view-source check. Do not verify from the deploy log.
- Check the server side too. The publishable key is only half of it — your secret key, your webhook endpoint, and your webhook signing secret each have a test and a live version, and they need to agree.
The fix that lasts is not "set the right key". It is "make the wrong key impossible to deploy" — no fallback, and a build that fails when the variable is absent.
The secret key is a different problem
Everything above is about the publishable key, which is designed to be public. If you find a key beginning sk_test_ or sk_live_ in your browser bundle, stop reading and rotate it now: a secret key can create charges, issue refunds and read customer data. ShipReady reports that separately as an exposed Stripe secret key, and it is a genuine emergency in a way that a test-mode publishable key is not.
The same applies to a Stripe webhook signing secret or a restricted key reaching the browser. The general pattern — secrets published through a browser-exposed environment variable — is common enough in AI-scaffolded apps that it has its own check.
While you are in there
Test-mode keys are the loudest payment configuration failure, but they are not the only one visible from outside. Two others are worth the same thirty seconds:
- Is the amount decided in the browser? If a hidden field or a client request carries the price, it can be edited before it is sent.
- Are your discount codes in the bundle? Compiled-in promotion codes are readable by anyone, and if the discount is applied client-side the code is not even needed.
- Is the checkout page itself served over HTTPS, including the form action? ShipReady reports insecure payment form transport when it is not.
How ShipReady detects this
ShipReady reads the JavaScript your live site actually serves and looks for test-environment markers: the pk_test_ publishable-key prefix, provider-specific test key formats, and test/sandbox SDK hosts such as sandbox-cdn.paddle.com. When it finds one it reports a payment provider in test mode with the marker it saw and the page it saw it on. When it finds both a live and a test key it reports mixed test and live keys instead, because that is a different and more confusing failure.
It never attempts a transaction and never touches your provider account — everything above is read from the page, which is also why you can reproduce all of it yourself with developer tools. The Payments pillar covers the rest of what a scan can see from outside.
If you have just launched, run a scan on the live URL before you spend money driving traffic to it. Scan your site free — no signup, and it checks the whole launch surface, not just payments. The pre-launch checklist is the same ground in task order.
Frequently asked questions
- Can a customer tell they are paying in test mode?
- Usually not. The checkout renders normally and the success page appears. Their card is never charged, so the only signal is the absence of a charge on their statement — which most people do not check against a receipt they did receive.
- Do I lose the orders that were placed in test mode?
- The money was never taken, so there is nothing to recover from your provider. If your application recorded the orders in your own database they still exist there, and you will need to decide whether to contact those customers and ask them to pay again.
- Is a publishable key in my JavaScript a security problem?
- No. Publishable keys are designed to be in the browser and can only do a narrow set of things. Only the prefix matters here. A secret key (`sk_live_`, `sk_test_`) in the browser is an entirely different matter and should be rotated immediately.
- Why did my test checkout work but a real card fails?
- That is the same problem from the other side. Test mode only accepts test card numbers such as 4242 4242 4242 4242; a real card is rejected because the test environment has no way to charge it.