An open API is an invitation to abuse: scraped data, drained quotas, surprise bills. The simplest widely used defense is the API key, a long random secret issued to each client. Weather services, map providers, and payment processors all start here. In this lesson we cover how API keys work, how to implement them properly, and where their limits lie.
An API key is an opaque random string, such as cyc_live_4f8a2b91c6d3..., that identifies and authenticates a calling application. The client includes it on every request; the server looks it up, confirms it is active, and attaches the owning account to the request. Prefixes like live and test, popularized by Stripe, let both humans and log scanners recognize a key's environment at a glance.
Note the emphasis on application: a key identifies a program or a customer account, not an individual human user. That distinction drives most of the trade-offs below.
Three transport options exist, in descending order of preference:
X-API-Key: cyc_live_... keeps the secret out of URLs and separates it from other auth schemes.Authorization: Bearer cyc_live_... works and reuses standard tooling.?api_key=... leaks the secret into server logs, browser history, proxies, and Referer headers.Always require HTTPS; over plain HTTP any header is readable by everyone on the network path.
The naive approach stores keys in a database column and compares strings. Production systems do three things differently.
Hash stored keys. Treat keys like passwords: store only a hash, so a leaked database does not leak usable credentials. Keep the first few characters in plaintext as a displayable hint (cyc_live_4f8a...).
Compare in constant time. Ordinary string comparison exits at the first mismatched character, letting attackers measure timing to guess keys byte by byte. Use your language's constant-time comparison, like crypto.timingSafeEqual in Node.
Attach metadata. A key row should carry its owner, creation date, optional expiry, per-key rate limits, and scopes (read-only versus read-write), so you can reason about and restrict each credential independently.
// Express middleware sketch
async function requireApiKey(req, res, next) {
const key = req.get('X-API-Key');
if (!key) return res.status(401).json({ error: { code: 'missing_api_key', message: 'X-API-Key header required' } });
const record = await keys.findActiveByHash(sha256(key));
if (!record) return res.status(401).json({ error: { code: 'invalid_api_key', message: 'Unknown or revoked key' } });
req.account = record.account;
next();
}Generate keys with a cryptographically secure random source (at least 32 bytes), show the full value exactly once at creation, and never email it. Support rotation by allowing two active keys per account, so customers can deploy the new key before revoking the old one with zero downtime. Revocation must take effect immediately, which is a genuine advantage keys hold over stateless tokens.
On the client side, keys live in environment variables or secret managers, never in source code. A hardcoded key in a public GitHub repo is typically discovered by scanners within minutes.
Keys authenticate applications, not users, so they cannot express "Alice can edit only her own documents." They carry no expiry or claims by themselves, and every request costs a database lookup. When you need per-user identity, sessions that expire, and claims the server can verify without storage, you need tokens, which is exactly where the next lesson picks up.
Next up: JWT Authentication, token-based auth for real user sessions.