---
# === IDENTITY ===
id: software/patterns/api-key-management/2026
canonical_question: "How do I implement API key management?"
aliases:
  - "API key generation best practices"
  - "how to store API keys securely"
  - "API key rotation strategy"
  - "API key hashing and storage"
  - "implement API key authentication"
  - "API key prefix design pattern"
  - "secure API key lifecycle management"
  - "API key scoping and permissions"
entity_type: software_reference
domain: software > patterns > api_key_management
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-02-24
confidence: 0.88
version: 1.0
first_published: 2026-02-24

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: null
  next_review: 2026-08-23
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "Hash keys before storage — never store plaintext API keys in your database"
  - "Prefix keys for identification (e.g., sk_live_, pk_test_) — enables quick lookup and leak detection"
  - "Support key rotation without downtime — allow two active keys per client during transition"
  - "Transmit keys only over HTTPS — never in URL query parameters"
  - "Scope keys to minimum required permissions — every key should have explicit API/resource restrictions"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs OAuth 2.0 client credentials flow for service-to-service auth"
    use_instead: "OAuth 2.0 client credentials documentation"
  - condition: "User needs JWT-based authentication with user sessions"
    use_instead: "software/system-design/auth-system/2026"
  - condition: "User needs secret management for deployment (env vars, vaults)"
    use_instead: "HashiCorp Vault or AWS Secrets Manager documentation"

# === AGENT HINTS ===
inputs_needed:
  - key: key_type
    question: "What type of API key do you need?"
    type: choice
    options: ["public (client-side, rate limiting only)", "secret (server-side, full access)", "both public + secret pair"]
  - key: auth_model
    question: "What is your authentication model?"
    type: choice
    options: ["key-only (simple identification)", "key + secret (HMAC signing)", "key + OAuth (hybrid)"]
  - key: rotation_needs
    question: "Do you need automated key rotation?"
    type: choice
    options: ["manual rotation is fine", "automated rotation with grace period", "zero-downtime rotation required"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/patterns/api-key-management/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-02-24)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/system-design/auth-system/2026"
      label: "Authentication & Authorization System Design"
    - id: "software/system-design/api-gateway/2026"
      label: "API Gateway Design"
  solves:
    - id: "software/debugging/ssl-tls-certificate-errors/2026"
      label: "SSL/TLS Certificate Errors"

# === SOURCES (7 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, community_resource, industry_report
# Reliability: high, moderate_high, moderate, authoritative
sources:
  - id: src1
    title: "OWASP Key Management Cheat Sheet"
    author: OWASP Foundation
    url: https://cheatsheetseries.owasp.org/cheatsheets/Key_Management_Cheat_Sheet.html
    type: community_resource
    published: 2024-12-01
    reliability: authoritative
  - id: src2
    title: "API Keys — Stripe Documentation"
    author: Stripe
    url: https://docs.stripe.com/keys
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
  - id: src3
    title: "Best practices for managing secret API keys — Stripe"
    author: Stripe
    url: https://docs.stripe.com/keys-best-practices
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
  - id: src4
    title: "Best practices for managing API keys — Google Cloud"
    author: Google Cloud
    url: https://docs.cloud.google.com/docs/authentication/api-keys-best-practices
    type: official_docs
    published: 2025-06-01
    reliability: authoritative
  - id: src5
    title: "Manage access keys for IAM users — AWS"
    author: Amazon Web Services
    url: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html
    type: official_docs
    published: 2025-09-01
    reliability: authoritative
  - id: src6
    title: "Best Practices for Building Secure API Keys"
    author: freeCodeCamp
    url: https://www.freecodecamp.org/news/best-practices-for-building-api-keys-97c26eabfea9/
    type: technical_blog
    published: 2023-08-15
    reliability: moderate_high
  - id: src7
    title: "OWASP API Security Top 10"
    author: OWASP Foundation
    url: https://owasp.org/www-project-api-security/
    type: community_resource
    published: 2023-06-01
    reliability: authoritative
---

# API Key Management: Generation, Storage, Rotation & Security

## TL;DR

