allowed_models: restricting a key to only the models it needs

A key that can call any model in the catalogue is more freedom than an integration needing exactly one model should have. Here is the allowed_models setting.

3 min · · Updated · API keys

By default, a fetch_... key can call any model in the catalogue — from the cheap glm-5.3-flash to claude-opus-5-5. For a lot of use cases that's more freedom than needed: if a key is issued to a narrow service, it might need exactly one model, not the whole catalogue.

What this setting does

On a specific key's page in the dashboard (https://zukko.pro/app/api-keys), you can set a list of models allowed for that key. A request using that key against any model outside the list gets rejected before it ever reaches the provider.

This setting sits next to the daily/monthly spend limit on the same key page — two independent but complementary ways to constrain one key: the limit controls "how much," the model list controls "with what."

What the rejection looks like

bash
{
  "error": {
    "message": "Model 'claude-opus-5-5' is not allowed for this API key",
    "code": "model_not_allowed_for_key"
  }
}

The status is 403. Worth distinguishing from 404 model_not_found: the model exists in the catalogue and the request is syntactically valid — this specific key's policy just forbids calling it.

bash
curl https://api.zukko.pro/v1/chat/completions \
  -H "Authorization: Bearer fetch_restricted_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-5-5",
    "messages": [{"role": "user", "content": "..."}]
  }'
# -> 403 model_not_allowed_for_key, if claude-opus-5-5 isn't in this key's allowed list

Why this matters

Guarding against accidentally calling an expensive model. claude-opus-5-5 costs $0.30/$0.30 per 1M tokens, while glm-5.3-flash is $0.04/$0.04 — 7.5 times cheaper. If a service is only supposed to run on a fast, cheap model, and a bug or a copy-pasted example leaves a different model in the code, the allowlist stops the call before it's billed, not after.

A key for a third-party service or partner. If you hand a key to a contractor for one specific integration — say, only a support chatbot running on glm-5.3-flash — there's no reason that same key should also be able to call other models or switch to a pricier model without your knowledge.

Separate keys per component of one product. A backend service that needs claude-sonnet-5 for core logic and a separate ticket-classification module that only needs glm-5.3-flash can hold two keys with non-overlapping allowed-model lists — a leak of one key doesn't open access to the whole catalogue.

An explicit record of what a service actually uses. The allowed-models list in the dashboard is, in effect, a declaration: this key is meant for exactly these models. If six months later model_not_allowed_for_key shows up in the logs, that's a hint either the wrong model snuck into the code, or the list needs deliberate expansion rather than a silent workaround.

Handling it in code

bash
import requests

ALLOWED_FALLBACK = "glm-5.3-flash"

def call_model(model_id, messages, key):
    resp = requests.post(
        "https://api.zukko.pro/v1/chat/completions",
        headers={"Authorization": f"Bearer {key}"},
        json={"model": model_id, "messages": messages},
    )
    if resp.status_code == 403:
        data = resp.json()
        if data.get("error", {}).get("code") == "model_not_allowed_for_key":
            # Don't try to guess a replacement here — unlike model_retired,
            # the server doesn't suggest one. Log it and decide deliberately.
            raise PermissionError(
                f"Model '{model_id}' is not allowed for this key. "
                f"Check allowed_models in the dashboard or use a different key."
            )
    resp.raise_for_status()
    return resp.json()

A key difference from model_retired: there, the server names the replacement in the message text because the substitution is objective — the model is retired for everyone. Here the restriction is specific to your own key and reflects your own policy, so the server doesn't suggest an alternative — deciding what to do (expand the list or use a different key) is on you.

A common configuration mistake

If a service alternates between models — say, primary claude-sonnet-5 with a fallback to gpt-5.6-sol for when the first returns 503 provider_unavailable — both models need to be in the allowlist up front. Otherwise the fallback logic itself runs into a 403 when the primary provider is down, and instead of switching to the backup model, the service gets a second error stacked on the first.

Bottom line

allowed_models is about scoping a key to a specific job, not spend protection on its own — that's what the separate spend limit is for. It's configured on the key's page in the dashboard, it triggers as 403 model_not_allowed_for_key, and unlike model_retired, it doesn't offer a replacement — because the restriction is yours, not a consequence of catalogue changes.

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_*

allowed_models: a per-key model allowlist · Zukko