My Stripe secret key is exposed
Most leaked keys cost you money by being used. This one can move money that's already yours. That's a different kind of urgent, and it changes the order you do things in.
Do this first
- Check the prefix.
sk_test_is not an emergency.sk_live_is. Thirty seconds, below. - Roll it โ Stripe Dashboard โ Developers โ API keys โ the โฏ menu next to the secret key โ Roll key. Choose expire immediately if you can deploy the new one right now.
- Then read your Payments, Refunds and Payouts for the whole window the key was public. Rolling stops the future; it does nothing about the past.
1. Which key is it?
Stripe puts the answer in the first few characters, which is genuinely helpful of them.
| Prefix | What it means |
|---|---|
pk_live_pk_test_ |
Not a leak. The publishable key is designed to sit in your frontend. It can create payment intents and tokenise cards; it can't read your data or move your money. It's in your page source on purpose. |
sk_test_rk_test_ |
Not an emergency. Test-mode keys only touch test data and fake card numbers. Roll it when convenient โ it's untidy, not dangerous. |
sk_live_ |
Emergency. Full API access to your live account. Keep reading. |
rk_live_ |
Depends. A restricted key, limited to the permissions someone chose for it. Open it in the dashboard and read what it can actually do before you panic โ it might be read-only on one resource, or it might be nearly as broad as a secret key. |
whsec_ |
Different problem, still real. The webhook signing
secret. It can't call the API, but anyone holding it can forge webhook
events your server will believe โ a fake
checkout.session.completed is a free order. Roll it in
Developers โ Webhooks โ your endpoint. |
2. What a live secret key actually allows
It's worth being specific, because "someone has your Stripe key" covers a wide range of outcomes:
- Reading your whole customer list. Names, email addresses, billing addresses, payment history, the last four digits and card brand for every card on file. Not the full card numbers โ Stripe never gives those back โ but everything around them.
- Issuing refunds. This is the one that costs real money fastest. Refunds go back to the original payment method, so it isn't directly a theft route, but it can empty a balance and it takes your revenue with it.
- Creating charges. Against saved payment methods, with your account as the merchant of record. Your customers see your name on the statement, and your account wears the disputes.
- Changing payout settings on some account configurations โ which is the theft route, and the reason to check it explicitly rather than assume.
Money can leave. That's the difference between this and a leaked LLM key, and it's why the "check what happened" step is not optional here.
3. Rolling it without taking payments down
Developers โ API keys, then the โฏ next to your secret key โ Roll key. Stripe offers you an expiry window โ 1 hour, 24 hours, or immediately โ during which the old key keeps working alongside the new one.
That window exists so you can deploy without an outage, and it is a genuine trade-off:
- If the key was in a public repository: expire it immediately. An hour is a long time when scrapers find committed keys in minutes. A few failed checkouts is a cheaper problem.
- If it leaked somewhere narrower โ a private repo, a screenshot in a Slack channel โ take the hour, deploy the new value to every environment, then confirm the old one is dead.
Don't forget the places that aren't your app: staging, a CI secret, a Zapier or Make connection, a serverless function with its own environment settings, that one cron job on a VPS.
4. Replace it with a restricted key, not another secret key
Nearly every AI-built app uses sk_live_ โ full account
access โ to do about four things: create a checkout session, read a session,
handle a webhook, maybe list a customer's subscriptions.
Stripe lets you create a restricted key
(rk_live_) with exactly those permissions and nothing else.
Developers โ API keys โ Create restricted key, then tick only
what your code calls. It's ten minutes of clicking, and it means the next
leak โ because there's usually a next one โ is a much smaller conversation.
Rolling the key fixes today. Downgrading to a restricted key fixes the category. Do both while you're already in the dashboard.
5. Checking what was done with it
Stripe keeps a proper audit trail, which makes this more tractable than most breaches. Cover the full window: from the commit that exposed the key to the moment you rolled it.
- Developers โ Logs. Every API request, with the key used and the source IP. This is the definitive answer โ if only your servers' addresses appear, nobody used it. Filter by the old key.
- Payments and Refunds. Anything you can't tie to a real order.
- Balance โ Payouts. Confirm the destination bank account is still yours, and that no payout schedule changed.
- Customers. New customer objects you didn't create โ a common shape of card-testing abuse.
- Developers โ Webhooks. Endpoints you didn't add. An attacker who adds one gets a live feed of your business, quietly, long after the key is dead.
If you find anything at all: contact Stripe support and say plainly that a live secret key was exposed and you believe it was used. They deal with this weekly, they can see more than you can, and getting in early matters for how fraudulent charges are handled.
6. Where it usually leaked from
Across a couple of hundred public AI-built apps, live Stripe keys turned up in three recurring places โ none of them the payment code itself:
- A "here's how to deploy this" markdown file. Real
environment variables pasted into
DEPLOYMENT.mdorSETUP_COMPLETE.mdso they could be copied into a hosting dashboard later. Markdown doesn't feel like code, so it doesn't feel like a leak. - A test or seed script with the key inline, because it
was faster than wiring up
dotenvfor a one-off. - A frontend environment variable.
VITE_STRIPE_SECRET_KEYand friends โ anything prefixedVITE_,NEXT_PUBLIC_orREACT_APP_is compiled into the JavaScript bundle you ship. The.envfile being gitignored makes no difference at all; the value is in the built output.
7. Keeping it out next time
- Secret key read from
process.envon the server only, never a literal - Never prefixed
VITE_/NEXT_PUBLIC_/REACT_APP_ - Publishable key in the frontend โ that's the one that belongs there
- Restricted key with only the permissions your code uses
- Webhook handler verifies the signature with
whsec_, and treats an unverified event as hostile .envin.gitignore, and no real values in any committed markdown- GitHub push protection on, so the next one is stopped before it lands
Paste this into whatever built the app:
Find every place a Stripe key appears in this repository, including markdown files, scripts, config and environment examples. Any secret key (sk_ or rk_) must be read from process.env on the server only โ move any client-side usage into a server route. Confirm the publishable key is the only Stripe key reaching the browser bundle, and that every webhook handler verifies the Stripe signature before trusting the event. List every file you changed.
Check whether there's another one
vibecheck scans your project for exposed keys โ live Stripe keys, secrets compiled into frontend bundles, unverified webhook handlers โ and explains each one in plain English, with a fix prompt to paste back into your AI coding tool.
Scan my project โ free