{"slug": "how-to-generate-an-api-key", "title": "How to Generate an API Key", "summary": "GitGuardian found 28.6 million secrets exposed in public GitHub commits in 2025, a 34% jump year over year, with over 1.2 million AI-service credentials alone up 81%. To generate a secure API key, use a cryptographically secure random number generator for 128 bits of randomness, add a type-and-environment prefix like sk_live_, and store only the SHA-256 hash.", "body_md": "# How to Generate an API Key\n\nSince we entered the agentic AI era, APIs and CLIs have been thrust into the zeitgeist. I've been seeing [Wall Street Journal articles](https://www.wsj.com/tech/ai/anthropic-claude-code-ai-7a46460e) mentioning both, which my nerd-self finds very cool.\n\nIf you own a SaaS, it means it is important for you to consider offering an API/CLI, and if so, you need to make sure it is secure or you risk your entire business.\n\nGitGuardian found 28.6 million secrets exposed in public GitHub commits in 2025, a 34% jump year over year. Over 1.2 million of those were AI-service credentials alone, up 81% year over year ([GitGuardian State of Secrets Sprawl 2026](https://blog.gitguardian.com/the-state-of-secrets-sprawl-2026/)).\n\nMost had no prefix, no hashing, and no revocation path, and the key was just \"my-voice-is-my-passport\" — just kidding on that last one, Sneakers fans.\n\nTo generate a secure API key, you use a cryptographically secure random number generator for 128 bits of randomness, add a type-and-environment prefix like `sk_live_`\n\n, and store only the SHA-256 hash.\n\nIf you keep reading, we'll go over the full lifecycle with working code in Node.js, Python, and Go, using Stripe API keys as a blueprint.\n\n- Generate 128 bits of randomness from a CSPRNG\n- Add a type-and-environment prefix like\n`sk_live_`\n\n- Store only the SHA-256 hash, and show the key exactly once\n- Verify in four steps: format, hash, lookup, scope\n- Revoke instantly, and rotate with a grace period\n\n## What Does a Good API Key Look Like?\n\n[Stripe's API keys](https://docs.stripe.com/keys) is one of the most widely recognized and copied formats around. In my previous API company we used this as a model for our API keys.\n\nEvery key encodes three pieces of information before the random token even starts:\n\n```\nsk_live_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6\n│  │     │\n│  │     └─ Random token (the secret)\n│  └─────── Environment (live or test)\n└────────── Type (sk = secret, pk = publishable)\n```\n\nThis design is intentional, and I personally love it for the expressiveness and simplicity.\n\nWhen GitHub overhauled their own token formats, they added identifiable prefixes (`ghp_`\n\n, `gho_`\n\n, `ghs_`\n\n) for the same reason: the old hex-only tokens were \"indistinguishable from other encoded data like SHA hashes\" and nearly impossible for scanners to detect ([GitHub Engineering](https://github.blog/engineering/platform-security/behind-githubs-new-authentication-token-formats/)).\n\nThe same convention plays out across every major API:\n\n| Provider | Format | Examples | What the Prefix Tells You |\n|---|---|---|---|\n| Stripe | `{type}_{env}_{token}` | `sk_live_` , `pk_test_` , `rk_live_` | Key type + environment |\n| GitHub | `{co}{type}_{token}` | `ghp_` , `gho_` , `ghs_` | Company + token type |\n| Twilio | `{type}{token}` | `SK` + 32 hex chars | Key type |\n| AWS | `{scope}{token}` | `AKIA` , `ASIA` | Permanent vs. session |\n\nThe pattern: **a human-readable prefix, then cryptographically random bytes.** The examples below encode that token as hex, which is what `randomBytes`\n\nreturns by default.\n\nStripe's real keys use a longer base62 alphabet; if you copy their format exactly, widen the validation regex from `[0-9a-f]`\n\nto `[A-Za-z0-9]`\n\n.\n\n### Publishable vs. Secret Keys\n\nStripe splits keys into two categories because the trust boundary matters:\n\nSafe to embed in frontend JavaScript. These keys can only create tokens (e.g., tokenize a credit card via Stripe.js). They can't read customer data, issue refunds, or make charges.`pk_live_`\n\n/`pk_test_`\n\n(publishable).Full API access. Server-side only. If this key leaks, an attacker can move money.`sk_live_`\n\n/`sk_test_`\n\n(secret).\n\nNow, you may not have this situation and only have a back-end process or only front-end access. When you're designing your own API, ask: does this client need full access, or just enough to submit data?\n\nThat answer determines whether to issue a `pk_`\n\nor `sk_`\n\nkey. Whether you are back-end access only or front-end access only, you should still consider this pattern.\n\nFor more on how APIs work under the hood, see our guide on [what an API actually is](https://www.jamdesk.com/blog/what-is-an-api?utm_source=blog&utm_medium=article&utm_campaign=how-to-generate-api-key).\n\n## Step 1: Generate Cryptographically Random Bytes\n\nThe entropy source matters more than anything else. Use your platform's CSPRNG (Cryptographically Secure Pseudorandom Number Generator — say that 3 times fast). Don't use `Math.random()`\n\nor `uuid.v4()`\n\n, and definitely not timestamps.\n\n**Node.js:**\n\n``` js\nimport { randomBytes } from 'crypto';\n\nconst token = randomBytes(16).toString('hex');\n// → \"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6\" (32 hex chars, 128 bits)\n```\n\n**Python:**\n\n``` python\nimport secrets\n\ntoken = secrets.token_hex(16)\n# → \"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6\"\n```\n\n**Go:**\n\n```\nimport (\n    \"crypto/rand\"\n    \"encoding/hex\"\n    \"fmt\"\n)\n\nfunc generateToken() (string, error) {\n    b := make([]byte, 16)\n    if _, err := rand.Read(b); err != nil {\n        return \"\", fmt.Errorf(\"CSPRNG failed: %w\", err)\n    }\n    return hex.EncodeToString(b), nil\n    // → \"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6\"\n}\n```\n\nSixteen bytes gives you 128 bits of entropy: 3.4 × 10³⁸ possible values. Brute-forcing that at a billion guesses per second would take 10²² years — unless the newfangled quantum computers become generally available, and then all bets are off.\n\nWhy not UUIDs? A v4 UUID gives you 122 random bits, which is plenty of entropy, and `crypto.randomUUID()`\n\nis CSPRNG-backed, so randomness isn't the problem.\n\nFormat control is: the fixed `8-4-4-4-12`\n\nshape leaks structure, can't carry a type-and-environment prefix, and forces dashes into your tokens. Raw `randomBytes`\n\nis the better fit when you want prefixes and a compact format.\n\n## Step 2: Add Environment-Aware Prefixes\n\nYou might — well, should — have at least two types of keys. One for your users' live site and one for the sandbox. If you don't have a sandbox yet, this is still a good practice since you may one day.\n\nWith Stripe's `{type}_{environment}_`\n\npattern every key becomes self-documenting and describes the environment:\n\n``` js\nimport { randomBytes } from 'crypto';\n\nfunction generateApiKey(type, environment) {\n  const token = randomBytes(16).toString('hex');\n  return `${type}_${environment}_${token}`;\n}\n\n// Publishable keys — safe for client-side code\ngenerateApiKey('pk', 'live');\n// → \"pk_live_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6\"\n\ngenerateApiKey('pk', 'test');\n// → \"pk_test_f7e8d9c0b1a2f3e4d5c6b7a8f9e0d1c2\"\n\n// Secret keys — server-side only, never expose\ngenerateApiKey('sk', 'live');\n// → \"sk_live_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d\"\n\ngenerateApiKey('sk', 'test');\n// → \"sk_test_9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c\"\n```\n\nStripe also offers restricted keys (`rk_live_`\n\n, `rk_test_`\n\n) with scoped permissions. If your API needs granular access control, that is a third type you can add.\n\nJust register its prefix in the validation patterns (Step 4) and the Express middleware below, or every restricted key will fail the format check.\n\nThe prefix is great because:\n\n**Secret scanning.** GitHub, GitGuardian, and TruffleHog all match on known prefixes. A bare hex string flies under every scanner. A prefixed key triggers alerts within minutes of being pushed — especially if it is published on a public repo.**Debugging speed.** When`pk_test_`\n\nappears in production logs, you know the client is misconfigured. Stripe gives some great error messaging if you try to use a live key in sandbox and vice versa.**Cheap validation.** A regex check rejects malformed tokens before they touch your datastore. You can do this right in your middleware. More on this in Step 4.\n\n## Step 3: Hash Before You Store\n\nOne rule for all production systems: **never store the plaintext key.** Store its SHA-256 hash in your database. If you are in the EU (GDPR), want to be SOC 2 compliant, or get Enterprise clients, you'll be asked this.\n\n``` js\nimport { createHash, randomBytes } from 'crypto';\n\nfunction hashApiKey(rawKey) {\n  return createHash('sha256').update(rawKey).digest('hex');\n}\n\n// At generation time:\nconst rawKey = generateApiKey('sk', 'live');\nconst hash = hashApiKey(rawKey);\n\n// Store `hash` in your database\n// Return `rawKey` to the user exactly ONCE\nconsole.log('Your API key (copy it now — you won\\'t see it again):');\nconsole.log(rawKey);\n\n// What gets saved to the database:\n// { id: \"key_8f3a\", hash: \"7f83b1657ff1fc53...\", prefix: \"sk_live\", last4: \"c5d6\", createdAt: 1713196800000 }\n```\n\n**Python equivalent:**\n\n``` php\nimport hashlib\n\ndef hash_api_key(raw_key: str) -> str:\n    return hashlib.sha256(raw_key.encode()).hexdigest()\n```\n\nIf you're thinking about using bcrypt, don't. API keys aren't passwords. They already carry 128 bits of entropy, so rainbow tables and dictionary attacks don't apply.\n\nSHA-256 is fast, deterministic, and produces a fixed 64-character hex digest that works well as a database lookup key. Bcrypt's intentional slowness would add latency to every API request for zero security gain.\n\nFor one more line of defense, hash with **HMAC-SHA256 and a server-side pepper** instead of plain SHA-256.\n\nIf your hash table ever leaks, plain digests are directly verifiable by anyone who can generate candidate keys; an HMAC pepper makes a stolen table useless without the secret:\n\n``` js\nimport { createHmac } from 'crypto';\n\n// PEPPER comes from your secret manager, never the database\nconst hashApiKey = (rawKey) =>\n  createHmac('sha256', process.env.API_KEY_PEPPER).update(rawKey).digest('hex');\n```\n\nHash incoming tokens the same way at verify time and the rest of the flow is unchanged.\n\nStripe, GitHub, and Twilio all follow this show-once pattern. If a user loses their key, they can't recover it and they must revoke and regenerate. A bit of user friction, but the right way for security.\n\nStore one non-secret hint alongside the hash: the prefix plus the last four characters (Stripe shows `sk_live_…c5d6`\n\n). You can't display the key again, so without that hint your dashboard can't tell two keys apart in a list.\n\nLong-lived secrets account for 60% of credential policy violations, according to GitGuardian's State of Secrets Sprawl 2026 report ([Help Net Security](https://www.helpnetsecurity.com/2026/04/14/gitguardian-ai-agents-credentials-leak/), 2026). Hashing is the cheapest mitigation you'll ever ship.\n\n## Step 4: Verify Keys at Runtime\n\nWhen a request arrives with a [ Bearer token](https://blog.postman.com/what-is-a-bearer-token/), run four checks in this order:\n\n**Validate the format**(regex, no I/O)** Hash the token**(SHA-256, CPU only)** Look up the hash**(one DB or Redis read)** Check scope**(project, environment, permissions)\n\n``` js\nimport { createHash } from 'crypto';\n\nconst KEY_PATTERN = /^sk_(live|test)_[0-9a-f]{32}$/;\n\nasync function verifyApiKey(rawKey, expectedProject) {\n  // 1. Format gate — rejects garbage before any I/O\n  if (!KEY_PATTERN.test(rawKey)) {\n    return { ok: false, reason: 'invalid_format' };\n  }\n\n  // 2. Hash the incoming token (~0.01ms, CPU only)\n  const hash = createHash('sha256').update(rawKey).digest('hex');\n\n  // 3. Single O(1) lookup from Redis or your DB\n  const record = await redis.get(`apikey:${hash}`);\n  if (!record) {\n    return { ok: false, reason: 'invalid_key' };\n  }\n\n  // 4. Scope check — does this key belong to this project?\n  const data = typeof record === 'string' ? JSON.parse(record) : record;\n  if (data.projectId !== expectedProject) {\n    return { ok: false, reason: 'wrong_project' };\n  }\n\n  return { ok: true, id: data.id };\n}\n```\n\nNotice the regex targets `^sk_`\n\n, not `^(pk|sk)_`\n\n. Narrow the pattern to the key type your endpoint expects, so a server-side search API should reject publishable keys and a client-side tokenization endpoint should reject secret keys. Be sure to use one regex per endpoint type.\n\nFor high-throughput APIs, put the hash lookup in Redis rather than your primary database for faster in-memory lookups. The read is O(1) by hash, handles thousands of requests per second, and revocation is just a `DEL`\n\non the key.\n\nI recommend treating Redis as a **cache, not the system of record**. I believe your DB should always be the system of record, and I've seen more than one Redis cache get unintentionally wiped.\n\nFor example, if Redis runs an eviction policy like `allkeys-lru`\n\n, a still-valid key can be evicted and a paying customer gets a spurious `invalid_key`\n\n; on a cache miss, fall through to your database and repopulate, or pin key records with `noeviction`\n\nor a dedicated instance.\n\n## Step 5: Revoke and Rotate\n\nNote: some of this is more advanced, so you can skip or breeze through this section.\n\nYour users will need to revoke their keys at some point, or you'll want to do it on their behalf.\n\nRevocation has one detail the other steps don't: the caller has the key's `id`\n\n(from your dashboard or admin API), not the raw key or its hash.\n\nThe raw key was shown once at creation and never stored. So the flow is:\n\n- Look up the record by\n`id`\n\nto retrieve the stored`hash`\n\n. - Delete\n`apikey:{hash}`\n\nfrom your store so verify calls fail immediately. And don't forget removing from Redis or your cache. - Mark the record as revoked for your audit trail.\n\n```\nasync function revokeKey(store, db, keyId) {\n  // 1. Find the stored hash via the key's management ID\n  const record = await db.findKeyById(keyId);\n  if (!record) throw new Error('Key not found');\n\n  // 2. Remove the hot-path lookup — future verify calls return invalid_key\n  await store.del(`apikey:${record.hash}`);\n\n  // 3. Mark as revoked for auditing\n  await db.updateKey(keyId, {\n    enabled: false,\n    revokedAt: Date.now(),\n  });\n}\n```\n\nIf you really want to get advanced, for rotation, generate the new key *before* revoking the old one. Store both hashes in parallel during a grace period, then delete the old hash.\n\nStripe gives you a configurable expiration window for the outgoing key. You can build the same into your admin API so clients don't break mid-request.\n\nOne caveat: a grace period is for *planned* rotation. If a key is actually compromised, skip the overlap and revoke it immediately, because the old key keeps its full scope for as long as both hashes are live.\n\nIf you add key expiry on top of rotation, a scheduled sweep that disables stale keys is all you need — a simple cron job covers it.\n\n## Express Middleware\n\nIf you're using Node.js Express, `verifyApiKey`\n\nbecomes real middleware with one wrapper function:\n\n``` js\n// middleware/api-auth.js\nimport { createHash } from 'crypto';\nimport { redis } from '../lib/redis.js';\n\nconst PATTERNS = {\n  secret:      /^sk_(live|test)_[0-9a-f]{32}$/,\n  publishable: /^pk_(live|test)_[0-9a-f]{32}$/,\n  restricted:  /^rk_(live|test)_[0-9a-f]{32}$/,\n};\n\nexport function requireKey(type = 'secret') {\n  const pattern = PATTERNS[type];\n\n  return async (req, res, next) => {\n    const token = req.headers.authorization?.replace('Bearer ', '');\n\n    if (!token || !pattern.test(token)) {\n      return res.status(401).json({ error: 'invalid_key_format' });\n    }\n\n    const hash = createHash('sha256').update(token).digest('hex');\n    const raw = await redis.get(`apikey:${hash}`);\n\n    if (!raw) {\n      return res.status(401).json({ error: 'invalid_key' });\n    }\n\n    const data = typeof raw === 'string' ? JSON.parse(raw) : raw;\n    req.apiKey = { id: data.id, projectId: data.projectId, type };\n    next();\n  };\n}\n```\n\nWire it into your routes:\n\n``` python\nimport express from 'express';\nimport { requireKey } from './middleware/api-auth.js';\n\nconst app = express();\napp.use(express.json());\n\n// Server-side endpoint — only sk_live_ and sk_test_ keys accepted\napp.post('/v1/search', requireKey('secret'), (req, res) => {\n  console.log(`Authenticated key: ${req.apiKey.id}`);\n  res.json({ results: ['...'] });\n});\n\n// Client-side endpoint — only pk_live_ and pk_test_ keys accepted\napp.post('/v1/tokenize', requireKey('publishable'), (req, res) => {\n  res.json({ token: 'tok_...' });\n});\n```\n\nTest it with curl:\n\n```\ncurl -X POST http://localhost:3000/v1/search \\\n  -H \"Authorization: Bearer sk_live_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"query\": \"test\"}'\n```\n\nOr from a client app:\n\n``` js\nconst res = await fetch('https://api.example.com/v1/search', {\n  method: 'POST',\n  headers: {\n    'Authorization': `Bearer ${process.env.API_SECRET_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({ query: 'test' }),\n});\n```\n\nKeep keys in environment variables, never in source code:\n\n```\n# .env — add this file to .gitignore BEFORE your first commit\nAPI_SECRET_KEY=sk_live_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6\n```\n\n## 6 Mistakes That Get API Keys Leaked\n\n**Skipping the prefix.** Millions of commits flow into public repositories every day. Without a recognizable pattern, your leaked key is invisible to every scanner on the planet. Eight characters of prefix buys you automatic detection.\n\n**Hardcoding keys in frontend bundles.** An `sk_live_`\n\ntoken in a React component ships to every browser that loads your app. Use `pk_`\n\nkeys client-side and keep `sk_`\n\nkeys behind server-side environment variables.\n\n**Logging the raw key in error handlers.** A failed auth check that logs `token=${rawKey}`\n\nputs plaintext credentials in your log aggregator, searchable by anyone with Datadog access. Log the key's `id`\n\nfield instead.\n\n**Committing .env files.** Your\n\n`.env`\n\nbelongs in `.gitignore`\n\nbefore the first commit. GitGuardian found 24,008 unique secrets exposed in MCP configuration files in 2025 alone ([GitGuardian State of Secrets Sprawl 2026](https://blog.gitguardian.com/the-state-of-secrets-sprawl-2026/)).\n\n**No rate limit on the verify path.** The keyspace is too large to brute-force, but that is not the real attack. Leaked and stuffed keys get replayed against your auth endpoint thousands of times a minute.\n\nThrottle failed verifications per IP and per key prefix so a stolen key trips a limit instead of running free.\n\n**No revocation endpoint.** Build one on day one, even if nobody uses it yet.\n\n99% of surveyed organizations hit at least one API security issue in the prior 12 months ([CybelAngel](https://cybelangel.com/blog/the-api-threat-report-2025/), 2025). When a key leaks, and statistically it will, a single `DEL`\n\ncommand is all that stands between the attacker and your data.\n\nAnd when a leak forces a mass revocation, treat it as an incident that need to be communicated to affected customers.\n\n## What to Build Next\n\nFive functions cover the entire key lifecycle: generate, hash, store, verify, revoke. Stripe runs this architecture at massive scale, so you know it works.\n\nIf you're building API documentation alongside your keys, [Jamdesk](https://www.jamdesk.com/?utm_source=blog&utm_medium=article&utm_campaign=how-to-generate-api-key) turns your OpenAPI spec into interactive docs with a built-in playground, so developers can test endpoints with their own keys. For a wider look at the options, see our roundup of [the best API documentation tools](https://www.jamdesk.com/blog/best-api-documentation-tools?utm_source=blog&utm_medium=article&utm_campaign=how-to-generate-api-key).\n\nIn summary, start with `pk_test_`\n\nand `sk_test_`\n\n. Ship the generate and verify endpoints, then add rotation when you need it.", "url": "https://wpnews.pro/news/how-to-generate-an-api-key", "canonical_source": "https://www.jamdesk.com/blog/how-to-generate-api-key", "published_at": "2026-07-08 18:40:16+00:00", "updated_at": "2026-07-30 16:39:32.075854+00:00", "lang": "en", "topics": ["ai-infrastructure", "developer-tools"], "entities": ["GitGuardian", "GitHub", "Stripe", "Twilio", "AWS", "Wall Street Journal"], "alternates": {"html": "https://wpnews.pro/news/how-to-generate-an-api-key", "markdown": "https://wpnews.pro/news/how-to-generate-an-api-key.md", "text": "https://wpnews.pro/news/how-to-generate-an-api-key.txt", "jsonld": "https://wpnews.pro/news/how-to-generate-an-api-key.jsonld"}}