- **Bottom line**: API keys identify callers and enforce rate limits — they are not a substitute for authentication on their own; combine with OAuth or HMAC for sensitive operations.
- **Key tool/command**: `crypto.randomBytes(32).toString('hex')` (Node.js) or `secrets.token_hex(32)` (Python)
- **Watch out for**: Storing plaintext API keys in your database — always hash with SHA-256 before storage, showing the raw key only once at creation.
- **Works with**: Any HTTP API, any language. Patterns are framework-agnostic.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Hash keys before storage — never store plaintext API keys in your database. Use SHA-256 (for fast lookup) or bcrypt (for extra brute-force resistance). [src1]
- Prefix keys for identification (e.g., `sk_live_`, `pk_test_`) — enables quick classification, leak scanning in git repos, and environment separation. [src2]
- Support key rotation without downtime — allow two active keys per client during the transition window, then revoke the old key. [src3]
- Transmit keys only over HTTPS and only in headers (`Authorization: Bearer <key>`) — never in URL query parameters, which get logged in server access logs and browser history. [src7]
- Scope keys to minimum required permissions — every key should have explicit API and resource restrictions. An unrestricted key is a breach waiting to happen. [src4]

## Quick Reference

| Aspect | Best Practice | Example | Rationale |
|---|---|---|---|
| Generation | Use CSPRNG, 32+ bytes (256 bits entropy) | `crypto.randomBytes(32)` | Prevents brute-force and prediction attacks [src6] |
| Format | Prefix + base62/hex payload | `sk_live_a1b2c3...` (Stripe style) | Prefix enables leak detection and environment routing [src2] |
| Storage | Hash with SHA-256, store only the digest | `SHA-256(key)` in DB, raw key shown once | Compromised DB does not expose usable keys [src1] |
| Transmission | `Authorization: Bearer <key>` header over HTTPS | Never in URL query params | URLs are logged in server logs, proxies, and browser history [src7] |
| Rotation | Dual-key overlap window (7-30 days) | Create new key, migrate, revoke old | Zero-downtime rotation without breaking consumers [src3] |
| Revocation | Immediate hard-delete or soft-delete with TTL | Delete hash from DB, return 401 on next use | Limits blast radius of compromised keys [src5] |
| Scoping | Per-key permission set (read/write/admin) | Google Cloud API restrictions | Least privilege — limits damage from a leaked key [src4] |
| Rate Limiting | Per-key rate limit, not just per-IP | 100 req/min per key, 429 on exceed | Prevents abuse and identifies heavy consumers [src7] |
| Logging | Log key prefix + action, never the full key | `sk_live_a1b2...` accessed `/api/v1/users` | Audit trail without exposing secrets [src5] |
| Expiration | Set TTL (90 days default), require renewal | `expires_at` column in DB | Limits window of exposure for forgotten keys [src5] |

## Decision Tree

```
START
├── Need to identify callers + enforce rate limits only?
│   ├── YES → Simple API key (single secret, SHA-256 hashed)
│   └── NO ↓
├── Need to verify request integrity (signed payloads)?
│   ├── YES → Key + Secret pair with HMAC-SHA256 signing
│   └── NO ↓
├── Need delegated user-level access with scopes?
│   ├── YES → OAuth 2.0 (API key for client ID, OAuth for user auth)
│   └── NO ↓
├── Need both client-side (browser) and server-side keys?
│   ├── YES → Publishable + Secret key pair (Stripe model)
│   └── NO ↓
└── DEFAULT → Single secret API key with SHA-256 hashing + prefix
```

## Step-by-Step Guide

### 1. Design your key format

Choose a prefix scheme that encodes key type + environment. The prefix is the only part you store in plaintext alongside the hash. [src2]

```
Format: {type}_{environment}_{random_payload}

Examples:
  sk_live_a8f2e9c1b4d7...   (secret key, production)
  sk_test_7e3b1a9d5f2c...   (secret key, sandbox)
  pk_live_c4d8a2f1e6b9...   (publishable key, production)
  pk_test_9b1e7c3d5a8f...   (publishable key, sandbox)
```

**Verify**: Your regex `^(sk|pk)_(live|test)_[a-f0-9]{64}$` should match all generated keys.

### 2. Generate cryptographically secure keys

