Why you should set up a separate API key per project

One key across every project saves five minutes now and creates a mess at the first incident. Here's what separating keys by project actually gets you.

5 min · · Updated · API keys

The easiest thing is to issue one fetch_... key and drop it into everything — a personal side project, a work service, a test script. It saves five minutes of setup and creates exactly the problems that tend to surface at the worst possible time.

One project's limit shouldn't be everyone's limit

On the key's page under /app/api-keys you can set a daily and monthly spend limit in dollars and a list of allowed models — but that's a setting on a specific key, not the whole account. If one key covers five projects, the limit has to be set with headroom for all five at once, which means it doesn't really protect any of them: a bug in one service can burn through the whole daily limit, and the other four just start getting 429 key_daily_limit_reached responses through no fault of their own.

With a separate key per project, the limit applies to that project specifically:

bash
# key for an internal analytics script — low ceiling
curl https://api.zukko.pro/v1/chat/completions \
  -H "Authorization: Bearer fetch_key_analytics" \
  -H "Content-Type: application/json" \
  -d '{"model":"glm-5.3-flash","messages":[{"role":"user","content":"..."}]}'

If that script loops out of control, it hits its own limit and stops — production, on its own key, keeps running as if nothing happened.

The allowed-model list limits the blast radius, not just the budget

A key can be restricted to specific models. For a project that only needs gpt-5.6-luna for short responses, there's no reason to also allow claude-opus-5-5 on it — a stray typo in a config, or someone else's code that got hold of the key, physically can't run the expensive model through it. The attempt comes back as 403 model_not_allowed_for_key.

This works the other way too: if one project deliberately runs on an expensive model for quality and another runs on a cheap one to save money, separating keys makes that boundary technical instead of just a convention someone has to remember. You don't have to hope nobody forgets which model belongs where in the code — the key itself won't allow the wrong call.

Tracking spend separately per key

GET /v1/usage returns charge history with cursor pagination — but it's shared across the whole account: all keys together, no per-key filter, and the balance is account-wide too. So splitting spend by project happens on your side: log X-Zukko-Cost-Micros per key (for non-streaming requests), and if you need strict separation, streaming requests included, set up a separate account:

bash
curl https://api.zukko.pro/v1/usage \
  -H "Authorization: Bearer fetch_your_key"

The X-Zukko-Cost-Micros header comes back on every regular (non-streaming) response — if one key covers every project, those numbers are mixed together and useless for a single product's unit economics. With separate keys and the cost logged alongside the key, each project's spend tallies itself, without manually sorting logs by "which call was this." X-Fetch-Balance-Cents, meanwhile, shows the whole account's balance (in microdollars, despite the name) and is shared by all keys.

Revoking one key doesn't touch the others

A key is shown once at creation and can end up compromised — exposed in logs, in a public repo, in a config someone wasn't supposed to see. If one key covers everything, revoking it means stopping every project at once until you issue a new one and update it everywhere. With separated keys, responding to a leak is a matter of dealing with one project, not an emergency shutdown of everything that touches the API.

Idempotency-Key and caching work at the request level, not the key level — but incident scope is still different

The Idempotency-Key header protects against being charged twice for the same request, and caching via the X-Zukko-Cache: true header (or temperature: 0 on models that accept it, not Claude) works at the user-and-request level. Neither replaces separating keys — they solve "don't pay twice for the same thing," not "don't let one project eat another project's budget."

When one key really is enough

For a personal project with no other contributors and no risk of the code with the key ending up somewhere else, splitting keys is just extra setup. The point where it starts paying off: as soon as someone else gets access to the code (a colleague, a freelancer, CI with external runners) or the project moves from "just playing around" from "something real actually runs on it."

What this looks like in practice

Create a key in the dashboard for a specific purpose, set its limit and allowed-model list right away, and store it in that project's own environment variables:

bash
export FETCH_API_KEY_PROJECT_A=fetch_...
export FETCH_API_KEY_PROJECT_B=fetch_...

From there, each project reaches for its own variable, and "whose spend was this" stops requiring an investigation.

Model retirements are also easier to survive one at a time

When a model gets retired, the error response names the replacement directly: {"error":{"message":"Модель 'X' снята с обслуживания. Используйте 'Y'","code":"model_retired"}}. If a model is simply disabled in the catalog, the response is different — 503 with code model_disabled, and you pick the replacement yourself from GET https://api.zukko.pro/v1/models. If one key covers several projects and all of them depend on the same model, the migration has to happen for everyone at once — there's no rolling back just part of the traffic, since the key either allows the old identifier or the new one.

With separate keys per project, each one has its own allowed-models list, and the switch to the replacement can happen in stages: update the allowed-models setting on one project's key first, confirm everything works with the new identifier, then roll the same change out to the rest. The exact same fact — a model got replaced — turns into either an emergency release everywhere at once, or a calm staged rollout, and the difference comes down to whether the keys were separated in the first place.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

Separate API key per project: why bother · Zukko