Skip to main content

E1101. API key rejected (401)

Severity: ERROR. Your API key is no longer valid. Generate a new key in the dashboard (Settings -> API Keys) and set CONTROLZERO_API_KEY.

What happened

The hosted backend rejected your API key with HTTP 401 while the SDK was fetching its policy bundle (or calling another hosted endpoint). The key is one of:

  • unknown -- no key with that value exists (typo, wrong account, or a placeholder that was never real),
  • revoked -- the key was deactivated in the dashboard,
  • expired -- the key had an expiry and that time has passed.

For safety the backend does not tell you which of these it is (see Why the reason is coarse below). In every case the fix is the same: the key you are using is no longer valid, so get a fresh one.

Why it matters

This is a permanent failure: retrying with the same key will not help. The SDK surfaces it so you can correct your configuration.

How to fix

  1. Open the dashboard at app.controlzero.ai/settings/api-keys.

  2. Create a new API key (or copy an existing active one).

  3. Set it in the environment your agent runs in:

    export CONTROLZERO_API_KEY=cz_live_...   # or cz_test_...

    Or write it to the config file:

    controlzero install <agent> --api-key=cz_live_...
  4. Re-run your agent. The SDK pulls a fresh bundle on the next start.

If you just rotated a key, the most common cause is that the process still has the old value cached in its environment. Confirm the running process sees the new key:

# in the same shell/container the agent runs in
echo "${CONTROLZERO_API_KEY:0:12}..."

Diagnose with controlzero status

You do not have to wait for a tool call to fail with E1101 to find a dead key. controlzero status validates every configured key against the backend and reports each one as active, expired, or revoked, so a key that shows up in the dashboard but no longer authenticates is visible immediately:

controlzero status
[INFO] Control Zero status
state dir : /home/you/.controlzero
mode : HOSTED
config source: env CONTROLZERO_API_KEY
key (masked, env/global default): cz_live_...e88a
...
key status : revoked
project : Acme Prod
bindings (per-agent keys below override the env/global default above):
claude-code cz_live_...e88a active (project Acme Prod)
codex cz_live_...1f2c revoked (project Acme Sandbox)

The key status line is the same active/expired/revoked check the backend applies, so a revoked here is the same key that returns E1101 at runtime. If you bind different keys to different agents (see the bindings table), the status command tells you which agent's key is the dead one -- the common case behind "the dashboard shows two keys but the CLI rejects mine." Fix the specific binding with controlzero install <agent> --api-key=....

Note: against the live backend a rejected key is reported as revoked (the backend returns a single coarse reason by design -- see Why the reason is coarse); the point of status is to surface that the key is rejected at all, up front, per binding.

What you will see

The SDK surfaces this as a HostedAuthError (code E1101, a subclass of RuntimeError) with an actionable message:

[E1101] Your Control Zero API key was rejected by the backend (invalid,
revoked, or expired). Generate a new API key in the dashboard (Settings ->
API Keys, https://app.controlzero.ai/settings/api-keys) and set
CONTROLZERO_API_KEY to it.
Docs: https://docs.controlzero.ai/errors/E1101-key-rejected

Catching this error

except RuntimeError catches this (Python); HostedAuthError is the specific class. Programmatic callers can branch on the exception:

  • Python: except HostedAuthError as e: e.reason # "invalid_or_revoked" | None
  • Node: catch (e) { if (e instanceof HostedAuthError) e.reason }

The backend 401 body is machine-readable:

{
"error": {
"code": "INVALID_API_KEY",
"message": "invalid API key",
"error_code": "E1101",
"reason": "invalid_or_revoked",
"remediation": "This API key is unknown, revoked, or expired. Create a new key at https://app.controlzero.ai/settings/api-keys and set CONTROLZERO_API_KEY to it."
},
"error_code": "E1101",
"reason": "invalid_or_revoked",
"remediation": "This API key is unknown, revoked, or expired. Create a new key at https://app.controlzero.ai/settings/api-keys and set CONTROLZERO_API_KEY to it."
}

The reason and error_code appear both nested under error and at the top level, so either access pattern works.

Why the reason is coarse

The reason is always invalid_or_revoked for a rejected key. The backend deliberately does not distinguish unknown vs revoked vs expired. If it did, a 401 would become an enumeration oracle: an attacker could probe which key values exist or which were revoked by watching which reason came back. The pre-lookup cases that reveal nothing about real keys -- a missing key header (missing_api_key, code E1103) and a malformed key (invalid_api_key_format, code E1104) -- keep their own reasons because they touch no stored key.

See also