API Keys: Identity for Applications
An API key identifies an application — which makes it the natural unit for scoping, rate limiting and metering, and the wrong tool the moment a user is delegating access. Keys are credentials: prefixed, hashed at rest, scoped, and rotatable without downtime.
Frame the contract
API design starts with a consumer, a design question and a guarantee — never with a URL.
What a key identifies, and what that buys
An API key answers "which *application* is calling" — not which human. That single fact drives everything keys are good at: the key is the natural unit for Scopes: Least Privilege as Contract Surface, for The Rate-Limit Contract buckets, for Quotas vs Rate Limits metering, for billing attribution, and for the kill switch when one integration misbehaves. It is also everything keys are bad at: a key carries no user, no consent, and no way for the person whose data is accessed to revoke just their part.
Design the key itself for operations, not just entropy. A structured format — sk_live_51NxK… — encodes what raw randomness cannot: the sk says secret key (vs a publishable client-side key), live says production (so a test key hitting prod is rejectable with a *specific* error), and a recognizable prefix lets secret scanners find leaked keys in public repos — the mechanism behind providers auto-revoking keys within minutes of a public commit. Store only a hash server-side and show the full key once at creation: keys are passwords for machines, and the Password Storage reasoning applies unchanged — a database dump must not be a credential dump. Display …4242 (last four) thereafter so a human can tell two keys apart without either being exposed.
sk_live_51NxKq2eZvKYlo2C9HGbXWpr4
│ │ └── 24+ chars of CSPRNG entropy (~128 bits)
│ └── environment: live | test → wrong-env calls get a
│ specific, debuggable error
└── type: sk secret (server only) | pk publishable (client-safe)
server stores: SHA-256(key), prefix, last4, scopes,
created_at, last_used_at, expires_at
shown to user: full key ONCE at creation · last4 afterwards
scanners match: the sk_live_ prefix — leaked keys in public
repos are found and revoked in minutesLifecycle is contract: create, rotate, revoke — without downtime
The most common key failure is not theft — it is *immortality*. One key, created in 2021, in a config file, with full access: nobody rotates it because rotation means downtime, and rotation means downtime because the contract allows only one active key per account. The fix is a contract clause, not a policy memo: allow multiple concurrent keys, so rotation is create-new → deploy-new → verify-via-telemetry → revoke-old, with zero gap. Without overlap, "rotate quarterly" is a rule everyone endorses and no one follows.
Revocation is the other half: when a key leaks, seconds matter, so revocation must propagate fast — which constrains implementation (a key-validity cache with a 24-hour TTL means a 24-hour breach window after you pressed the button; validity checks need short TTLs or active invalidation). Expiry policy is a genuine trade-off, not a checkbox: expiring keys force rotation hygiene but guarantee that integrations die on a schedule — usually at 2 a.m., in the hands of whoever inherited the integration. Long-lived keys plus last_used_at telemetry, unused-key alerts, and a low-friction rotation flow is an honest alternative; pick one deliberately and document it.
1GET /v1/orders?api_key=9f3a1c…2 3# one key per account → rotation = downtime → never rotated4# query string → key lands in access logs, CDN logs,5# browser history, referrer headers, analytics6# stored in plaintext server-side "so support can read it"7# no last_used_at → revoking is a leap of faith1GET /v1/orders2Authorization: Bearer sk_live_51NxKq2…3 4key management API:5 POST /keys → new key (full value, once)6 GET /keys → prefix+last4, scopes,7 last_used_at per key8 DELETE /keys/{id} → revoked in seconds9 10rotation: create B → deploy B → watch A's last_used_at11 go stale → revoke A (zero downtime)Every clause in the good version exists to make the safe behavior the easy one: overlap makes rotation routine, header transport keeps logs clean, hashes make the database undumpable, and last_used_at turns "is anything still using the old key?" from a guess into a query.
When a key is the wrong credential
The boundary is delegation. The moment the resource owner is a *user* granting *someone else's app* access to their data, a key is the wrong shape: to give the app a key is to give it your whole identity — no consent screen showing what is being granted, no scope chosen by the user, no way to revoke this app without breaking every app, and no audit trail distinguishing the app's actions from yours. That job needs user-delegated authorization — OAuth 2.x — Delegated Authorization — where consent, scoping and per-app revocation are the protocol's whole point. The pre-OAuth web actually ran on the key-shaped version of this ("enter your password for your email account so we can import your contacts"), and its abandonment is the design lesson.
The other disqualifier is *placement*: a secret key can live only where secrets can live — server-side. A key shipped in a mobile app binary or browser bundle is public within hours of someone opening devtools or unzipping the APK; that is why publishable-key/secret-key splits exist, and why browser and mobile consumers authenticate users (sessions, token flows) rather than embedding application credentials. Keys also compose with, not replace, per-key restrictions: binding a key to source IP ranges or referrers narrows what a thief can do with it — defense-in-depth on top of scopes, in the same spirit as Least Privilege, never the primary control.
- Key fits: server-to-server integrations, partner backends, CI jobs, internal scripts — the application is the principal and can keep a secret.
- Key does not fit: user-delegated access (use OAuth 2.x — Delegated Authorization — consent, user-chosen scope, per-app revocation), browser or mobile code (cannot hold secrets), human login (that is Authentication in the Contract's session territory).
- Always attach: scopes per key, rate-limit identity,
last_used_at, optional IP/referrer binding. - Never do: plaintext at rest, query-string transport, one-key-per-account contracts, validity caches measured in hours.
Key points
- A key identifies an application, making it the unit of scoping, rate limiting, metering and kill-switching — and useless for representing a user's consent.
- Structure the key: type + environment prefix and last4 make keys operable, scannable in leaked repos, and debuggable on wrong-environment calls.
- Store only hashes, show the full key once — keys are machine passwords and the password-storage rules apply unchanged.
- Multiple concurrent keys are what make rotation possible without downtime; one-key contracts guarantee immortal credentials.
- Revocation speed is a contract property bounded by your validity-cache TTL — decide the number, then document it.
- User-delegated access is OAuth's job, not a key's: consent, user-chosen scopes and per-app revocation cannot be retrofitted onto an application credential.
Follow the failure
How the contract fails or gets misused, hop by hop — and what it costs when it completes.
- 1Provider → contract: one key per account, full access, passed as
?api_key=, stored plaintext "for support". - 2Consumer → config: the key goes into a repo; a fork later goes public with the key in
config.ymlhistory. - 3Scanner or attacker → key: the unprefixed key is indistinguishable from any hex string — no scanner flags it; an attacker's does not need to.
- 4Attacker → API: full-access reads at modest rates for weeks; per-key metering does not exist, so nothing looks unusual.
- 5Provider → remediation: revoking means breaking the customer's production; rotation was never designed in, so the breach window stretches while a maintenance window is negotiated.
- Leaked keys grant full, silent, long-lived access — the standing-credential breach that per-key scopes, prefixes and telemetry exist to bound.
- Rotation-hostile contracts turn "revoke the leaked key" into "schedule customer downtime", stretching breach windows from minutes to weeks.
- Keys misused for user delegation concentrate every user's authority into credentials scattered across third-party databases, unrevocable per-app.
Design, observe, evolve
A contract decision is incomplete until you know how you would notice it failing and how it changes later.
- • Ship key management as API surface: create, list (prefix+last4+last_used_at), revoke — with multiple concurrent keys so rotation is a deploy, not an outage.
- • Prefix keys by type and environment, hash at rest, display once; register the prefix with secret-scanning ecosystems.
- • Attach scopes, rate-limit identity and metering to every key at creation; default new keys to the narrowest scope the flow allows.
- • Transport in the Authorization header only; bound revocation latency explicitly and test it like any other SLO.
- • `last_used_at` and per-key usage curves: an unused key is revocable risk; a key's sudden geography or volume shift is the leak signal.
- • Age of active keys as a fleet metric — a dashboard where the p50 key age is three years is a finding, before any breach.
- • Wrong-environment and revoked-key call rates: both indicate consumer confusion or automation still holding dead credentials.
- • Introducing prefixes, scopes or expiry to an existing key population is a migration: new keys get the new shape, old keys work under a deprecation clock with per-key usage telemetry driving outreach.
- • Tightening defaults (narrower initial scopes, shorter validity caches) applies cleanly to new keys; applying it to live keys is a breaking change to running integrations and needs the full notice machinery.
- • Real key management — hashing, overlap, scoping, telemetry — is a product surface with UI, docs and support cost; the naive one-key table is a fraction of the work and all of the breach.
- • Hash-at-rest means support can never read a customer's key back — the correct property, and a recurring support friction you should plan scripts around.
- • Expiring keys enforce hygiene at the cost of scheduled integration outages; non-expiring keys with telemetry trade guaranteed pain for monitored risk — neither is free.