An API key is a long random string that your service hands to a customer and later accepts as proof of who is calling. Generating one correctly takes a single line of code. Designing one that is safe to store, easy to recognise in a leak and painless to rotate takes a few more decisions — and they are much harder to change once keys are in the wild.
This guide covers both: the line of code, and the decisions around it.
The one rule: use a CSPRNG
Every API key must come from a cryptographically secure pseudo-random number generator (CSPRNG). These are seeded by the operating system from hardware entropy, and their output cannot be predicted from previous output.
The generators you reach for by habit are not secure. Math.random() in JavaScript, Python's random module and Go's math/rand are designed for simulations: given enough output, an attacker can reconstruct the internal state and predict every future key. Timestamps, UUIDv1 and hashes of user IDs are not secrets at all.
How long should an API key be?
256 bits (32 bytes) of randomness. That is the working default across the industry, and there is rarely a reason to deviate.
| Random bytes | Bits | Hex length | Base64URL length | Verdict |
|---|---|---|---|---|
| 16 | 128 | 32 | 22 | Minimum acceptable |
| 24 | 192 | 48 | 32 | Fine |
| 32 | 256 | 64 | 43 | Recommended default |
| 64 | 512 | 128 | 86 | No practical gain |
128 bits is already beyond brute force, but 256 gives margin against a weak implementation elsewhere and costs nothing. Going beyond 256 makes the key longer without making any real attack harder. You can check the strength of an existing key with the entropy calculator.
Note that the *length of the string* is not the strength. A 64-character hex key contains 256 bits; a 64-character key drawn only from digits contains about 212. Always reason in bytes of randomness, then pick an encoding.
Generating the key
Node.js
import { randomBytes } from "node:crypto";
// 32 bytes = 256 bits, encoded as URL-safe Base64 (43 characters)
const key = randomBytes(32).toString("base64url");The base64url encoding needs Node.js 16 or later. Use toString("hex") for a 64-character hex key instead.
Python
import secrets
key = secrets.token_urlsafe(32) # 43 URL-safe characters
hex_key = secrets.token_hex(32) # 64 hex charactersThe secrets module exists precisely for this. Never use random.choice to build a key.
Go
import (
"crypto/rand"
"encoding/base64"
)
func NewAPIKey() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}Import crypto/rand, not math/rand. The compiler will not warn you if you get it wrong.
Command line
openssl rand -hex 32
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='Design the format, not just the bytes
A bare 43-character string works, but every serious API provider now wraps the random part in a recognisable structure. Compare a random blob with a key like sk_live_ followed by random characters: the second tells an engineer, a log scanner and GitHub's secret scanning exactly what they are looking at.
A good key format has three parts:
- A prefix naming the issuer and the key type, such as
acme_live_oracme_test_. Separate live and test keys so a test key can never be mistaken for production. Prefixes add no entropy — they are pure metadata. - The random body, 32 bytes from a CSPRNG, encoded as Base62 or Base64URL so it survives copy-paste, URLs and double-click selection.
- An optional checksum. GitHub's tokens end in a 6-character CRC32 checksum of the body. A scanner can verify the checksum offline and discard false positives without ever calling the API.
The custom prefix key builder lets you prototype a format and see what it looks like before you commit to it. The API key format reference shows the formats real providers use.
Avoid characters that cause trouble later: +, / and = break URLs, and visually ambiguous characters (0/O, l/1) matter if a human ever reads a key aloud.
Store a hash, never the key
Treat an API key like a password: your database should not be able to give it back. If it can, a SQL injection or a leaked backup hands an attacker working credentials for every customer.
The standard pattern:
- Generate the key and show it to the customer once.
- Store
SHA-256(key)and a short non-secret identifier (often the prefix plus the first few characters) so the customer can recognise the key in your dashboard. - On each request, hash the presented key and look up the hash.
import { createHash } from "node:crypto";
const hashKey = (key) => createHash("sha256").update(key).digest("hex");
// On creation
await db.apiKeys.insert({ hash: hashKey(key), hint: key.slice(0, 12), userId });
// On each request
const record = await db.apiKeys.findOne({ hash: hashKey(presentedKey) });Why SHA-256 and not bcrypt or Argon2? Slow password hashes exist because human passwords are guessable. A 256-bit random key is not, so a fast hash is already unbreakable — and a slow hash would add tens of milliseconds to every single API request. If you want defence in depth against a leaked database, use HMAC-SHA256 with a server-side secret ("pepper") instead of a bare hash.
Because the lookup is by hash, you also avoid comparing secrets character by character, which removes the classic timing side channel.
Scope, expiry and rotation
Generating the key is the start of its lifecycle, not the end:
- Scope it. A key that can only read invoices does far less damage when leaked than one with full account access. Let customers create several narrowly scoped keys instead of one master key.
- Let it expire. Optional expiry dates, with reminders, turn forgotten keys into dead keys.
- Support two active keys. Rotation without downtime needs an overlap period where both the old and the new key work. The rotation planner lays out a schedule.
- Record last use. A "last used" timestamp tells a customer which keys are safe to delete.
- Revoke instantly. Since you store hashes, revocation is a single row delete or flag update.
Issuing keys in bulk
If you need keys for a migration, a test fixture or a batch of partner accounts, generate them in one pass rather than looping over a web form. The bulk API key generator produces up to hundreds of keys and exports them as CSV — still entirely in your browser.
Frequently asked questions
Is a UUID good enough as an API key?
A UUIDv4 contains 122 random bits, which is technically enough, but it is a poor format: many libraries generate UUIDs with non-cryptographic randomness, the format has no prefix for scanners to recognise, and UUIDv1 and v7 contain timestamps rather than pure randomness. Generate 32 random bytes instead.
Should API keys be encrypted or hashed in the database?
Hashed. Encryption is reversible, so anyone who obtains the encryption key along with the database recovers every API key. A hash cannot be reversed, and you never need the original key back — only the ability to check a presented key against it.
How do I show an API key again if I only store the hash?
You do not. Show it once at creation with a clear copy button and a warning, then display only a hint such as the prefix and first characters. If the customer loses it, they create a new key and revoke the old one. This is exactly how Stripe, GitHub and AWS behave.
What is the difference between an API key and a secret key?
Many providers issue a pair: a publishable or public key that identifies the account and may appear in client code, and a secret key that authorises privileged actions and must stay on the server. If your API needs both, give them distinct prefixes so they cannot be confused.
Can I generate API keys in the browser?
Yes, using crypto.getRandomValues, which draws from the same operating-system CSPRNG a server would use. It is fine for generating keys you will paste into configuration. For keys your service issues to customers, generate them server-side so the key is created where it will be hashed and stored.