How to Generate and Manage API Keys for Your Users
Letting customers call your API means issuing API keys. How to generate secure keys, prefixes that make them identifiable and scannable, storing only a hash, showing the key once, scopes and expiry, rotation, rate limits per key, logging usage, and revoking keys.
When your customers want to call your API from their own code — or connect your product to Zapier, an AI agent or a script — you need to give them API keys. Getting this right is mostly about a few careful decisions. (What is an API key?, API authentication methods)
1. Generate keys properly
Use a cryptographically secure random generator, with enough randomness (at least 128 bits, commonly 256):
import { randomBytes } from 'node:crypto'
function generateKey() {
const secret = randomBytes(32).toString('base64url') // 256 bits
return `sk_live_${secret}`
}
Never use Math.random(), UUIDv4 alone for long-lived secrets, timestamps, or anything derived from user data.
2. Add a prefix
Prefixes like sk_live_, sk_test_, or your product name (acme_sk_) help:
- users (and your support team) see what kind of key it is,
- you separate test and live environments,
- secret scanners — including GitHub's — can detect your keys if they're leaked. You can register your key format with GitHub's secret scanning partner programme. (GitHub secret scanning)
3. Store only a hash
Treat API keys like passwords: store a hash, never the key itself.
import { createHash } from 'node:crypto'
const hash = createHash('sha256').update(key).digest('hex')
await db.apiKey.create({
data: {
userId,
name: 'Zapier integration',
prefix: key.slice(0, 12), // for display: "sk_live_Ab3x…"
hash,
scopes: ['invoices:read'],
expiresAt: null,
},
})
A fast hash like SHA-256 is fine here (unlike passwords) because keys are long and random — there's nothing to brute-force. If your database leaks, attackers get hashes, not working keys. (Hashing vs encryption, Password hashing)
4. Show the key once
Display the full key once, at creation, with a clear "copy it now — you won't see it again". Afterwards show only the prefix and last-used date. Lost key? Create a new one.
5. Look up and verify
async function authenticate(req) {
const key = req.headers.authorization?.replace(/^Bearer /, '')
if (!key) return null
const hash = createHash('sha256').update(key).digest('hex')
const record = await db.apiKey.findUnique({ where: { hash } })
if (!record || record.revokedAt || (record.expiresAt && record.expiresAt < new Date())) return null
void db.apiKey.update({ where: { id: record.id }, data: { lastUsedAt: new Date() } })
return record
}
Accept keys in a header (Authorization: Bearer … or X-API-Key), never in the URL — URLs end up in logs. (Query parameters explained)
6. Scopes
Let users create keys that can do only what they need: invoices:read vs invoices:write. A key for a read-only dashboard shouldn't be able to delete data. Check the scope on every endpoint. (Principle of least privilege, Role-based access control)
Keys belong to an account or workspace, and every query must still be limited to that account's data. (IDOR explained)
7. Rate limits per key
Limit requests per key (and per account), return 429 with Retry-After, and document the limits. (Implementing rate limiting)
8. Rotation and revocation
- Users can revoke any key instantly.
- Allow multiple active keys so users can rotate without downtime: create new, deploy it, revoke old.
- Optional expiry dates, and reminders before expiry.
- Revoke automatically if a scanner reports a key leaked.
9. Log usage
Record which key made which request (key ID, never the key), so users can see activity and you can investigate abuse. (Structured logging)
10. Test vs live
Separate test keys that only touch test data make integrations safe to build — the approach Stripe popularised.
The UI checklist
- Name each key ("Zapier", "CI").
- Choose scopes and optional expiry.
- Show once; copy button.
- List with prefix, created, last used.
- Revoke button with confirmation.
EasySpawn gives your API a server and Postgres database to store hashed keys and usage logs, with Claude Code to build the key management screens and middleware. See how it works or join the waitlist.
Related: What Is an API Key? · API Authentication Methods · Implementing Rate Limiting · GitHub Secret Scanning
Keep reading
Refresh Tokens Explained: Short-Lived Access, Long-Lived Sessions
Access tokens should expire quickly; refresh tokens let users stay logged in without re-entering passwords. How the pair works, where to store each, refresh token rotation and reuse detection, revocation, handling refresh in the front end, and when plain session cookies are simpler.
OAuth PKCE Explained: Why Every OAuth Flow Should Use It
PKCE (Proof Key for Code Exchange) protects the OAuth authorization code flow from code interception. How code_verifier and code_challenge work step by step, why it's required for SPAs and mobile apps and recommended for everyone, the state parameter, and the mistakes to avoid.