DEV Community

Cover image for How to Validate and Secure Your Stripe API Keys
ilshaad
ilshaad

Posted on • Originally published at codelesssync.com

How to Validate and Secure Your Stripe API Keys

Validate and secure your Stripe API keys: key types and prefixes, restricted least-privilege keys, safe storage, key rotation, and what to do after a leak.

By Ilshaad Kheerdali · 27 June 2026


A leaked Stripe secret key is not a small mistake. Anyone holding your sk_live_ key can create charges, issue refunds, read your entire customer list, and trigger payouts to themselves. That is full control of the money side of your business, handed over in a single string.

The frustrating part is that almost none of these leaks come from clever attacks. They come from sloppy handling: a key pasted into a frontend bundle, committed to a public repo, dropped into a Slack thread, or baked into a screenshot in a bug report. Bad actors run automated scanners against public repositories around the clock, so a key that hits GitHub can be abused within minutes.

This post walks through how to recognise each Stripe credential type, validate a key before you trust it, store it safely, and rotate it without downtime. If you want to sanity-check a key right now, paste it into the free Stripe API Key Validator. It runs entirely in your browser and never sends the key anywhere.

Understanding Stripe API key types and prefixes

Stripe uses the key prefix to encode both what a credential is and which mode it belongs to. Get fluent in reading prefixes and most key mistakes disappear.

Prefix Type Safe client-side? What it does
pk_test_ / pk_live_ Publishable Yes Identifies your account in browser/mobile code (Stripe.js, Elements)
sk_test_ / sk_live_ Secret No, server only Full access to your account via the API
rk_test_ / rk_live_ Restricted No, server only Scoped access limited to the permissions you grant
whsec_ Webhook signing secret No Verifies webhook events came from Stripe, not an API key
sk_org_ Organization key No, server only Organization-level access across multiple Stripe accounts

The middle substring is the mode switch. _test_ keys operate on sandbox data and _live_ keys operate on real data, and objects never cross between modes. A test key pointed at live data will silently return empty results rather than throw a loud error, which is why a "my sync returns nothing" bug is so often just a test/live mismatch.

Stripe is blunt about exposure in its official key documentation: "Only publishable keys are safe to expose outside your application's backend. You're responsible for protecting other Stripe API keys, including restricted API keys." Publishable keys are designed to be visible. Everything else belongs server-side.

How to validate and secure your Stripe API keys

Before you wire a key into config or paste it into a third-party tool, confirm it is actually the key you think it is. There are three quick checks:

  1. Prefix. Does it start with the type you intended? If you meant to grant read-only access but the string starts with sk_live_, you are about to hand over full account control.
  2. Mode. Is it _test_ or _live_? Make sure it matches the environment you are configuring.
  3. Character set and length. Stripe keys are a fixed alphabet with an expected length. A truncated copy-paste or a stray whitespace character is a common reason a "correct" key fails auth.

Doing this by eye is error-prone, especially when keys are masked in dashboards. The Stripe API Key Validator does all three at once: it detects the key type and mode from the prefix, validates the character set and length, masks the value for safe display with a visibility toggle, and prints security guidance specific to that key type. Crucially it is 100% client-side with zero network requests, so even a live secret key never leaves your browser. That makes it safe to use on a real key, unlike a random "paste your key here" web form you should never trust.

To secure your Stripe API keys properly, validation is step one. The rest of this post covers the handling rules that keep a valid key from becoming a liability.

Why the webhook signing secret (whsec_) is not an API key

This trips up a lot of developers. The whsec_ value looks like a key, so people try to authenticate API requests with it and get nothing but auth errors.

The webhook signing secret is not a credential for calling Stripe. Stripe generates a unique secret for each webhook endpoint, and you use it to verify that incoming events genuinely came from Stripe rather than a spoofed request. Your handler reads the Stripe-Signature header and runs it through constructEvent (or Webhook.construct_event) along with the secret. Signature scheme v1 is used, and Stripe's libraries enforce a default timestamp tolerance of 5 minutes to block replay attacks.

Two things ruin webhook verification in practice. First, you must verify against the raw request body, not a parsed and re-serialized JSON object, or the signature will never match. Second, the secret is per-endpoint: the secret printed by the Stripe CLI is different from the one shown for a Dashboard endpoint, so using the wrong one fails every time. See the Stripe webhooks documentation for the exact verification flow in your language.

Restricted keys and the principle of least privilege

If your code only needs to read data, never give it a key that can move money. A restricted API key (RAK) starts with rk_live_ or rk_test_ and, in Stripe's words, "can do only what you give it permission to do." When you create one in the Dashboard you set a permission of Read, Write, or None per Stripe resource. Note that write implies read: any key that can write a resource can also read it.

This is the principle of least privilege in action, where a key should have the minimum permissions necessary to do its job and no more. Stripe's own example is sharp: a restricted key scoped to read dispute data only lets a bad actor read dispute data. They cannot create charges, touch customer payment methods, or trigger payouts. The blast radius of a leak shrinks to almost nothing.

Stripe recommends giving each service its own restricted key (billing, reporting, and your webhook handler each get a separate scoped key) and recommends always preferring restricted keys over unrestricted secret keys, especially when handing a key to an AI agent or any third-party integration. The full guidance lives in the restricted API keys docs. The takeaway: a single all-powerful sk_live_ shared across every integration is the worst possible pattern, and the easiest one to fix.

Storing keys safely: environment variables, secrets managers, and never committing to git

