API keys and OAuth solve different problems, and choosing between them starts with one question: is there a third party in the picture?
If your server calls someone else's API on its own behalf, you want a key. If someone else's application calls your API on behalf of your user, you want OAuth. With no third party involved, OAuth adds ceremony without adding safety.
What follows: how they actually differ, what changed in OAuth over the last few years, and one property of API keys that almost nobody writes about — even though it shapes how you live with them in production.
An API key: one string that says "this program is allowed"
A key is a long random string you put in a request header:
X-Api-Key: lix_live_9f2c...
The server finds the key, works out which account it belongs to, and lets the request through. That is the whole mechanism.
The properties that follow from it:
- a key does not expire on its own. It lives until somebody revokes it;
- a key identifies a program, not a person. Which teammate made the call is usually invisible;
- a key grants whatever the account can do, unless you scope it separately;
- a key is the only secret. If it leaks, access leaks — there are no intermediate steps.
OAuth: a protocol about consent
OAuth 2.0 (RFC 6749) answers a different question. It has four participants: the owner of the data, the application that wants it, an authorization server, and the server holding the data.
The flow runs roughly like this. The application sends the user to the service's own page. The user sees who is asking and for what, and agrees. The application receives a code, exchanges it for an access token, and calls the API with that. The token is short-lived — minutes or hours; when it expires, the application trades a refresh token for a new one.
What this buys you that a key fundamentally cannot:
- the user's password never reaches the application. That is the entire point;
- permissions split into scopes. "Read links" and "delete links" are separate grants;
- consent is visible and revocable. The user sees a list of connected applications and can disconnect one without touching the others;
- tokens expire by themselves. A stolen access token is worthless an hour later.
The price is complexity: an authorization server, application registration, a consent screen, storage and rotation of refresh tokens, and handling expiry on every call.
Comparison along the axes that actually decide it
| API key | OAuth 2.0 | |
|---|---|---|
| Who it identifies | the program | the user the program acts for |
| Lifetime | indefinite | access token: minutes to hours |
| How access ends | revoke the key | revoke consent or the token |
| Scope of rights | usually the whole account | per-action scopes |
| Secret held by the third party | the key itself | tokens only, never the password |
| Integration effort | one request header | an authorization server and the full cycle |
| Where it fits | server-to-server, scripts, in-house integrations | public apps, marketplaces, "Sign in with…" |
The rule for choosing
The question is not which is safer in the abstract. It is who is delegating access to whom.
Your server → someone's API, your own account
→ API key
Someone's app → your API, your user's account
→ OAuth
Scheduled job, CI, backend integration
→ API key
User must see and revoke access per application
→ OAuth
Receiving postbacks is the textbook case for a key. When your server tells ours that an order was paid, there is no user in that exchange, nobody to consent, and nowhere to put a consent screen. The same holds for webhooks running the other way.
What changed in OAuth while you weren't looking
Half the articles on the subject describe OAuth as it stood in 2015. It has narrowed considerably since.
In January 2025 the IETF published RFC 9700, Best Current Practice for OAuth 2.0 Security. It consolidates years of accumulated attack experience and formally deprecates two flows that used to be considered acceptable:
- the implicit grant — the one that returned a token straight in the URL;
- resource owner password credentials — where the application asked the user for their login and password directly. Precisely what OAuth was invented to avoid.
At the same time PKCE became mandatory for all client types, including server-side ones, rather than just mobile clients as before.
Worth knowing separately: OAuth 2.1 is still a draft, not a published RFC. It gathers the same changes into one document — mandatory PKCE, exact-string redirect URI matching, no implicit or password grant, no bearer tokens in query strings. Citing it as a current standard is premature.
The practical takeaway: if a tutorial recommends the implicit flow, the tutorial is out of date.
The weak spot of API keys nobody mentions
A key never expires. That makes revocation your only lever. And here is the awkward part: revocation is usually not instant.
Checking a key on every request means hitting the database. On any API with real traffic, that result gets cached. In our case the mapping from key to account sits in cache for five minutes. So between clicking "revoke" and access actually stopping, up to five minutes pass, during which a compromised key keeps working.
This is not an oversight, it is a trade: without the cache, every request queries the database. Most key-based APIs make a similar bargain — it just tends not to be said out loud. It matters for two reasons. First, in a real leak, revoking the key is the first action, not the last: afterwards you want to check what happened during those minutes. Second, it is exactly the problem that OAuth's short-lived tokens attack from the other end — they expire on their own, with no database involved.
Living with keys without getting hurt
If keys are enough — and for most integrations they are — the minimum looks like this:
- Store only the hash. The server compares a hash of the presented key against the stored one; the original string is shown to the user once, at creation. Ours are matched by SHA-256.
- Give the key a prefix. A string shaped like
lix_live_...is recognisable to secret scanners in repositories and log pipelines. - One key per integration. Revoking one then doesn't take down the rest, and the audit trail shows exactly what was compromised.
- Never put a key in a query string. URLs end up in web server logs, in the
Refererheader and in browser history. Header only. - Rotate on a schedule, not after an incident. A key that has been rotated once rotates quickly. A key that never has will turn out to be hardcoded in four places.
- Watch last-used timestamps. A key untouched for six months is not a spare, it is an open door.
What we use
The Lix.li API authenticates with keys: an X-Api-Key header, keys created and revoked in the dashboard, hash-based matching, a separate rate limit per key. There is no OAuth, deliberately: the API's use cases are server-side — create a link, pull statistics, accept a conversion — and none of them involve a third party the user would need to consent to.
If the job became letting other people's applications into our customers' accounts, keys would stop being enough: the user must be able to see who was granted what and disconnect one application without breaking the others. That is the point where OAuth stops being ceremony and becomes the only honest option.
The calls themselves are laid out in the API documentation, including which response codes come back for an invalid versus a revoked key. On a neighbouring topic, why a 301 redirect turns a POST into a GET is worth reading for anyone wiring up postbacks: an authenticated request can arrive by a different method than the one it left with.