Security basics
A Stripe secret key exposed in your frontend can move money
A Stripe secret key exposed in your frontend can refund, charge and read every customer record you hold. Your pk_live_ key is meant to be there.

In short
- A Stripe secret key exposed in your frontend is the leak that gets charged to your account: refunds, new charges, and every customer record you hold.
- pk_live_ belongs in your app and always did. sk_live_ differs from it by one character and has unrestricted permissions across your whole Stripe account.
- Create the replacement key first, put it live, then expire the old one. Deleting the line from your code recalls nothing anyone already downloaded.
- We found a live Stripe secret key in 3 of 30,998 scanned apps. All three came out graded D.
Open your live app in a browser, view the page source, and search it for
sk_live_. If a long string comes back, a Stripe secret key is exposed in your
frontend, and every visitor you have ever had could have copied it.
Here is the part that general advice about API keys gets wrong: Stripe hands you
two live keys, they look almost identical, and one of them is supposed to be
in your app. The key beginning pk_live_ belongs there. The key beginning
sk_live_ has, in Stripe's own words, unrestricted permissions on all APIs.
They differ by one character in the middle of a long string, which is most of
the reason this keeps happening.
Between 12 and 14 August 2026 we ran nine external checks over 30,998 live apps built with Lovable, Bolt, v0, Replit and Base44. Three of them were shipping a live Stripe secret key, and two more were shipping a restricted one, which makes this among the rarest things the sweep turned up; the same nine checks found a Google API key in 1,142 apps. All three of the secret keys came back graded D, because a single critical finding caps the grade there no matter what else the app got right. The full numbers are in our scan report.
Is a Stripe secret key exposed in your frontend a problem?
Yes, if the string begins sk_live_. No, if it begins pk_live_.
Stripe issues two live keys because the two halves of a payment happen in two different places. Picture a shop counter. The card reader faces the customer and is bolted down in full view, and the worst a stranger can do with it is pay you. The till behind the counter is a different object entirely. It opens, it holds the day's takings, and the drawer under it holds a card for every customer with their address written on it.
pk_live_ is the card reader. Its job is to build a payment form inside
somebody else's browser, and Stripe's documentation says publishable keys are
safe to expose in front-end code.
sk_live_ is the till key. Stripe describes secret keys as having unrestricted
permissions on all APIs, which is the same sentence read from the other side:
there is nothing in your account it cannot reach.
Your app needs the card reader in the browser to take a payment at all. It should never need the till key.
pk_live_ and sk_live_: how to tell them apart
Read the second character of the prefix. That is the whole test.
| Key | Starts with | Safe in the browser? | What it does |
|---|---|---|---|
| Publishable | pk_live_… | Belongs here | Builds the payment form and tokenises a card. Cannot read customers or move money. |
| Secret | sk_live_… | Never | Unrestricted permissions on all of Stripe's APIs, across your whole account. |
| Restricted | rk_live_… | Never | Only the permissions you ticked when you made it. Still a working credential in a stranger's hands. |
| Test keys | sk_test_, pk_test_ | No | Touches your sandbox only. A smaller problem with the same habit behind it. |
The middle of the prefix is the second thing to read. _test_ reaches nothing
but your sandbox, so a leaked test key costs you no money; it still publishes
how your integration is built, and Stripe's guidance is to treat any secret or
restricted key seen where it should not be as compromised. Newer accounts can
also carry an organisation key beginning sk_org_, which operates across more
than one Stripe account at once. It follows the same rule, and a leak of one
reaches further than a single account.
What someone can do with a leaked Stripe secret key
Everything you can do in your own dashboard that does not need your password.
Not "gain unauthorised access". Concretely, with nothing but the string and a terminal: read your full customer list, with names, email addresses, billing addresses and the last four digits of every card. Read every payment you have ever taken, and what each customer paid for. Issue refunds. Create charges and payment links in your name. Cancel subscriptions.
Then there is the use that has nothing to do with you. Your account becomes a place to run card testing, which is Stripe's own term for a fraudster pushing stolen card numbers through somebody's integration to find the ones that still work. The cards belong to other people. The declines, the disputes and the explaining belong to you.
What they mostly cannot do is pay themselves. A refund goes back to the card that made the original payment, and a payout goes to the bank account on file, which is yours. That sounds like good news and is not: it means the damage arrives as your money leaving, your customers' records copied, and your account used for someone else's fraud, rather than as a transfer you could point at and chase.
None of this needs a sophisticated attacker. Stripe's own documentation says fraudulent actors continuously scan public codebases for exposed keys, and those scanners do not need to know who you are to find yours.
How to roll a Stripe secret key without breaking live payments
Create the replacement first, put it live, then expire the old one. In that order.
- In the Stripe Dashboard, create a new secret key. Leave the old one alone for now; both work at once, and that overlap is what keeps your checkout running.
- Put the new key where the old one lived, which should be a server, an Edge Function or a serverless route. Never the app the browser downloads.
- Deploy, then take one real payment. A live charge that succeeds is the only proof the new key is wired up correctly.
- Expire the old key. Stripe describes this exactly: expiring a secret or restricted key prevents it from making any further API calls.
- Read your payment history for the period the key was exposed, and your Stripe email for anything you did not do.
If the key is already out in public and you would rather lose a few payments than leave it live for another hour, do it the other way round. Rolling a key blocks it immediately and generates a new one, and Stripe notes that webhook endpoints created with the old key stay active, so your event handling survives the emergency.
There is one more step, and it is the one most people do first: deleting the key from your code. Do it, and be clear what it does. Your visitors already downloaded the file that carried it, that file is in browser caches you do not control, and the old value is still in your version history. Expiring the key at Stripe is what closes the door. Removing the line stops you shipping it again.
Publishable keys, for what it is worth, cannot be expired at all. Stripe never built the control, because that key was never meant to be private.
What a Stripe restricted key is, and when it is the right answer
A key cut for one job instead of every job.
A restricted key begins rk_live_ and carries only the permissions you tick
when you create it. A key that may read invoices cannot issue a refund. A key
that may create charges cannot read your customer list. Stripe recommends
migrating from secret keys to restricted ones for exactly this reason, and it is
good advice about the code running on your server.
It is not a way to make a browser key acceptable. Two of the 30,998 apps in our sweep were shipping a restricted key in the frontend, and both came back graded C. Our scan treats a restricted key as a high finding where a secret key is a critical one, and a single high finding caps the grade at C. Whoever finds that key still gets every permission you ticked, from anywhere.
Where a restricted key earns its place is the awkward middle case, and vibe-coded apps produce a lot of those. An automation tool that needs to read your payouts. A reporting script someone on Fiverr wrote for you. An Edge Function that only ever creates one kind of charge. Each of those runs on a server and each of them needs a fraction of your account, so each of them gets its own key with that fraction ticked, and the day one leaks you revoke one key instead of re-wiring your entire integration.
How to check what your app is actually shipping
Start by hand, because it costs nothing and needs nothing installed. Open your
live site, view the page source, and search it for sk_live_, then rk_live_,
then pk_live_. Finding the third and not the first two is the result you want.
What a page-source search misses is the JavaScript the page loads afterwards,
which on an app built by Lovable, Bolt or Replit is nearly all of it. Our free
scanner opens your app in a real browser, waits for the bundles to arrive, and
reads those. It classifies what it finds rather than matching key-shaped
strings, so pk_live_ comes back marked as correct and
sk_live_ comes back as a critical finding, and the two are never in the same
pile.
You get a grade, a score and the counts on screen in about 20 seconds, with no account. Give an email address and the detailed list comes with it, along with a fix written for the builder you used that you can paste straight in.
Three things the scan will not do, which are the reasons it is safe to point at
a live app taking real payments: it never logs in, it never writes anything, and
it never keeps a key it finds. An exposed secret is stored as a masked hint of
the shape sk_live_…a1b2, and the real value is thrown away.
Scan your app, or read
what each of the nine checks looks at first.
What to do right now
What to do
- Read the second character before anything else.
pk_live_in your bundle is correct and needs no action;sk_live_andrk_live_do. - Create the replacement key first and confirm a real payment on it, then expire the old one at Stripe. Expiring is what closes the door.
- Deleting the key from your code closes nothing on its own. The file that carried it is already downloaded, cached and in your version history.
- Move whatever needed the key onto a server: an Edge Function, a serverless route, anything that is not the browser.
- Read your payment history and your customer list for the period the key was live. Rotation stops what happens next and says nothing about what already happened.
- Give each server-side job its own restricted key with only the permissions it needs, so the next leak costs you one key rather than all of them.
A Stripe key usually arrives late. The app ships, it runs for a while, and then one day you add checkout, and that is the deploy that puts a payment credential into the bundle for the first time. Whatever scan you ran at launch was a photograph of an app that could not take money yet.
Reeve Monitor is built for that gap. It re-runs all nine checks every hour on up to three apps, watches uptime every 60 seconds, tells you the day a result changes rather than waiting for you to look, and sends a monthly report in plain language. A key that reaches the bundle with a Thursday deploy is in that hour's re-scan. It is $12 a month at list, with seven days free before it charges you, and the pricing page is sometimes below the figure here and never above it.
If you would rather work through this as a list, the 10-minute security checklist covers this alongside the other things worth closing in a newly launched app. For the wider question of which keys belong in a browser at all, we have a guide to telling publishable keys from secret ones, the same question for an OpenAI key, and a census of what 30,998 apps actually shipped.
FAQ
Is my Stripe publishable key safe in the frontend?
Yes. A key beginning pk_live_ is meant to be in the page, and Stripe says so in its own documentation: publishable keys are safe to expose in front-end code. It builds the payment form and tokenises a card. It cannot read your customers, move money or refund anything. If a scanner or a friend told you a Stripe key is exposed, read the second character before you do anything else, because a pk_live_ key sitting in your bundle is your integration working the way Stripe designed it.
What can someone do with a leaked Stripe secret key?
Everything you can do in your own dashboard that does not need your password. They can read your full customer list with names, email addresses, billing addresses and the last four digits of each card, read every payment you have ever taken, issue refunds until your balance is empty, create charges and payment links, and run stolen card numbers through your account to find the ones that still work. Stripe calls that last one card testing, and it arrives at your door as disputes and declines on an account you thought was quiet.
How do I roll a Stripe key without downtime?
Create the replacement first. In the Stripe Dashboard, create a new secret key, put it where the old one lived on your server, deploy, and confirm one real payment goes through on the new key. Only then expire the old one, which stops it making any further API calls. If the key is already public and you would rather lose a few payments than leave it live, roll it instead: rolling blocks the key immediately and generates a new one, and Stripe notes that webhook endpoints created with the old key stay active.
What is a Stripe restricted key?
A key you cut for one job. A restricted key begins rk_live_ and carries only the permissions you tick when you create it, so a key that may read invoices cannot issue a refund. Stripe recommends moving from secret keys to restricted ones for exactly that reason. Read it as advice about the code on your server, not as a way to make a browser-side key acceptable: a restricted key in your bundle is still a credential a stranger can use, and our scan grades one as a high finding.
Does Stripe tell me if my key leaks?
Sometimes, and you cannot plan on it. Stripe says it proactively scans the internet for leaked API keys using tools like the GitHub token scanner, and it may notify you or deactivate a key it finds. Its own best-practices page adds that detection is not guaranteed. The scanning that works best is aimed at public code repositories, and a key compiled into the JavaScript on your own domain is not a repository. Treat any key you have seen somewhere it should not be as compromised, whether or not Stripe has said anything.
Is a test key (sk_test_) dangerous in my frontend?
It is a much smaller problem than a live one and it is still worth closing. A test key touches only your sandbox, so nobody can take your money with it. What it does hand over is a working map of how your integration is put together, and it tends to mean the same copy-paste habit is one deploy away from shipping the live key instead. Stripe treats any secret or restricted key seen where it should not be as compromised. Expire it and move the call to a server.