Stripe Error: api_key_invalid — Invalid API Key Provided
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
try {
await stripe.customers.list({ limit: 1 });
} catch (err) {
// err.type === 'StripeAuthenticationError'
// err.code === undefined (auth errors don't have a `code` field)
// err.statusCode === 401
// err.message === 'Invalid API Key provided: sk_test_***...***'
}
api_key_invalid is Stripe’s authentication-layer rejection. Before your request reaches any business logic, Stripe checks the bearer token against its key registry; if it doesn’t find a live record, the response is HTTP 401 and your SDK throws StripeAuthenticationError. The error has nothing to do with payments, customers, or your account — it’s purely about whether the credentials you presented are valid.
The fix is almost always boring: a test key in production, an env var that didn’t load, or a rolled key whose replacement never reached the deploy. Add a startup assertion that prints the key prefix and length, and you’ll catch 95% of these before they reach a customer. The other 5% are rotations and revocations that Dashboard → Developers → API keys reveals in seconds.
Why this happens
- Test key used in live mode (or vice versa). Stripe has two parallel environments. `sk_test_*` keys only access test data; `sk_live_*` keys only access live data. Pointing a test key at live API endpoints (or live key at the test dashboard's data) returns `Invalid API Key`. The keys themselves are valid — they're just for the wrong mode.
- Environment variable is undefined or empty. If `process.env.STRIPE_SECRET_KEY` is undefined, the SDK sends the literal string `undefined` (or `null`/empty) as the bearer token. Common in Vercel/Netlify when the env var was added but the deployment wasn't rebuilt, or in Docker when `--env-file` wasn't passed.
- Key was rolled or deleted in the Dashboard. If a teammate rolled the key after a leak, your old copy stops working immediately. Stripe may also retire restricted keys with overly broad scopes during their security audits. Dashboard → Developers → API keys shows last-used timestamp and revocation history.
- Whitespace, newline, or quote characters in the key. Copy-pasting from a Slack message, terminal, or env file can introduce trailing whitespace or smart quotes. The key looks right but has invisible bytes the API rejects. `.env` files quoted with `STRIPE_SECRET_KEY="sk_live_..."` sometimes pass the literal quotes through, especially on Windows.
- Restricted key missing the required permission. Restricted keys (`rk_test_*` / `rk_live_*`) are scoped. If the operation you're calling needs a permission the key wasn't granted (e.g., creating a charge with a key that only has read access), Stripe returns `api_key_invalid` rather than a permission-specific error.
How to fix it
Fixes are ordered by likelihood. Start with the first one that matches your context.
1. Confirm the key matches the intended mode
Print the key prefix at startup (never the whole key) and assert it matches the environment. Test deploys must use `sk_test_*`; production must use `sk_live_*`. A simple boot-time check catches misconfiguration before any customer hits the broken state.
const key = process.env.STRIPE_SECRET_KEY;
if (!key) throw new Error('STRIPE_SECRET_KEY is missing');
const expected = process.env.NODE_ENV === 'production' ? 'sk_live_' : 'sk_test_';
if (!key.startsWith(expected)) {
throw new Error(
`STRIPE_SECRET_KEY mode mismatch: expected ${expected}, got ${key.slice(0, 8)}…`
);
}
2. Trim whitespace and verify env loading
Strip whitespace before passing to the SDK and log the key length (a real key is 107 chars including the prefix). A length mismatch immediately reveals truncation.
const raw = process.env.STRIPE_SECRET_KEY;
const key = raw?.trim().replace(/^['"]|['"]$/g, '');
console.log('Stripe key length:', key?.length, 'prefix:', key?.slice(0, 8));
if (!key || key.length < 100) {
throw new Error('STRIPE_SECRET_KEY looks malformed');
}
const stripe = new Stripe(key);
3. Check the key wasn't rolled or deleted
Open Dashboard → Developers → API keys, locate your key by its last 4 chars, and check the "Last used" column. If "Never" or a date before now, the key has been replaced. Roll a new one in the same mode and update your env var. Audit the deletion log to find out who rolled it and why.
4. Use a restricted key with the right permissions, or fall back to the secret key
For services that only need a subset of Stripe (e.g., a webhook handler that only reads events), generate a restricted key with the minimum needed permissions. If the restricted key returns `api_key_invalid` on a specific operation, check the permission grid in the Dashboard — adding the missing scope fixes it. Don't fall back to a secret key in production code as a workaround.
5. Rotate keys safely with overlapping windows
When rolling a leaked or expiring key, Stripe lets you generate the replacement and keep both active for an overlap window. Deploy the new key first, monitor that traffic flows, then revoke the old one. Skipping the overlap causes brief `api_key_invalid` storms while deploys propagate.
Detection and monitoring in production
Alert on any `StripeAuthenticationError` immediately — they should be near-zero in steady state. A sudden burst means a deploy shipped with the wrong env or someone rolled the key. Tag errors by `process.env.NODE_ENV` and the key prefix (first 8 chars) so you can tell mode mismatches from key revocations at a glance.
Related errors
- stripecard_declinedThe customer's card issuer refused to authorise the charge. Stripe relays the issuer's decline reason in `decline_code` (e.g., `insufficient_funds`, `lost_card`, `do_not_honor`).
- stripeinsufficient_fundsThe customer's card issuer declined the charge because the account doesn't have enough available balance to cover the amount, including any pending authorisations and holds.
- stripeauthentication_requiredThe card issuer requires the customer to complete 3D Secure (SCA / PSD2) authentication, but the PaymentIntent was confirmed without the customer completing the 3DS challenge — usually because you're using a bare Card element instead of the Payment Element or didn't handle `requires_action`.
- stripesignature_verification_failedYour webhook handler called `stripe.webhooks.constructEvent()` and the signature in the `Stripe-Signature` header didn't match the HMAC-SHA256 of the raw request body computed with your endpoint secret.
- openairate_limit_exceededYour account has exceeded its per-minute request (RPM) or per-minute token (TPM) limit for the model you're calling. Limits are tier-based and per-model.
Frequently asked questions
What's the difference between `Invalid API Key` and `Permission denied`? +
Why does my key work locally but fail in production? +
Can a publishable key (`pk_*`) be used server-side? +
Why is `api_key_invalid` thrown as `StripeAuthenticationError` and not `StripeInvalidRequestError`? +
Should I commit my Stripe key to git in a `.env.example`? +
How do I rotate a Stripe key without downtime? +
Why do I see `api_key_invalid` on Stripe's webhook deliveries? +
Can I scope an API key to specific endpoints or IPs? +
When to escalate to Stripe support
Stripe support is the right channel only if (a) the Dashboard shows the key as active but every request fails 401 from your environment, suggesting an account-level or regional Stripe-side issue, or (b) you suspect the key was rolled by Stripe's auto-detect-exposed-keys system without notification. For everything else (mode mismatch, env var missing, copy-paste error, deleted key), the fix is in your code or your hosting environment.