Once you have the right key, where it lives matters as much as what it can do. Stripe's best practices guide lays out a clear hierarchy.

  • Never put secret keys in source code. Bad actors continuously scan public repositories for Stripe keys. And remember git history retains a key even after you delete it from the latest commit, so a "quick fix" that removes the line does nothing unless you rewrite history and rotate the key.
  • Never embed keys in client applications. Use publishable keys client-side. A secret key in a frontend bundle or mobile app is a full-account compromise waiting to be discovered.
  • Store secrets in a vault. AWS Secrets Manager, Google Cloud Secret Manager, Azure Key Vault, or HashiCorp Vault are the recommended homes. Environment variables are an acceptable fallback when a vault is not available.
  • Add a guardrail at commit time. Periodically audit your codebase for sk_live_ and rk_live_ patterns, and add a pre-commit hook that rejects any commit containing them. This catches the mistake before it ever reaches a remote.

A minimal example of reading a key from the environment rather than hard-coding it:

// Good: the key lives in the environment, never in the repo
const stripe = require('stripe')(process.env.STRIPE_RESTRICTED_KEY);

// Bad: this string is now in your git history forever
// const stripe = require('stripe')('sk_live_51Hxxxx...');
Enter fullscreen mode Exit fullscreen mode

You can also restrict keys to stable IP addresses and monitor your API request logs to spot misuse early. Limiting where a key works and watching how it is used are both cheap insurance.

Rotating and expiring Stripe keys without downtime

Keys are not set-and-forget. Rotate them periodically, and rotate immediately whenever a team member with access leaves or a key has been pasted somewhere it should not be.

Rotating a key in the Dashboard revokes it and generates a replacement that is ready to use immediately. The detail that saves you from an outage: both the old and new keys keep working for up to 7 days. That window lets you deploy the new key everywhere before the old one dies, so older deployments still holding the previous key do not suddenly break. If you need longer than 7 days, create a new key manually, migrate, then expire the old one. When you do cut over, you can choose Now to delete the old key instantly or schedule a future expiration.

One quirk worth knowing: you can expire a secret or restricted key (after which you create a new one and update your code), but you cannot expire a publishable key. Publishable keys can only be rotated and replaced. Also, a key left unused for 180 or more days may have its access limited, which you can restore from the Dashboard.

The mistake to avoid is a hard cutover that revokes the old key the instant you create the new one. Use the dual-key window instead and a rotation becomes a non-event.

What to do if your Stripe key is exposed

Treat any exposure as a compromise, full stop. Stripe's guidance is unambiguous: if a restricted or secret key is exposed or compromised, rotate it immediately even if you are not sure anyone saw it. If you find a sensitive key somewhere it should not be, assume it has been seen.

Concretely:

  1. Rotate the key now. Do not wait to confirm misuse. Generate a replacement using the dual-key window and migrate.
  2. Audit your request logs. Check Stripe's API logs for unexpected charges, refunds, or reads around the time of exposure.
  3. Find the source. A key in git history needs the history rewritten, not just a new commit. A key in a screenshot or chat needs that artefact removed too.

Stripe does proactively monitor for exposed keys and may deactivate one and notify you, but it explicitly does not guarantee it will catch every leak. Your own rotation and monitoring process is the real safety net.

Stop hand-rolling key management: let CLS handle it

Here is where most of this gets easier in practice. If your reason for touching the Stripe API at all is to get your payments data into Postgres, you do not need a powerful secret key sitting in your own pipeline.

CLS syncs Stripe data into your PostgreSQL database (Supabase, Neon, AWS RDS, Railway, and more) and only ever needs read access. The best-practice setup is exactly what this post recommends: create a restricted, read-only key scoped to the resources you want to sync, then connect that. CLS stores the credential encrypted at rest with AES-256, runs managed scheduled syncs so you are not writing and babysitting your own cron jobs, and auto-creates the tables for you. The walkthrough is in the Stripe setup guide.

So validate your key in the Stripe API Key Validator, scope it down to read-only, and hand the minimal version to whatever consumes it. If that consumer is your analytics warehouse, see how to sync Stripe data to PostgreSQL and a comparison of the best tools to sync Stripe data to a database.

Frequently Asked Questions

What does a Stripe secret key look like?

A Stripe secret key starts with sk_test_ in test mode or sk_live_ in live mode, followed by a long string of letters and numbers. It grants full access to your account, so it must stay server-side and never appear in client code or source control. You can confirm a key's type and mode by checking its prefix, or by pasting it into a client-side validator.

Can I expose my Stripe publishable key?

Yes. Publishable keys (pk_test_ and pk_live_) are the only Stripe credentials designed to be safe in browser and mobile code. They identify your account to Stripe.js and Elements but cannot read sensitive data or move money. Every other key type, including restricted keys, must be kept private.

How do I rotate a Stripe API key?

Rotate it from the Stripe Dashboard, which revokes the old key and issues a replacement immediately. Both the old and new keys keep working for up to 7 days, so deploy the new key everywhere within that window before the old one expires. If you need more time, create a new key manually, migrate, then expire the old one.

What is the whsec_ webhook secret used for?

The whsec_ value verifies that incoming webhook events genuinely came from Stripe. It is not an API key and cannot authenticate API requests. Your handler passes the raw request body, the Stripe-Signature header, and this secret into a verification function to confirm the event is legitimate and not replayed.

Are restricted keys safer than secret keys?

Yes, when scoped correctly. A restricted key (rk_) only has the permissions you grant it, so a leaked read-only key cannot create charges or trigger payouts. Stripe recommends giving each service its own restricted key and preferring restricted keys over unrestricted secret keys, especially for third-party integrations and AI agents.


Related:

Top comments (0)