pwnmyvibecode_

Stripe keys: which ones are safe to expose

Only the publishable key (pk_live_ or pk_test_) belongs in your front end. Secret keys (sk_live_) and restricted keys (rk_live_) act as your Stripe account over the API, so they stay on your server, and a leaked one should be rotated straight away.

Updated

Check your live site now

Free, no signup, read only. A grade and plain fixes in seconds.

Stripe key types and what each one can do

  • pk_live_ and pk_test_: publishable keys. Built for the browser and mobile apps, used by Stripe.js, Elements and Checkout. They can send card details to Stripe and confirm a payment your server already set up. They can't read your data or move money.
  • sk_live_ and sk_test_: secret keys. Full API access to that mode of your account.
  • rk_live_ and rk_test_: restricted keys. Secret keys with permissions you pick per resource. Smaller blast radius, still secret.
  • whsec_: webhook signing secrets. Your server uses them to prove a webhook really came from Stripe. Server only.

Test and live mode are separate worlds. A test secret key can't touch real money, but it still exposes your test data and how your integration works, so keep sk_test_ out of the browser too.

One thing that looks like a secret but isn't a key: the PaymentIntent client secret (it contains _secret_). Stripe designed it to be sent to the browser for that single payment.

What someone can do with a leaked Stripe secret key

A live secret key is effectively a login to your Stripe account through the API. Depending on what your account uses, someone holding it could:

  • Read your customers: names, emails, addresses and payment history. Stripe never returns full card numbers.
  • Issue refunds and cancel subscriptions.
  • Create or change products, prices and coupons.
  • Add a webhook endpoint of their own, so your future events flow to them.
  • Create payments through your account, including testing stolen card numbers, which brings disputes and can put your account at risk.

A restricted key can only do what you allowed. Look at its permissions on the API keys page before deciding how bad a leak is, but rotate it either way.

How to rotate a leaked Stripe key

  1. Open the API keys page in the Stripe Dashboard (under Developers) and make sure you're looking at live mode.
  2. Open the menu next to the leaked key and choose Rotate key. Stripe issues a new key and lets you choose when the old one expires. For a leak, pick Now. Restricted keys can be rotated or expired the same way.
  3. Put the new key in your host's environment variables under a server-only name, such as STRIPE_SECRET_KEY, and redeploy.
  4. Delete the old key from your code, any committed .env files and your git history.
  5. Open the key's request logs from its menu on the API keys page and look for calls you didn't make, then check recent refunds, new coupons, price changes and your list of webhook endpoints.
  6. If you find abuse, contact Stripe support and tell them the key was exposed.

Rotating breaks anything still using the old key the moment it expires. Update your server env quickly, but don't leave a leaked live key working just to avoid a few minutes of downtime.

terminal
# 1. Put the leaked value in a file, one per line, mapped to a placeholder
echo 'PASTE_THE_LEAKED_KEY_HERE==>REMOVED' > replacements.txt

# 2. Rewrite every commit (work on a fresh clone, keep a backup)
git filter-repo --replace-text replacements.txt

# 3. Force push the rewritten branches, then delete replacements.txt
git push --force --all

How to keep sk_live_ on your server

The pattern is always the same. Your front end calls your own route. The route talks to Stripe with the secret key and hands back only what the browser needs, like a Checkout URL or a client secret.

.env.local
# Server only: no NEXT_PUBLIC_ or VITE_ prefix
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Public by design
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
app/api/checkout/route.ts
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const ALLOWED_PRICES = new Set(["price_basic", "price_pro"]);

export async function POST(req: Request) {
  const { priceId } = await req.json();
  if (!ALLOWED_PRICES.has(priceId)) {
    return Response.json({ error: "Unknown price" }, { status: 400 });
  }
  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: "https://example.com/thanks",
    cancel_url: "https://example.com/pricing",
  });
  return Response.json({ url: session.url });
}

The classic AI-built mistake is an agent moving STRIPE_SECRET_KEY under a VITE_ or NEXT_PUBLIC_ name so client code can create a Checkout Session. The checkout works, and the key is now in the public bundle. On Lovable or Bolt with Supabase, put that call in an Edge Function instead.

Our free scan flags sk_live_ and rk_live_ keys it finds in your page HTML and your site's own JavaScript. It ignores publishable keys, because those are supposed to be there.

Questions

Is the Stripe publishable key safe to expose?

Yes. pk_live_ and pk_test_ keys are made for front-end code. They can't list customers, issue refunds or read payments.

How much damage can a leaked Stripe secret key do?

A leaked secret key can refund payments, read customer data, change your products and prices, and run payments through your account. Rotate it as soon as you find it.

Should I use a restricted key instead of the secret key?

For any service that only needs part of the API, yes. A restricted key with only the permissions it needs limits the damage if it leaks. It's still a secret and still stays on the server.

Does GitHub catch Stripe keys?

GitHub secret scanning recognises Stripe secret keys, and push protection can block a commit that contains one. Neither looks at the JavaScript your deployed site serves, so check that separately.