Use your language's CSPRNG. Never use `Math.random()` or `random.random()`. [src6]

```javascript
// Node.js
const crypto = require('crypto');
const prefix = 'sk_live_';
const payload = crypto.randomBytes(32).toString('hex'); // 64 hex chars
const apiKey = `${prefix}${payload}`;
// Result: sk_live_a8f2e9c1b4d7...  (78 chars total)
```

**Verify**: Check entropy — `node -e "console.log(crypto.randomBytes(32).toString('hex'))"` should produce a different value each run.

### 3. Hash and store the key

Store only the SHA-256 hash in your database. Return the raw key to the user exactly once. [src1]

```javascript
const crypto = require('crypto');

function hashApiKey(rawKey) {
  return crypto.createHash('sha256').update(rawKey).digest('hex');
}

// On key creation:
const rawKey = generateApiKey();       // sk_live_a8f2e9...
const keyHash = hashApiKey(rawKey);    // Store this in DB
const prefix = rawKey.slice(0, 12);    // sk_live_a8f2 — store for identification

// INSERT INTO api_keys (key_hash, prefix, client_id, scopes, created_at, expires_at)
// VALUES ($1, $2, $3, $4, NOW(), NOW() + INTERVAL '90 days')
```

**Verify**: `SELECT key_hash FROM api_keys WHERE prefix = 'sk_live_a8f2'` returns a 64-char hex string, not the raw key.

### 4. Validate keys on every request

Extract from header, hash, and look up. [src7]

```javascript
function validateApiKey(req) {
  const authHeader = req.headers['authorization'];
  if (!authHeader?.startsWith('Bearer ')) return null;

  const rawKey = authHeader.slice(7);
  const keyHash = hashApiKey(rawKey);

  // SELECT * FROM api_keys WHERE key_hash = $1 AND revoked_at IS NULL AND expires_at > NOW()
  const keyRecord = await db.query(
    'SELECT * FROM api_keys WHERE key_hash = $1 AND revoked_at IS NULL AND expires_at > NOW()',
    [keyHash]
  );
  return keyRecord.rows[0] || null;
}
```

**Verify**: Send a request with `Authorization: Bearer <valid-key>` — should return 200. Invalid key — should return 401.

### 5. Implement key rotation

Allow two active keys per client. Create the new key, let the client migrate, then revoke the old one. [src3]

```sql
-- Allow 2 active keys per client
ALTER TABLE api_keys ADD CONSTRAINT max_active_keys
  CHECK (/* enforced in application layer */);

-- Rotation: create new key (old stays active)
INSERT INTO api_keys (key_hash, prefix, client_id, scopes, expires_at)
VALUES ($1, $2, $3, $4, NOW() + INTERVAL '90 days');

-- After client confirms migration: revoke old key
UPDATE api_keys SET revoked_at = NOW() WHERE id = $old_key_id;
```

**Verify**: Both old and new keys return 200 during the overlap window. After revocation, old key returns 401.

## Code Examples

### Node.js: Complete API Key Service

> Full script: [node-js-complete-api-key-service.js](scripts/node-js-complete-api-key-service.js) (31 lines)

```javascript
// Input:  client_id, scopes array
// Output: { rawKey, prefix, keyHash, expiresAt }
const crypto = require('crypto');
class ApiKeyService {
  generateKey(type = 'sk', env = 'live') {
# ... (see full script)
```

### Python: API Key Generation and Validation

```python
# Input:  client_id, scopes list
# Output: dict with raw_key, prefix, key_hash, expires_at

import secrets
import hashlib
from datetime import datetime, timedelta

def generate_api_key(key_type="sk", env="live"):
    """Generate a prefixed API key with 256-bit entropy."""
    payload = secrets.token_hex(32)  # 64 hex chars, 256 bits
    raw_key = f"{key_type}_{env}_{payload}"
    key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
    prefix = raw_key[:12]
    return {"raw_key": raw_key, "prefix": prefix, "key_hash": key_hash}

def validate_api_key(raw_key: str, db_cursor) -> dict | None:
    """Validate an API key by hashing and looking up the digest."""
    key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
    db_cursor.execute(
        "SELECT client_id, scopes, expires_at FROM api_keys "
        "WHERE key_hash = %s AND revoked_at IS NULL AND expires_at > NOW()",
        (key_hash,)
    )
    return db_cursor.fetchone()
```

