Pull up any API you integrated with this week. Stripe, Anthropic, Resend, Vercel, Supabase, Linear. They all support OAuth elsewhere in the product, but when you call their public developer APIs, the auth pattern converges: an API key. One header, one curl, authenticated.
Public APIs act on behalf of an organization or system, not an individual user. When Zuplo calls Stripe, it calls as Zuplo, not as me. That’s the API key sweet spot. Two developer experience wins follow: time-to-first-call measured in seconds, and self-serve management so a leaked key can be rolled instantly from a dashboard.
AI agents matter in 2026 too. They authenticate as systems, not humans. One header and a curl, not an OAuth dance. API keys are the agent-native auth pattern.
I’ve spent many years building API products and helping Zuplo customers build out their own authentication experiences. The nine practices below are what consistently separates a key system that scales gracefully from one teams have to rebuild a year in. The video is the same content if you’d rather watch it.
19:21API Key Authentication Best Practices
Watch Josh walk through the nine practices on YouTube.
What about OAuth and JWTs
Quick aside before the practices.
OAuth and OIDC are great when the consumer authenticates as an individual, when the API needs to know that Josh clicked the button. API keys are great when the consumer authenticates as an organization, a system, or an agent, when the call is Zuplo talking to Stripe, not me personally.
The security argument cuts both ways. JWT scopes and claims are just base64-encoded, so anyone holding a token can decode it and read the claims profile. An API key is an opaque string with nothing to read.
Revocation is the other big one. Kill an API key in the dashboard and it’s dead on the next request. A JWT with a long refresh window keeps a life of its own until it expires or you rebuild your trust chain.
Neither pattern is inherently more secure. Pick the one that matches the identity you’re authenticating.
When to Use API Keys
Decision guide for choosing API keys, JWT, or OAuth based on the identity you're authenticating.
- You're designing an API key system for an API-first product
- You're auditing an existing key system that's grown organically
- You serve AI agents as first-class consumers, not just human developers
The big decision: retrievable or irretrievable
Every other choice in this post hangs off this one.
Irretrievable keys are shown once at creation, then stored only as a one-way hash. The user stashes the secret somewhere safe; lose it, create a new key. Stripe, Anthropic, AWS, and Linear work this way.
Retrievable keys can be viewed again from the dashboard, which means the service stores them in reversible form (encrypted at rest, decryptable on demand). Resend and Supabase work this way.
Both have a defensible security argument, and I’m not going to give you a strong recommendation either way. The “big company picks irretrievable, small or casual customers pick retrievable” rule of thumb you sometimes hear isn’t a good way to decide.
The argument for irretrievable: if someone breaks into your database, the keys are gone forever, just one-way hashes. Nothing to reuse. The argument against: the moment you hand a developer a key they can only see once, they paste it into Notepad or Sublime so they don’t lose it. That’s the part of the workflow that’s hard to engineer around.
Retrievable keys, stored encrypted at rest and decryptable on demand by the dashboard, sidestep that pattern at the cost of a more attractive database for an attacker to break into.
Zuplo defaults to retrievable. It’s my personal preference and it’s the more common choice. Pick whichever fits how you want your customers to handle the secret. The nine practices below apply either way.
1. Pick retrievable or irretrievable, then commit
Do not waver. The choice cascades through your dashboard UX, rotation flow, support runbook, and onboarding docs. Make it on day one and document it.
Loosening an irretrievable system to retrievable later means re-issuing every active key, so if you’re genuinely on the fence, retrievable is the easier direction to walk back from.
2. Support a rolling transition period
All keys must be revocable. That’s table stakes.
The mistake is making revocation instantaneous. The moment you nuke an old key, every system holding it breaks, including agents that cached it in memory.
Stripe handles this well: when you roll a key, you pick a future expiration on the old one rather than killing it immediately. That gives the team time to push the new key to staging, production, and any agent fleets without a coordinated outage.
Agent-heavy environments need longer overlap windows. Agent fleets take time to pick up the new key from config, and a few minutes isn’t enough for a queued workload to drain.
API Key Management
Zuplo's roll-key API creates a new key and sets an expiration on the old one, so consumers have time to migrate without downtime.
3. Show the key creation date in the UI
People rotate keys on a schedule. Impossible if the dashboard doesn’t tell them when each key was created.
It matters more when a user holds multiple keys. In 2026, that’s one key per agent plus a few for production services. A list of identical masked strings with no dates and no labels is a non-starter.
Show the creation date. Show a label. Let users name keys at creation. None of this is hard. All of it pays off the first time someone has to figure out which key is which.
4. Add a checksum to the key
Append a checksum at the end of every key. Simple version: a CRC32 of the random body, encoded into the last few characters. Signed version: an HMAC of the body using a server-side secret, which verifies authenticity, not just shape.
It earns its place three ways:
- Pre-database shape check. Reject garbage in microseconds, no DB hit.
- Secret-scanner signal. Scanners use it to filter random strings during pattern matching, reducing false positives.
- Hallucination defence. Agents that call your API directly will sometimes invent keys. A model’s guess produces strings that look right but fail the checksum, rejected without a database round-trip.
The signed version goes further. With an HMAC at the end, you reject every key not issued by your service, no matter how plausibly shaped, with zero database calls.
5. Cache key lookups, carefully
Every API call checks the key. If every check is a database round-trip, your API’s tail latency floor is your DB’s tail latency floor. The default should be a read-through cache: check the cache, fall back to the DB on a miss, store the result.
Two non-obvious moves separate a good cache from a fragile one:
- Cache invalid keys too. Repeat offenders get a fast 401 from cache instead of a fresh DB hit each time. An attacker spraying the same malformed key a thousand times pays once.
- Keep TTLs short, with a kill switch. Cached keys slow revocation: a key revoked at the source is still good in cache until it expires. Sixty seconds is a sensible ceiling. Pair it with a kill switch that flushes the cache on demand for the rare “we leaked one, get it out now” moment.
This matters more in 2026 than it did three years ago. An AI agent can fire a thousand requests in the time a human fires one. Caching is no longer nice-to-have, it’s load-bearing. Zuplo runs this across 300+ edge locations asynchronously, so the DB call almost never blocks the request.
6. Support secret scanning
Mistakes happen. Keys end up in source control. They also end up in places they didn’t used to:
- Chat transcripts with AI assistants
- Prompts pasted into shared workspaces
- Tool call logs and agent traces
- Public Notion pages, Slack channels, and Loom videos
Join GitHub’s secret scanning partner program. Register your prefix and a webhook, and GitHub forwards matches it finds in public repositories so you can notify the customer or auto-revoke.
Push protection sits on the other side of the same wall. It blocks committers from pushing matching strings into private repos in the first place, so the common “I just realised I committed our key” never happens.
This is why the prefix and checksum from earlier matter. They make secret scanning’s job possible.
API Key Leak Detection
Zuplo participates in GitHub's secret scanning program. When a `zpka_` key leaks, GitHub forwards the match to Zuplo and you get an email and in-app alert with the repository URL.
7. Hide keys until they’re needed
Everyone has a 4K camera in their pocket and a screen-share window open most of the day.
Mask keys by default with a reveal button. There’s a ladder here:
- Good. Mask the key, with a reveal button.
- Better. Pair reveal with a copy button so the key never has to be displayed visually.
- Best. Copy straight to the clipboard, paste into a masked field on the destination, and the key never appears on screen at all.
The threat surface is bigger than it looks. If your dashboard is screen-shared during a Loom recording or a pair-programming session with an AI assistant, the key now sits in a video file or a model context. Masked-by-default protects against both.
8. Use snake_case, not dots or dashes
Tiny detail, big quality-of-life win.
Dots and dashes break double-click selection in most browsers and terminals.
Trying to copy sk-ant-abc... selects only ant, then you drag-select the
whole thing manually. Underscores let users select the entire key in one click.
Be nice to future developers. The double-click win alone is worth it.
9. Label your keys with a prefix
Stripe uses sk_live_... and pk_test_....
GitHub
uses ghp_... for personal access tokens. Zuplo uses zpka_.
Three jobs, all important:
- Support triage. “How does your key start?” instantly diagnoses misuse. Wrong prefix, wrong product. Wrong environment, wrong key.
- Secret scanning triage. A leaked publishable key is annoying. A leaked secret key is panic. The prefix tells the scanner which is which.
- Agent and SDK routing. An agent or SDK reads the prefix and knows what kind of key it’s holding before using it.
The cost is zero. The benefits compound. Pick a prefix and use it.
Buckets and Environments
Group keys into buckets so prefixes like `_live_` and `_test_` map to the right scope, and rotate or revoke an environment in one place.
The canonical key validation flow
Here’s what a well-built API key system does on every request:
- Receive request. Check that a key is present and correctly formatted.
- Validate the checksum. Reject garbage in microseconds with a 401, no database hit.
- Check the cache. If the key is cached and valid, proceed.
- Fall back to the key store. If retrievable, decrypt and compare. If irretrievable, hash and compare.
- Cache the result. Valid or invalid, store it. The same garbage key submitted a thousand times gets a fast 401 from cache instead of a thousand DB hits.
- Rate limit per key and per source IP. Per-key limits cap a single tenant’s burst. Per-IP limits catch attackers spraying random keys from one address: every guess has its own bucket, so a per-key limit alone never trips. Apply both.
This flow holds whether the caller is a human script, a backend service, or an AI agent. That’s the point. Keys are a transport-agnostic auth pattern, and the same lifecycle applies regardless of who’s holding the secret.
API Key Authentication
How Zuplo validates inbound keys on every request: shape and checksum check, edge cache lookup, key store fallback, and per-key rate limiting.
Get all of this for free
Zuplo built its API key service after studying how the best in class do it. Every practice in this post is in the box:
zpka_prefix and checksum signature- Retrievable by default
- Roll-key API with configurable expiry
- Dashboard with creation dates and labels
- Edge cache for key lookups across 300+ locations
- GitHub secret scanning integration
The same service handles human developers and AI agents on equal footing.
If you want a policy that drops in front of any API and enforces all of this, see the api-key-inbound policy. One config block, one route attachment, and you’re authenticating with the same patterns the API-first leaders use.