Authentication
API keys, the two key types, where to put them, and how to survive a leak.
One header. No OAuth dance, no token exchange, no expiring access tokens to refresh.
curl -s "https://api.pokemontcgapi.com/v1/sets/sv3" \
-H "X-Api-Key: ptcg_live_9f2c1d8b4a7e3f60"Authorization: Bearer <key> is accepted as an exact alias, for clients whose HTTP layer only speaks bearer tokens.
Two kinds of key
Which one you need is decided by where the code runs, not by what it does. A secret key in a browser bundle is public the moment it ships, whatever your intentions were.
| Row | ptcg_live_ | ptcg_pub_ |
|---|---|---|
| Runs in | Your server | A browser |
| Scopes | Everything the plan allows | Catalogue reads only |
| Referrer allow-list | Not applicable | Required — requests from other origins get 403 ORIGIN_NOT_ALLOWED |
| Prices | Yes | Current prices only, never history or bulk |
| If it leaks | Revoke immediately | Low blast radius; still revoke |
CORS is open, and that is not the same thing
Every origin may call this API directly from a browser: no proxy of your own to build, deploy and keep patched. But CORS protects your user’s browser, not your key. Key safety comes from the ptcg_pub_ type and its referrer allow-list.
Where the key goes
- Header, always. There is no
?api_key=query parameter, on purpose: query strings end up in access logs, in referrer headers and in browser history. - One key per deployment. Staging, production and each developer get their own, so revoking one does not stop the others.
- Never in a repository. A key in git is a key in every clone and every fork, forever. If one gets committed, revoke it and issue a new one — rotation is additive and costs you nothing.
When authentication fails
| Status | Code | Meaning |
|---|---|---|
| 401 | MISSING_API_KEY | No key on a route that needs one. |
| 401 | INVALID_API_KEY | No account matches. Usually a truncated copy-paste. |
| 401 | KEY_REVOKED | The key was revoked. Issue a new one. |
| 401 | KEY_EXPIRED | The key had an expiry and it has passed. |
| 403 | INSUFFICIENT_SCOPE | Real key, wrong scopes — typically a browser key on a priced route. |
| 403 | ORIGIN_NOT_ALLOWED | Browser key used from an origin outside its allow-list. |
Do not retry a 401
None of these get better on their own. Retrying a 401 in a loop burns your rate limit and produces nothing — treat the whole family as fatal, surface it, and stop.
Rotating a key
Rotation is additive, so it never needs a maintenance window:
1. Create a second key in the dashboard.
2. Deploy it. Both keys are live and both count against the same account quota.
3. Confirm the old key has stopped being used (dashboard shows last-used per key).
4. Revoke the old key.A leaked key is an emergency, and downtime is the correct outcome
Revoke first, redeploy second. A revoked key returns 401 KEY_REVOKED immediately everywhere — that is the point.
How keys are stored
We keep the first 16 characters in clear text, indexed, and an Argon2id hash of the whole key peppered with a secret that lives in the application environment rather than the database. The prefix makes lookup a single indexed row instead of hashing every candidate; the pepper means a stolen database dump is not enough to verify a key against.
The consequence for you: we cannot recover a lost key, only issue a new one.