---
title: "API Key Best Practices for 2026"
description: "Stripe, Anthropic, Resend, Supabase, and Linear all ship API keys for their public APIs. Here's why that pattern wins, plus nine practices for building a key system that scales instead of one teams rebuild a year in."
canonicalUrl: "https://zuplo.com/blog/2026/05/04/api-key-best-practices"
pageType: "blog"
date: "2026-05-04"
authors: "josh"
tags: "API Best Practices, API Security"
image: "https://zuplo.com/og?text=API%20Key%20Best%20Practices%20for%202026"
---
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.

<CalloutVideo
  variant="card"
  title="API Key Authentication Best Practices"
  description="Watch Josh walk through the nine practices on YouTube."
  videoUrl="https://youtu.be/MZaOYPtnPzw"
  thumbnailUrl="https://img.youtube.com/vi/MZaOYPtnPzw/maxresdefault.jpg"
  duration="19:21"
/>

## 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.

<CalloutDoc
  title="When to Use API Keys"
  description="Decision guide for choosing API keys, JWT, or OAuth based on the identity you're authenticating."
  href="https://zuplo.com/docs/articles/when-to-use-api-keys"
  icon="book"
/>

<CalloutAudience
  variant="useIf"
  items={[
    `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](https://docs.stripe.com/keys#roll-secret-keys): 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.

<CalloutDoc
  title="API Key Management"
  description="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."
  href="https://zuplo.com/docs/articles/api-key-management"
  icon="book"
/>

## 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](https://docs.github.com/en/code-security/secret-scanning/secret-scanning-partner-program/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.

<CalloutDoc
  title="API Key Leak Detection"
  description="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."
  href="https://zuplo.com/docs/articles/api-key-leak-detection"
  icon="lightning"
/>

## 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](https://docs.stripe.com/keys) uses `sk_live_...` and `pk_test_...`.
[GitHub](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)
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.

<CalloutDoc
  title="Buckets and Environments"
  description="Group keys into buckets so prefixes like `_live_` and `_test_` map to the right scope, and rotate or revoke an environment in one place."
  href="https://zuplo.com/docs/articles/api-key-buckets"
  icon="book"
/>

## The canonical key validation flow

Here's what a well-built API key system does on every request:

1. **Receive request.** Check that a key is present and correctly formatted.
2. **Validate the checksum.** Reject garbage in microseconds with a 401, no
   database hit.
3. **Check the cache.** If the key is cached and valid, proceed.
4. **Fall back to the key store.** If retrievable, decrypt and compare. If
   irretrievable, hash and compare.
5. **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.
6. **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.

<CalloutDoc
  title="API Key Authentication"
  description="How Zuplo validates inbound keys on every request: shape and checksum check, edge cache lookup, key store fallback, and per-key rate limiting."
  href="https://zuplo.com/docs/articles/api-key-authentication"
  icon="book"
/>

## 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](https://zuplo.com/docs/policies/api-key-inbound). One
config block, one route attachment, and you're authenticating with the same
patterns the API-first leaders use.