API Keys: How They Work, Best Practices, and What to Avoid

API Keys: How They Work, Best Practices, and What to Avoid

A junior dev pastes a Stripe secret key into a Slack thread to debug a webhook. Three minutes later it's in a public repository, indexed by GitHub's secret scanner, and someone in Lithuania is testing it with a $1 charge to see if it works. By the time anyone notices, $4,000 has been charged across 80 dummy customers. This isn't a hypothetical — it's a near-weekly event in companies that haven't thought hard about how API keys actually work.

API keys feel deceptively simple: a long opaque string the server checks before serving a request. But the questions of where to put them, how to scope them, when to rotate them, and what to do when they leak are where the real engineering happens.

What an API Key Actually Is

An API key is a shared secret. The client sends it on every request; the server compares it against a list (or hash, if the server is well-built) of known valid keys and decides what the caller can do.

At the byte level it's nothing exotic — typically 32-64 bytes of base64 or hex, often with a prefix that identifies the type:

sk_live_51Hxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xoxb-xxxxxxxxxxxx-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx

The prefix is more than cosmetic. It tells secret scanners (GitHub's, Trufflehog, gitleaks) what kind of credential they've found and which provider to notify. Stripe scans GitHub continuously for sk_live_ prefixes and revokes leaked keys automatically — saving real money in the process. If you're designing your own keys, copy this pattern.

The key itself should be generated from a cryptographically secure random source — crypto.randomBytes in Node, secrets.token_urlsafe in Python, never Math.random(). Use the Hash Generator to verify the entropy looks right (a properly random 32-byte key should compress poorly).

Why API Keys Are Common Despite Being Weak

API keys have a real weakness: a leaked key gives the holder full access until it's revoked. There's no per-request verification of who's actually making the call, no expiration shorter than "until rotation," no proof of presence. Compared to OAuth's short-lived access tokens or mutual TLS, an API key is the security equivalent of a door key you mail to everyone who needs to enter.

So why do they dominate? Because they're trivially easy to integrate. A new developer can curl an API in 30 seconds with a key — versus the multi-step OAuth dance that requires registering an app, handling redirects, refreshing tokens, and managing scopes. For machine-to-machine traffic where the human is absent (a backend service calling Stripe, a cron job hitting Sendgrid), the OAuth ceremony adds complexity without proportional benefit.

The pragmatic answer: API keys are fine for server-to-server contexts where you control both ends. They're dangerous in browsers, mobile apps, or anywhere else the key might end up in a place an attacker can read.

API Keys vs OAuth vs JWT

These three get conflated constantly. Here's the distinction.

API keys are static long-lived secrets identifying an application. They authenticate the caller as "the holder of this key." That's it.

OAuth 2.0 is an authorization framework that issues short-lived access tokens after a user (or service) consents to specific scopes. The key insight is delegation: a third-party app can act on behalf of a user without ever seeing their password. See the OAuth 2.0 deep dive for the full flow breakdown.

JWTs are a token format, not an auth scheme. A JWT is a self-contained, signed JSON payload that any party can verify without calling the issuer back. JWTs are commonly used as OAuth access tokens, but they can also be used as API keys with built-in expiration and scopes. Inspect any JWT with the JWT Decoder to see the claims structure.

The mental model:

API key:  "I'm app X" (long-lived, opaque, server checks list)
OAuth:    "User Y authorized app X to do Z until tomorrow"
JWT:      "Here's a signed claim — verify the signature locally"

For internal service-to-service traffic, API keys win on simplicity. For user-delegated access (a third-party app posting to a user's Twitter), OAuth is the only sane choice. For stateless microservices that need to verify tokens without a database hit, signed JWTs are the answer.

Where to Put the Key

The wrong answer is the URL query string:

GET https://api.example.com/users?api_key=sk_live_xxxxxx

Query strings end up in browser history, server access logs, referrer headers, and CDN logs. Even with HTTPS, the URL is logged at every hop. Stripe's docs explicitly forbid this. So do GitHub's. So should you.

The correct answer is the Authorization header:

GET /users HTTP/1.1
Host: api.example.com
Authorization: Bearer sk_live_xxxxxx

The Bearer scheme is the modern standard. Some older APIs use custom header names (X-API-Key, Api-Token) — both work, but Authorization: Bearer is the convention to follow when designing a new API. Build and replay these requests with the HTTP Request Builder when debugging auth issues.

Mobile apps and browsers should never embed long-lived keys directly. The right pattern is: client authenticates with the user's credentials, server returns a short-lived token, client uses that token. The static API key, if there is one, lives only on the server.

Rotation, Scoping, and Rate Limits

Three controls turn a dangerous static credential into a manageable one.

Rotation means swapping keys on a schedule (90 days is the common default) or whenever a leak is suspected. Rotation only works if you've designed for it: support multiple valid keys simultaneously during the rollover window, and make rotation a one-command operation. If rotating a key requires a 4-hour deployment, nobody will do it. Stripe's secret rotation docs describe their pattern.

Scoping restricts what a key can do. A key for the analytics service shouldn't be able to charge customers. GitHub's fine-grained personal access tokens let you restrict tokens to specific repositories and specific permissions — so a leaked PAT can't access everything the user owns. Implement this in your own API by tagging each key with a list of allowed scopes and checking on every request.

Rate limits scoped per key prevent a leaked key from being weaponized against your bill. If a free-tier key suddenly tries to make 10,000 requests per second, you want it auto-suspended, not silently fulfilled. See the API rate limiting deep-dive for the algorithms behind this.

Storing Keys (Client and Server)

On the server, the rule is: never store keys in plain text in the database. If your database is leaked (which it will be, eventually), the attacker should get hashes, not raw keys. The pattern:

// At key creation
const rawKey = `sk_live_${crypto.randomBytes(32).toString('hex')}`;
const keyHash = crypto.createHash('sha256').update(rawKey).digest('hex');
const keyPrefix = rawKey.slice(0, 12); // for display: "sk_live_a3f2"
await db.insert({ keyHash, keyPrefix, scopes, ownerId });
return rawKey; // shown to the user once, never again

// On every request
const provided = req.headers.authorization?.replace('Bearer ', '');
const providedHash = crypto.createHash('sha256').update(provided).digest('hex');
const record = await db.findOne({ keyHash: providedHash });

This means you can never display the key again after creation — but that's the right behavior. If a user loses their key, they generate a new one. Never email keys.

On the client side (mobile apps, desktop tools, CLIs), keys should live in OS-level secret stores: Keychain on macOS, Credential Manager on Windows, libsecret on Linux. Never in localStorage (any JS on the page can read it), never in plain config files committed to git.

For server applications, environment variables loaded from a secrets manager (AWS Secrets Manager, Vault, Doppler) at boot time. Read the environment variables and secrets management guide for the full pattern.

Common Leaks and How to Detect Them

The leak vectors, ranked by frequency:

  1. Committed to git. A .env file accidentally added, a key in a comment, a test fixture with real credentials. GitHub's secret scanning catches common formats automatically, but only after the push.
  2. Leaked through screenshots. A developer screenshots their terminal showing an API call, posts it in a public Slack or Stack Overflow thread, and the key is visible.
  3. Logged in error messages. A library logs the full request including headers on exception, and the log ships to a third-party logging service.
  4. Embedded in client-side code. A frontend dev hardcodes a backend key into the JS bundle, forgets to remove it, ships to production.

Detection: run gitleaks or trufflehog in CI on every PR, and check your provider dashboards for unusual usage spikes. Most providers (Stripe, AWS, GitHub) email you when they detect your key in a public repo via scanning partnerships. When a leak happens, rotate first and investigate second — the cost of an unnecessary rotation is 5 minutes of redeployment; the cost of a delayed one is potentially everything. The Password Strength Checker is a quick sanity check on the entropy of any new key before it ships.

Practical Defaults for 2026

If you're designing an API key system today, these defaults will keep you out of trouble:

  • 32 bytes of random data, base64url-encoded, with a 4-8 character prefix identifying type and environment (sk_live_, pk_test_).
  • Hashed with SHA-256 in the database; raw key shown to the user exactly once.
  • Sent in Authorization: Bearer <key> only — never in URLs.
  • Scoped per key with explicit permission lists; deny by default.
  • Rate-limited per key with overlap-supporting rotation windows of 7-14 days.
  • Auto-revocation on detected leak (your own scanner + provider notifications).

The OWASP API Security Top 10 is worth bookmarking — broken authentication is consistently in the top three issues year after year. When debugging key issues, the JSON Formatter helps with auth response bodies, and the HTTP Request Builder lets you replay calls with different keys to isolate scoping bugs.

FAQ

Should I use an API key or OAuth for my new API?

Use API keys for server-to-server contexts where the API consumer is an application you trust and the key never leaves a server. Use OAuth when end users are authorizing third-party apps to act on their behalf — that's literally what OAuth was designed for. Mixing the two is also common: a public OAuth flow for user-facing integrations, plus internal API keys for your own services calling each other.

How long should an API key be?

At least 128 bits (16 bytes) of entropy. 256 bits (32 bytes) is the common production default, encoded as 43 base64url characters or 64 hex characters. Anything shorter is brute-forceable in theory; anything longer doesn't add meaningful security beyond 256 bits.

Why do some keys have prefixes like sk_live_ or ghp_?

Two reasons. First, it lets developers identify the key type without context (live vs test, secret vs publishable, which provider). Second, automated secret scanners use the prefix as a signature — GitHub scans every push for known prefixes and notifies the issuing provider to revoke leaked keys. If you're rolling your own keys, adopt this pattern.

Should the API key be in a header or a query string?

Header. Always. Query strings end up in server logs, browser history, referrer headers, and CDN access logs. Use Authorization: Bearer <key> for new APIs.

How often should I rotate API keys?

90 days is a reasonable default for most APIs. More sensitive keys (production database credentials, payment processor keys) might warrant 30-day rotation. Less sensitive keys (read-only internal services) can go longer. The frequency matters less than the rotation actually working — a key you've never rotated and have no plan to rotate is the highest risk.

What's the difference between a publishable key and a secret key?

Publishable keys are designed to be embedded in browsers and mobile apps — they can do limited operations (initiate a checkout, return a public profile) and have no power to read sensitive data or perform privileged actions. Secret keys live only on servers and can do everything. Stripe popularized this split; copy the pattern if you have public-facing API consumers.

How do I store an API key in a frontend application safely?

You don't, directly. The pattern is: the user authenticates to your backend with normal credentials (email/password, OAuth, etc.), your backend issues a short-lived session token, and that session token is what the frontend sends with each request. The static long-lived API key (if any) lives only on the backend and never touches the browser.

What should I do if my API key is leaked?

Rotate it immediately — that's step zero, before anything else. Then check provider logs for unusual activity since the leak window started. If charges or data access happened, contact the provider's support and your own legal/security teams. Update any clients using the old key. Finally, run a post-mortem: how did it leak, and what control would have prevented it (scanner in CI, scoped tokens, shorter rotation)?