Manage API keys in production

Learn to give each service and environment its own scoped API key, load keys from a secrets manager, rotate them without downtime, and respond when one leaks.

  • 40 min
  • Advanced

The Associate course taught the basics: keys live in environment variables, never in a repository or a browser bundle, and a small proxy stands between your front end and the API. That is enough for one service run by one person. It stops being enough the day you have a staging environment, a nightly job, a contractor and a customer-facing widget, all spending the same credits.

This lesson treats keys as infrastructure. You will build an inventory where every key has one owner and one job, inject keys from a secrets manager at runtime, narrow each key to what its job needs, rotate without dropping a request, and run a leaked-key incident from detection to cleanup.

One key per consumer, and an inventory to prove it

APIVerve gives you two kinds of credential. The primary key belongs to the account and is the only one that can be rotated or given an expiry. Sub-keys are additional credentials on the same account: they reach the same catalog, spend the same credits, and each one can carry its own restrictions and be deleted without touching anything else. Every sub-key also gets its own rate-limit bucket, so a bulk job on one sub-key cannot eat the per-minute window your interactive traffic depends on.

The rule that holds up in practice is one key per thing that could independently need revoking:

  • Per environment. Production, staging and each developer's machine. A staging key ends up in a CI log or a screenshot far more often than a production one.
  • Per job. The nightly enrichment run, the bulk import, the scheduled report. These are the workloads that loop when something breaks, and a loop is much easier to stop when it has its own credential.
  • Per third party. A contractor, an agency, a customer's server. Anything running on a machine you do not administer.

What does not need its own key: three services inside one deployment that ship together and share a blast radius. A key per microservice sounds tidy and mostly buys you rotation work.

Keep the primary key in exactly one place you control, ideally unused by day-to-day traffic, and put every consumer on a sub-key. That arrangement is what makes the rest of this lesson possible: rotation by overlap, revocation of one consumer, per-job rate caps.

The two kinds are easy to tell apart. Sub-keys carry a distinct prefix (apv_sbk...) while the primary key starts apv_. A service that should only ever hold a sub-key can assert that at startup, which catches the day someone pastes the primary key into the wrong secret:

config.mjs
// Node 18+, ESM. Fail at startup, not on the first request.
export function loadApiKey({ requireSubKey = true } = {}) {
  const raw = process.env.APIVERVE_API_KEY;
  if (!raw) throw new Error('APIVERVE_API_KEY is not set');

  // Secrets written with `echo` often carry a trailing newline, and fetch
  // refuses a header value containing one.
  const key = raw.trim();
  if (!key.startsWith('apv_')) throw new Error('APIVERVE_API_KEY does not look like an APIVerve key');
  if (requireSubKey && !key.startsWith('apv_sbk')) {
    throw new Error('This service must run on a sub-key, not the primary key');
  }
  return key;
}

The inventory

An inventory is a short, committed, non-secret file that answers the questions you will be asked during an incident: which keys exist, who owns each one, where it is stored, what it can reach, and how to tell its log lines apart. Never write the key into it. Write a fingerprint instead: the first few hex characters of a SHA-256 of the key. A fingerprint is enough to match a key to its inventory row and useless to an attacker.

keys.inventory.json
[
  { "name": "orders-api prod", "kind": "sub-key", "fingerprint": "3f9a2c1e", "owner": "payments team",
    "stored": "gcp:projects/acme/secrets/apiverve-orders-prod", "blocked": "all except emailvalidator, phonenumbervalidator",
    "ip": "203.0.113.7/32", "rateCap": 60 },
  { "name": "nightly enrichment", "kind": "sub-key", "fingerprint": "b71d04aa", "owner": "data team",
    "stored": "github:actions/APIVERVE_NIGHTLY", "blocked": "all except countrylookup", "ip": null, "rateCap": 20 }
]

Compute fingerprints locally, never by sending a real key anywhere, including to a hashing API. The function is two lines:

fingerprint.mjs
import { createHash } from 'node:crypto';
export const fingerprint = (key) => createHash('sha256').update(key, 'utf8').digest('hex').slice(0, 8);

To see what that digest looks like, and to check your local function against an independent implementation, hash a made-up value with the Hash Generator API. The first eight characters of data.hashes.sha256 are the fingerprint your inventory would record:

Try it liveHash Generator1 credit
GEThttps://api.apiverve.com/v1/hashgenerator
curl "https://api.apiverve.com/v1/hashgenerator?text=apv_sbk_EXAMPLE-not-a-real-key&algorithms=sha256" \
  -H "x-api-key: YOUR_API_KEY"
Sign in or create a free account to run this with your own key.

A made-up value. Real keys are fingerprinted locally and never sent anywhere.

Check yourself

Your inventory needs a way to match a log line or a leaked string to a specific key. What should it record?

Run every example on your own key. Free plan, no card.

Scaling up?

Volume pricing, custom SLAs, and dedicated support for high-traffic teams.

Contact sales