Guide · Backend
API Key Management Explained: Formats, Hashing, Rotation and Leaks
25 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- API key management is everything you do with the keys your API hands to customers: create them, store them, limit them, rotate them and cancel them when they leak.
- Give every key a prefix, a short label such as apk_live_ that says who issued it, and a checksum, a few extra characters that show the key is intact. Scanners can then spot leaked keys with very few false alarms.
- Store only a hash of each key, a one-way fingerprint that cannot be turned back into the key. Show the full key to the customer once, then forget it.
- Rotate without downtime by letting the old and new key work side by side for a short overlap. On leak day, revoke at once, with no overlap.
- Beginners can stop after the section on scopes and expiry. Engineers get tested code for the checksum, hashed storage with a server-side secret, timing-safe comparison, a rotation drill and a signed leak-report receiver.
What goes wrong when you hand out API keys without a plan?
Keys leak, and you cannot tell which one, what it could do, or how to replace it without breaking a customer. API key management is the plan that fixes all three.
Picture a common setup. A small team in Bengaluru runs an invoicing API that about 300 business customers call from their own servers. The setup is made up, but every test result below came from our own runs.
Their first version was simple. Each key was a random 40-character hex string, and the database stored it as plain text in a column called api_key. Every key could do everything, and no key ever expired.
Then a customer pushed a config file to a public repository. Nothing in the leaked string said it belonged to this API, so nobody noticed for weeks. When they did, cancelling the key broke the customer's other servers, which used the same key.
This guide covers issuing keys to your API's customers. Storing secrets your own services use, such as a database password, is a different job. SevyDevy's guide on secrets management covers that side.
Which words do you need before learning API key management?
Eight words carry the whole guide. Each one is tied to the invoicing API.
- API key: a long random string a customer's program sends with each request, so your API knows which customer is calling.
- Secret part: the random characters in the key that nobody can guess. Everything else in the key can be public.
- Scope: a permission attached to a key, such as invoices:read. A key can do only what its scopes allow.
- Hash: a one-way fingerprint of some data. The same key always gives the same hash, but the hash cannot be turned back into the key.
- Pepper: a secret value the server mixes into every hash, using HMAC, a standard way to hash with a secret. It lives in a secrets manager, not in the database.
- Checksum: a few characters computed from the rest of the key. If one character changes, the checksum no longer matches.
- Rotation: replacing a key with a new one on purpose, ideally while both work for a short time.
- Revocation: cancelling a key so every later request with it fails, usually because it leaked.
What is API key management and what does it cover?
API key management is the full life of a key, from the moment you create it to the moment it stops working. The OWASP Secrets Management Cheat Sheet names four stages: creation, rotation, revocation and expiration.
For an API that issues keys, two more jobs sit around those stages. You decide how the key is stored, and you decide what it may do. The table maps each stage to the invoicing API's first, naive version.
| Stage | What you decide | The naive version |
|---|---|---|
| Create | Format, length, randomness, prefix | 40 random hex characters, no prefix |
| Store | What the database keeps | The full key in plain text |
| Limit | Scopes, expiry, allowed networks | Every key could do everything, forever |
| Check | How each request is matched to a key | A plain string comparison in SQL |
| Rotate | How a customer moves to a new key | Cancel the old key and hope |
| Revoke | Who can cancel a key, and how fast | A manual database edit after an email |
The life of one API key
Drag to exploreNotice that the database only ever sees a fingerprint, and that a leak report jumps straight to revocation without waiting for the normal rotation path.
Diagram connections:
- Issue key to Show once (full key, once)
- Issue key to Store fingerprint (fingerprint only)
- Store fingerprint to Check each request (looked up per request)
- Check each request to Rotate (planned change)
- Rotate to Revoke (after the overlap)
- Leak report to Revoke (immediately)
- Issue key: Random secret plus prefix and checksum
- Show once: Customer copies it; you never show it again
- Store fingerprint: HMAC digest, key ID, last 4 chars
- Check each request: Checksum, digest, scope, expiry
- Rotate: New key; old one works for a few days
- Leak report: Scanner finds the key in public code
- Revoke: Every later request fails with 401
An everyday picture helps with the whole cycle. A hotel gives each guest a key card. The card is the key, the rooms it opens are its scopes, and checkout time is its expiry. A lost card gets cancelled and replaced, which is revocation.
Where the picture breaks: a hotel cannot recognise its own card lying on a pavement. An API key can carry a printed label, its prefix, so anyone who finds it knows whose it is. Copying a key also takes one paste, and leaves no trace.
When should you use API keys instead of OAuth?
Use an API key when one program calls your API on its owner's behalf, such as a customer's billing server. Use OAuth, a standard for letting users grant an app limited access, when a person must log in or approve access.
OWASP's API Security Top 10 (2023 edition) is direct about this. Under API2, broken authentication, it says API keys should not be used for user authentication. They should only be used to authenticate API clients, meaning the calling programs.
An API key says which customer account is calling. It does not prove which person is at the keyboard. SevyDevy's guide comparing OAuth, OIDC and SAML covers the user side; this guide stays with the program-to-program keys most business APIs still run on.
What should a good API key look like?
A good key has four parts: a prefix that names the issuer and environment, a public key ID, a long random secret and a checksum. Each part solves one problem from the naive version.
GitHub explained the idea in a 2021 engineering post, updated in 2023, on its token formats. Its personal access tokens start with ghp_, OAuth tokens with gho_, and refresh tokens with ghr_. The gh names the company and the next letter names the token type.
The post gives three reasons for the details. The underscore is not in the standard Base64 alphabet, Base64 being a common way to write binary data as text. So random encoded data rarely contains it by accident. Double-clicking selects the whole token. The last 6 characters are a checksum made with CRC32, a standard error-detecting code, written in Base62 with leading zeros.
Base62 means the 62 characters 0 to 9, A to Z and a to z. Each one carries about 5.95 bits of randomness, against 4 bits for a hex character. GitHub's post shows its OAuth tokens rising from 160 to 178 bits without getting longer.
| Part | Example | Why it is there |
|---|---|---|
| Prefix | apk_live_ | Says who issued it and live vs test. Scanners and humans recognise it. |
| Key ID | 8 Base62 characters | Public. Finds the database row and can appear in logs and dashboards. |
| Secret | 32 Base62 characters | About 190.5 bits of randomness. Never stored, never logged. |
| Checksum | 6 Base62 characters | CRC32 of everything before it. Catches typos and fake look-alikes offline. |
Here is the generator we ran on Python 3.13.18, standard library only. Python's docs say the secrets module uses the most secure source of randomness the operating system provides. That is what makes the secret unguessable.
import secrets
import string
import zlib
ALPHABET = string.digits + string.ascii_uppercase + string.ascii_lowercase
PREFIX = "apk_live_"
def b62(n, width=6):
out = ""
while n:
n, r = divmod(n, 62)
out = ALPHABET[r] + out
return out.rjust(width, "0")
def rand62(n):
return "".join(secrets.choice(ALPHABET) for _ in range(n))
def checksum(body):
return b62(zlib.crc32(body.encode()))
def new_key():
key_id = rand62(8)
body = f"{PREFIX}{key_id}_{rand62(32)}"
return key_id, body + checksum(body)
def parse(key):
if not key.startswith(PREFIX) or len(key) != 56:
return None
body, check = key[:-6], key[-6:]
return key[9:17] if checksum(body) == check else None
key_id, key = new_key()
print(key, len(key))
print(parse(key), parse(key[:-1] + "x"))It printed a random 56-character key, then its key ID and None. A key in this shape looks like apk_live_EXAMPLE1_FAKEfakeFAKEfakeFAKEfakeFAKEfake0iuZPs, and its last 6 characters are a valid checksum. The None appears because changing the last character broke the checksum. Every key in this guide is fake.
Pause and predict: out of about 2,000 keys with exactly one character changed, how many slip past a 6-character checksum? Make a guess before reading on.
None did. Our test rejected 1955 of 1955 single-character typos and 1977 of 1977 swaps of two neighbouring characters. That is a random sample, not a proof, but it is the job a checksum exists for.
A checksum is not security, because anyone can compute it. It only proves the key is well-formed, which lets your API and scanners reject junk without a database lookup.
How do secret scanners catch leaked API keys?
A scanner searches code and files with a regular expression, a text pattern, for each known key format. A distinctive prefix makes that pattern precise, and a checksum lets the scanner drop near-misses.
We tested this on a fake config file holding a Git commit hash, an old-style 40-character hex token, one real-shaped key and one look-alike key from documentation. The pattern for apk_live_ keys found two strings, and the checksum dropped the look-alike.
A 40-hex pattern for the old token matched both the token and the commit hash, and could not tell them apart. That is the problem GitHub's post describes: old tokens looked like other encoded data such as SHA hashes.
We also built a million random strings in the right shape. None passed the checksum, which matches the odds of about 1 in 57 billion for a random 6-character tail.
How GitHub's partner program sends you leaked keys
GitHub runs a secret scanning partner program for companies that issue keys. As of 29 October 2026, its docs say a partner supplies a regular expression for each key type and a public, internet-accessible endpoint.
When a match turns up in a public repository or a public package on npm, the JavaScript package registry, GitHub sends that endpoint an HTTP POST. The body is a JSON array, and each entry has the token, its type, a URL and a source such as commit.
The docs recommend three things for key formats: a unique prefix, high-entropy random strings and a 32-bit checksum. They also say to treat every reported key as public and compromised.
How should you store API keys in a database?
Store a hash of each key, never the key itself. Then a stolen database copy, an over-curious engineer or a leaked backup does not hand anyone a working key.
This is why many providers show a key only once. As of 29 October 2026, Stripe's API keys page says a live secret key you create is shown one time and cannot be retrieved later. If it is lost, you rotate or delete it.
Keep a few safe fields next to the hash: the key ID, the last 4 characters, the scopes, the creation time and a last-used time. Dashboards can show apk_live_ plus the ID and last 4, which is enough for customers to tell keys apart.
Why a fast hash is fine for random keys
For passwords, a fast hash is a mistake. OWASP's Password Storage Cheat Sheet says fast hashes such as SHA-256, a widely used general-purpose hash, are unsuitable because attackers can try guesses very quickly. People pick guessable passwords, so speed matters.
A random 190-bit secret is not guessable at any speed. NIST SP 800-63B-4, final since July 2025, sets this rule for look-up secrets, another kind of random authentication code: store them hashed with an approved hash function. Only those under 112 bits need a salted password hashing scheme.
NIST does not name API keys in that rule. We apply it by analogy, because an API key is also a random secret the server issues, not one a person chooses.
Our numbers show the trade. One core did about 779,000 SHA-256 hashes a second, at 1.28 microseconds each. scrypt, a deliberately slow password hash, at OWASP's settings of N=2^17, r=8 and p=1 took 949 milliseconds per hash.
A slow hash would add almost a second to every API request and buy nothing. Even at an assumed trillion guesses a second, finding a 190.5-bit secret takes about 3.6 times 10 to the power 37 years on average.
| Option | Time per check in our run | Verdict for random API keys |
|---|---|---|
| Plain text key | None | Never. A database leak becomes a key leak. |
| SHA-256 of the key | 1.28 microseconds | Fine for 112 bits or more of randomness. |
| HMAC-SHA-256 with a pepper | 4.71 microseconds | Better. A stolen database alone cannot check guesses. |
| Slow password hash (scrypt, Argon2id) | 949 milliseconds for scrypt | Wrong tool. Built for guessable passwords; too slow per request. |
What the pepper adds
HMAC-SHA-256 is a hash that also takes a secret key; here that key is the pepper. Without the pepper, nobody can compute or check a stored digest, even with the whole database.
OWASP's password guidance says the pepper must be stored separately from the database, in a secrets vault or a hardware security module. Treat it the same way here. For 190-bit keys it is a second layer, not the main defence.
A stolen row gives a thief little. In our run, the stored digest sent as a key failed, because the server hashes whatever it receives. The real key checked with the wrong pepper failed too.
How do you check an API key safely on every request?
Check the cheap things first and the secret last: shape and checksum, then the row by key ID, then a constant-time comparison of the digest, then status and scope. Constant-time means the check takes the same time wherever the values differ. Each step can reject a request without the next.
Checking one request's API key
Drag to exploreNotice that a typo or a scanner-style junk string is rejected before any database work, and that a wrong scope gives 403, not 401.
Diagram connections:
- Request with key to Prefix and checksum (parse key)
- Prefix and checksum to Find row by key ID (checksum ok)
- Prefix and checksum to 401 or 403 (bad shape: 401)
- Find row by key ID to HMAC with pepper (row found)
- HMAC with pepper to Digest matches? (digest)
- Digest matches? to Live and in scope? (yes)
- Digest matches? to 401 or 403 (no: 401)
- Live and in scope? to 200 and update last used (yes)
- Live and in scope? to 401 or 403 (no: 401 or 403)
- Request with key: Customer server → your API
- Prefix and checksum: Offline check, no database
- Find row by key ID: Gets digest, scopes, expiry, status
- HMAC with pepper: Pepper loaded from secrets manager
- Digest matches?: hmac.compare_digest, no early exit
- Live and in scope?: Not revoked, not expired, has scope
- 200 and update last used: Request runs
- 401 or 403: 401 bad key; 403 key lacks scope
Here is the core of the check, cut down to run on its own. The key inside it is a fake placeholder, not a key from our generator.
import hashlib
import hmac
import secrets
PEPPER = secrets.token_bytes(32) # load from your secrets manager
def digest(key):
return hmac.new(PEPPER, key.encode(), hashlib.sha256).hexdigest()
# What the database row keeps: never the key itself
key = "apk_live_EXAMPLE1_FAKEfakeFAKEfakeFAKEfakeFAKEfake000000"
row = {"key_id": "EXAMPLE1", "digest": digest(key), "last4": key[-4:],
"scopes": {"invoices:read"}, "revoked": False}
def check(presented, row, needed_scope):
if not hmac.compare_digest(digest(presented), row["digest"]):
return 401
if row["revoked"]:
return 401
if needed_scope not in row["scopes"]:
return 403
return 200
print(check(key, row, "invoices:read"))
print(check(key, row, "refunds:write"))
print(check(row["digest"], row, "invoices:read"))It printed 200, 403 and 401. In HTTP, 401 means the caller is not authenticated and 403 means it is known but not allowed. The first call had the right key and scope. The second had the right key but asked for refunds:write. The third sent the stored digest itself, which failed.
Why the digest comparison must not exit early
A normal == comparison stops at the first different byte. Python's docs say hmac.compare_digest avoids that content-based short-circuiting. It is designed so its timing does not reveal how much of a value matched.
We measured it on 1 MB inputs to make the gap visible. == took 0.11 microseconds when the first byte differed and 65.95 microseconds when the last byte did. compare_digest took about 700 microseconds both times.
The docs add one limit: if the two inputs differ in length, timing could reveal the lengths, but not the values. Comparing two hex digests of equal length avoids that.
Add rate limiting on failed key checks too, as cheap protection against floods of bad keys. NIST SP 800-63B-4 requires it for passwords and for look-up secrets under 112 bits. SevyDevy's rate limiter guide shows token bucket and sliding window designs.
How do scopes and expiry limit the damage of a leaked key?
A scope caps what a leaked key can do, and expiry caps how long it can do it. Together they turn a leak from a full account takeover into a small, time-boxed problem.
Stripe's restricted keys are a clear real example. As of 29 October 2026, its docs let you set each resource to None, Read or Write, and say write permission includes read. They also suggest one restricted key per service, such as billing, reporting or a webhook handler.
For the invoicing API, the customer's reporting job got a key with only invoices:read and a 90-day expiry. In our run, that key fetched invoices with 200 and was refused a refund with 403 missing scope refunds:write. After 91 simulated days it got 401 expired.
Network limits help too. Stripe's access policies can restrict a key to listed IP addresses or ranges, written in CIDR notation, a short way to write an address range, such as 192.0.2.0/24. A key copied to another network then fails even with the right secret.
OWASP's Secrets Management Cheat Sheet says to create secrets that expire after a defined time where possible. Expiry forces the rotation habit that leak day needs.
How do you rotate API keys without downtime?
Issue the new key first and let both keys work during a short overlap. Move every caller across, then revoke the old key once its traffic reaches zero. The overlap is what removes the downtime.
Cancelling first is the naive way, and it fails because keys are copied to many places. The customer in the opening had the same key on several servers, and each one broke at a different moment.
Stripe documents this pattern. As of 29 October 2026, rotating a key in its Dashboard keeps the old and new keys working for up to 7 days. Its docs suggest expiring the old key only after its request volume has been zero for a few hours or days.
To support this, your table needs two things. Each account can hold more than one active key, and each key has its own revoked_at time and last_used_at time. Never let rotation overwrite the old key's row in place.
Create the twin
Issue a new key with the same scopes and name. Show it once. Set the old key's revoked_at to a time in the future, such as 7 days away.
Roll it out slowly
The customer switches one server first and watches for 401 and 403 errors, then the rest.
Watch the old key's traffic
Show last_used_at for both keys on the dashboard. The old key should go quiet well before its deadline.
Let it expire, or extend
If the old key still has traffic near the deadline, find the caller instead of extending forever.
Record it
Log who rotated, when and why. OWASP's cheat sheet asks for audit logs that cover rotation and expiry.
Our simulated drill on SQLite 3.53.1 behaved as planned. On day 0 both keys returned 200. On day 3 a server still on the old key got 200. On day 7 the old key got 401 revoked and the new key 200. The table showed the old key last used on day 3.
Planned rotation vs leak-day revocation
Drag to exploreNotice that both paths end with the old key revoked, but only the planned path has an overlap. On leak day, the leaked key gets no overlap at all.
Diagram connections:
- Why are we rotating? to Issue twin key (planned)
- Issue twin key to Overlap window (both live)
- Overlap window to Watch last_used_at (move callers)
- Watch last_used_at to Old key revoked (traffic is zero)
- Why are we rotating? to Revoke now (leak)
- Revoke now to Issue replacement (then)
- Issue replacement to Old key revoked (nothing to wait for)
- Why are we rotating?: Schedule or staff change, or a leak?
- Issue twin key: Same scopes; shown once
- Overlap window: Both keys work, for example 7 days
- Watch last_used_at: Old key traffic should reach zero
- Revoke now: Leaked key fails at once
- Issue replacement: Customer deploys it fast
- Old key revoked: Every request with it gets 401
What do you do on the day an API key leaks?
Revoke the leaked key immediately, give the owner a replacement, then find out what the key did and how it leaked. Containment comes first, because the key may already be in use.
Both primary sources are firm here. Stripe's best-practice page says to rotate an exposed key immediately even if you are not sure anyone saw it. OWASP's cheat sheet says exposed keys should undergo immediate revocation.
Stripe separates exposure, meaning the key became visible somewhere it should not, from compromise, meaning there is evidence of misuse. Treat exposure as compromise until your logs say otherwise. Revoking at once can break the customer's servers until they deploy the replacement; that is the price, and narrow scopes keep it small.
Revoke now
Set revoked_at to now for the leaked key. No overlap: an attacker would enjoy it too.
Issue a replacement
Same scopes, shown once. Tell the owner which key ID leaked and where it was found.
Read the key's logs
Look at every request since the leak, by endpoint and source address. Scopes decide how far this search must go.
Remove it from the source
Delete it from the file, the logs and anywhere else it landed. OWASP's remediation steps say revoked secrets must be removed from the exposed system.
Fix the path
Add a pre-commit hook or secret scanning so the same mistake is caught before the push.
Write it up
Record times: leaked, detected, revoked, replaced. The gap between leaked and revoked is the number to shrink.
Receiving leak reports automatically
If you join a scanning partner program, the leak report becomes a webhook, an HTTP call to your server. GitHub's docs say each message is signed with ECDSA, a public-key signature scheme, on the P-256 curve with SHA-256, and the signature covers the raw message body.
We tested a receiver with cryptography 50.0.1. We generated our own P-256 key pair to stand in for the scanner's published key and signed a fake report. The report held one real-shaped key and one junk string.
The receiver verified the signature first, then ran the checksum, then revoked the matching key. It answered revoked HL93HqCN for the real one and ignored the junk as a false positive. A body with one changed letter failed with bad signature.
One result surprises people. We parsed the same JSON and wrote it back out, which only added spaces, and the signature check failed. Verify the exact bytes you received, before any parsing, as GitHub's docs instruct.
How do you explain API key management in a system design interview?
Interviewers raise it when your design has a public API that business customers call. Show that you have thought past generating a random string.
Say who the key identifies
A calling program and its account, not a person. Users get OAuth or sessions.
Describe the format
Prefix with issuer and environment, public key ID, at least 128 bits of secret, checksum. Scanners and support staff can recognise it.
Store a fingerprint
HMAC-SHA-256 with a pepper from a secrets manager, plus key ID, last 4, scopes and timestamps. Show the key once.
Check cheaply, then carefully
Checksum offline, row by key ID, compare_digest on the digest, then status and scope. 401 vs 403.
Limit the blast radius
Blast radius is how much damage one failure can do. Shrink it with scopes per service, expiry and optional network allow-lists.
Rotate with overlap
Several active keys per account, revoked_at in the future, last_used_at to know when traffic stops.
Plan leak day
Revoke with no overlap, replace, audit logs, and a signed leak-report webhook from scanners.
Questions people ask
- What is API key management in simple words?
- It is the full lifecycle of the keys your API gives to customers. You create each key in a recognisable format and store only its hash. You limit it with scopes and expiry, rotate it without breaking callers and revoke it fast after a leak. Doing these on purpose turns a leaked key from an incident into a short, routine fix.
- Should I store API keys in plain text in my database?
- No. Store a hash of each key, ideally HMAC-SHA-256 with a pepper kept in a secrets manager. Then a leaked database or backup does not hand anyone a working key. Show the full key to the customer once at creation, and keep only the key ID, last 4 characters, scopes and timestamps for display and lookup.
- Why is SHA-256 fine for API keys but not for passwords?
- Because a random API key cannot be guessed, while a password often can. OWASP warns that fast hashes let attackers test password guesses quickly. A 190-bit random key would take more than 10 to the power 37 years even at a trillion guesses a second. NIST SP 800-63B-4 applies the same logic to random look-up secrets of 112 bits or more.
- How long should an API key be?
- We would aim for at least 128 bits of random secret, and more is cheap. For random look-up secrets, NIST accepts a plain approved hash at 112 bits or more. Our format uses 32 Base62 characters, about 190 bits, plus a prefix, a public key ID and a checksum, for 56 characters in total.
- How often should API keys be rotated?
- Rotate on a schedule your customers can practise, and always when someone with access leaves or a key is exposed. OWASP recommends regular rotation and expiry where possible. Stripe suggests rotating when team members change roles or when policy requires. What matters more than the interval is that rotation is a tested, overlap-based routine.
- What should my API do when GitHub reports a leaked key?
- Treat it as compromised and revoke it immediately. GitHub's partner program docs say to consider every reported secret public and compromised, and suggest revoking it and notifying the user. Verify the message signature over the raw body first, check the key's checksum, then revoke, issue a replacement and review the key's logs.
- Is an API key the same as an OAuth token?
- No. An API key identifies a calling program and its account, and usually lives for months. An OAuth access token is issued after a user grants an app limited access, and is often short-lived. OWASP's API Security Top 10 says API keys should authenticate API clients, not users.
Sources and further reading
- GitHub Engineering blog: Behind GitHub's new authentication token formats (5 April 2021, updated 10 May 2023)
- GitHub Docs: Secret scanning partner program (payload, signature, format advice)
- GitHub Docs: Supported secret scanning patterns (partner alerts vs user alerts)
- Stripe Docs: API keys (key types, show once, rotation with 7-day overlap, access policies)
- Stripe Docs: Restricted API keys (None, Read, Write; one key per service)
- Stripe Docs: Best practices for managing secret API keys (exposure vs compromise)
- NIST SP 800-63B-4: Authentication and Authenticator Management (sections 3.1.1.2, 3.1.2.2, 3.2.2)
- NIST CSRC: SP 800-63B-4 publication record (final, July 2025)
- OWASP Secrets Management Cheat Sheet (lifecycle, rotation, revocation, remediation)
- OWASP Password Storage Cheat Sheet (fast hashes, peppers, scrypt parameters)
- OWASP API Security Top 10 2023: API2 Broken Authentication
- Python docs: hmac.compare_digest
- Python docs: secrets module