### Go: API Key Generation and Validation

> Full script: [go-api-key-generation-and-validation.go](scripts/go-api-key-generation-and-validation.go) (27 lines)

```go
// Input:  clientID string, scopes []string
// Output: ApiKey struct with RawKey, Prefix, KeyHash
package apikeys
import (
    "crypto/rand"
# ... (see full script)
```

## Anti-Patterns

### Wrong: Storing plaintext API keys

```javascript
// BAD — raw key stored in database
await db.query(
  'INSERT INTO api_keys (api_key, client_id) VALUES ($1, $2)',
  [rawApiKey, clientId]  // rawApiKey in plaintext!
);
```

### Correct: Storing hashed API keys

```javascript
// GOOD — only the SHA-256 hash is stored
const keyHash = crypto.createHash('sha256').update(rawApiKey).digest('hex');
await db.query(
  'INSERT INTO api_keys (key_hash, prefix, client_id) VALUES ($1, $2, $3)',
  [keyHash, rawApiKey.slice(0, 12), clientId]
);
```

### Wrong: Passing API keys in URL query parameters

```
GET /api/v1/users?api_key=sk_live_a8f2e9c1b4d7... HTTP/1.1
```

### Correct: Passing API keys in Authorization header

```
GET /api/v1/users HTTP/1.1
Authorization: Bearer sk_live_a8f2e9c1b4d7...
```

### Wrong: Using Math.random() for key generation

```javascript
// BAD — predictable, not cryptographically secure
const key = 'sk_live_' + Math.random().toString(36).substring(2);
```

### Correct: Using CSPRNG for key generation

```javascript
// GOOD — cryptographically secure random bytes
const key = 'sk_live_' + crypto.randomBytes(32).toString('hex');
```

### Wrong: No expiration or rotation policy

```sql
-- BAD — keys live forever
CREATE TABLE api_keys (
  id SERIAL PRIMARY KEY,
  key_hash TEXT NOT NULL,
  client_id TEXT NOT NULL
  -- no expires_at, no revoked_at!
);
```

### Correct: Built-in expiration and revocation

```sql
-- GOOD — keys have lifecycle management
CREATE TABLE api_keys (
  id SERIAL PRIMARY KEY,
  key_hash TEXT NOT NULL UNIQUE,
  prefix VARCHAR(12) NOT NULL,
  client_id TEXT NOT NULL,
  scopes TEXT[] DEFAULT '{"read"}',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  expires_at TIMESTAMPTZ NOT NULL,
  revoked_at TIMESTAMPTZ,
  last_used_at TIMESTAMPTZ
);
CREATE INDEX idx_api_keys_hash ON api_keys (key_hash) WHERE revoked_at IS NULL;
```

### Wrong: Using same keys across environments

```bash
# BAD — production key used in staging
STAGING_API_KEY=sk_live_a8f2e9c1...   # This is a PRODUCTION key!
```

### Correct: Environment-specific keys with prefixes

```bash
# GOOD — prefix makes environment obvious
STAGING_API_KEY=sk_test_7e3b1a9d...   # Clearly a test key
PROD_API_KEY=sk_live_a8f2e9c1...      # Clearly a production key
```

## Common Pitfalls

- **Key logged in server access logs**: If keys are in URL parameters, they appear in Nginx/Apache logs, CDN logs, and browser history. Fix: always use the `Authorization` header. [src7]
- **No rate limiting per key**: Without per-key rate limits, a single compromised key can DDoS your service. Fix: implement `429 Too Many Requests` with per-key counters (Redis `INCR` + `EXPIRE`). [src7]
- **Showing the raw key again after creation**: If your API has a "show key" endpoint, you are storing the key in a reversible way. Fix: show the key exactly once at creation; for forgotten keys, generate a new one. [src6]
- **Using the same hash for lookup and storage**: If you SHA-256 without a per-key salt, rainbow tables are theoretically possible. Fix: for most API keys (high entropy, 256 bits), unsalted SHA-256 is sufficient; for lower-entropy keys, use bcrypt or add a per-key salt. [src1]
- **Not invalidating keys on team member departure**: When an employee leaves, their personal API keys remain active. Fix: tie keys to user accounts and revoke on deprovisioning. [src5]
- **Missing audit trail**: Without logging which key accessed what, you cannot investigate breaches. Fix: log key prefix + endpoint + timestamp on every request. [src4]

## Diagnostic Commands

```bash
# Check if a key is being sent in URL params (Nginx access log)
grep 'api_key=' /var/log/nginx/access.log | head -20

# Count active (non-expired, non-revoked) keys per client
psql -c "SELECT client_id, COUNT(*) FROM api_keys WHERE revoked_at IS NULL AND expires_at > NOW() GROUP BY client_id ORDER BY count DESC;"

# Find keys that haven't been used in 90+ days (candidates for revocation)
psql -c "SELECT prefix, client_id, last_used_at FROM api_keys WHERE last_used_at < NOW() - INTERVAL '90 days' AND revoked_at IS NULL;"

# Verify a key hash matches (for debugging)
echo -n "sk_live_yourkeyhere" | sha256sum

# Check for leaked keys in git history
git log -p --all -S 'sk_live_' -- '*.js' '*.py' '*.env'

# AWS: list access keys and their age
aws iam list-access-keys --user-name <username>
aws iam get-access-key-last-used --access-key-id <key-id>
```

## Version History & Compatibility

| Standard/Platform | Status | Key Changes | Notes |
|---|---|---|---|
| OWASP API Security Top 10 (2023) | Current | API2:2023 Broken Authentication | Recommends API keys for identification, not sole authentication [src7] |
| Stripe Key Design (2011-present) | Industry standard | Prefixed keys (`sk_`, `pk_`), restricted keys (2020) | Most widely copied key format [src2] |
| AWS IAM Access Keys | Current | Rotation recommended every 90 days | AWS recommends temporary credentials (STS) over long-lived keys [src5] |
| Google Cloud API Keys | Current | Application + API restrictions (2019) | Supports HTTP referrer, IP, and API-level scoping [src4] |
| RFC 6750 (Bearer Tokens) | Current | Defines `Authorization: Bearer` header | Standard transport mechanism for API keys [src7] |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Identifying API consumers for rate limiting and analytics | Authenticating end users with sessions | OAuth 2.0 + session tokens |
| Simple server-to-server authentication where both parties are trusted | Delegating limited access on behalf of a user | OAuth 2.0 authorization code flow |
| Public-facing APIs that need per-client throttling | Request integrity/tampering protection is required | HMAC-SHA256 signed requests |
| Internal microservice identification (low sensitivity) | Highly sensitive financial or health data APIs | Mutual TLS (mTLS) or OAuth 2.0 client credentials |
| Rate limiting and usage tracking per consumer | Fine-grained, per-resource, per-action authorization | RBAC/ABAC with JWT claims |

## Important Caveats

- API keys alone do not authenticate users — they identify callers. For user-level auth, combine with OAuth 2.0 or session tokens. OWASP API Security Top 10 (API2:2023) explicitly warns against using API keys as the sole authentication mechanism. [src7]
- SHA-256 without salt is acceptable for high-entropy keys (256-bit random). For lower-entropy keys or if regulatory compliance demands it, use bcrypt or Argon2 instead. [src1]
- Key prefixes are not secret — they are intentionally stored in plaintext for identification. Do not rely on prefix secrecy for any security property. [src2]
- Cloud provider key management (AWS IAM, Google Cloud API keys) follows the same principles but with platform-specific tooling. The patterns in this unit are implementation-agnostic. [src4] [src5]
- Automated key rotation (e.g., AWS Secrets Manager rotation Lambdas) adds complexity — only adopt if you have more than ~50 keys to manage or compliance requires it. [src5]

## Related Units
<!-- Generated from related_kos frontmatter -->

- [Authentication & Authorization System Design](/software/system-design/auth-system/2026)
- [API Gateway Design](/software/system-design/api-gateway/2026)
- [SSL/TLS Certificate Errors](/software/debugging/ssl-tls-certificate-errors/2026)
