Short answer
A 401 response with "type": "authentication_error" and the message invalid x-api-key means the Claude API rejected the credential in the request's x-api-key header. Anthropic's error reference says a 401 means there is an issue with your API key, for example that it's malformed, revoked or expired. Retrying with the same key won't change the result.
Often the key you created is fine and a different value is sent in its place. Check these three things in order:
- What was sent. A stale
ANTHROPIC_API_KEY, a placeholder, or a${VAR}that was never filled in. - Where it was sent. A gateway key sent to
api.anthropic.comis rejected, and so is an Anthropic key sent to a gateway that issues its own keys. - Whether the key still works. Disabled, deleted and expired keys stop working. Check the API keys page in the Claude Console.
What the error looks like
The raw response from api.anthropic.com, as quoted in anomalyco/opencode#47114 (request ID shortened):
{"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"},"request_id":"req_..."}
Tools wrap it in their own text. Two examples from public bug reports:
401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}
Warning: Error 401: authentication_error - invalid x-api-key
The first is from Gentleman-Programming/gentle-shell#681, the second from rocketride-org/rocketride-server#1105. In the official Python SDK, a 401 raises anthropic.AuthenticationError.
When a valid key stops working
Anthropic's authentication docs list these ways a key stops working:
| Situation | What happens |
|---|---|
| The key is disabled in the Console | Requests fail until someone clicks Re-enable |
| The key is deleted | Permanent. The key is archived and can't be restored |
| The key has expired | Requests return 401 authentication_error. Expired keys can't be reactivated; create a new one |
| A personal key's owner leaves the organization | The key is archived. Being re-invited doesn't bring it back |
| A workspace key (legacy) loses its workspace | The key stops working when the workspace is archived |
Expiration is set when the key is created: 3 hours, 1 day, 7 days, 30 days, a custom duration, or Never if your organization allows it. Anthropic emails the key's creator before keys with a lifetime of 7 days or more expire. Shorter-lived keys expire without a warning email. The API keys table in the Console shows each key's expiration date.
When the key looks right but still fails
These are the patterns behind most public reports of this error:
- Another value shadows your key. In gentle-shell#681, the tool preferred an environment variable over its own saved credential, and a placeholder
ANTHROPIC_API_KEYinherited from the parent process won. Unsetting the variable fixed it. The official Python SDK also readsANTHROPIC_API_KEYfrom the environment when you don't passapi_key. - A placeholder was sent as-is. In rocketride-server#1105, a key field set to
${ROCKETRIDE_ANTHROPIC_KEY}was sent literally instead of being replaced with the value from.env. - The request went to Anthropic instead of your gateway. In opencode#47114, a configured proxy base URL was lost after a plugin reload. The proxy's key went to
api.anthropic.comand came back asinvalid x-api-key. The reporter also flagged the security side: a key meant for a private relay was sent to a host the user never configured. - A subscription token was sent as an API key. In openclaw/openclaw#178387, a credential from
claude setup-tokenworked for chat, but a PDF tool sent it inx-api-keyand got this 401. The analysis in that issue is that this kind of credential has to go inAuthorization: Bearer. Console API keys start withsk-ant-api. - The key was copied incompletely. A missing character, a trailing newline or a space from a chat message or a document makes the key malformed.
How to fix it
- Look at what your code actually sends. Print a masked version of the key and the base URL from the same process that makes the request:
import os
key = os.environ.get("ANTHROPIC_API_KEY", "")
print(len(key), repr(key[:10]), repr(key[-4:]))
print(os.environ.get("ANTHROPIC_BASE_URL"))
repr shows a trailing \n or space. A key that doesn't start with sk-ant-api isn't an Anthropic Console API key.
- Test the key on its own. Bypass your app and call the API directly with the same key:
curl https://api.anthropic.com/v1/models \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01"
If this returns a model list, the key is valid and the problem is in how your app loads or routes it. If it returns the same 401, fix the key itself.
- Check the key in the Console. On the API keys page, check its status and expiration. If it was deleted or has expired, create a new key and replace the old value everywhere it is stored, including
.envfiles, CI secrets and secrets managers. - Remove stale values. Run
env | grep ANTHROPIC(PowerShell:Get-ChildItem Env:ANTHROPIC*) in the shell that starts your app. Remove old exports from shell profiles, and check that.envplaceholders are filled in at runtime. - Check the host. If you use a gateway, make sure every code path uses its base URL. The Python SDK reads
ANTHROPIC_BASE_URL, but other tools may only read their own setting. A key from one service won't work on another. - Match the header to the credential. Console API keys work in
x-api-key. Anthropic's docs now recommendAuthorization: Bearer <key>and keepx-api-keyas a supported legacy header. Tokens from a Claude subscription login aren't Console API keys.
Troubleshooting table
| What you see | Likely cause | Fix |
|---|---|---|
| 401 right after you rotated keys | The old key is still set somewhere | Search env vars, .env files and CI secrets for the old value |
curl works, your app fails |
The app loads a different value or sends it elsewhere | Print the masked key and base URL from inside the app |
| Works locally, fails in CI | The secret is empty or misnamed in CI | Check the secret's name and that the job can read it |
| Fails only through a proxy setup | Requests reach api.anthropic.com with the proxy's key |
Set the base URL in every code path, not only in one config |
| Works with chat, fails with one tool | That tool sends the credential in another header | Use a Console API key for that tool, or report it to its maintainers |
| Key worked for days, then stopped | It expired or was disabled | Check its expiration and status in the Console |
Using a TeamoRouter key
TeamoRouter is an API gateway with one key for Claude, GPT, Gemini and DeepSeek. Claude runs on the native Anthropic protocol (/v1/messages). TeamoRouter keys start with sk-teamo-. Sent to api.anthropic.com, a TeamoRouter key is rejected like any other key Anthropic didn't issue, so set the base URL everywhere you use it. There's no subscription; you pay per use.
- Create an API key in the TeamoRouter console.
- Top up from $5. The top-up dialog takes cards. You can also click Pay with crypto and pay with USDT, USDC or ETH from any exchange app — the checkout shows which networks your amount supports (BEP20 works from $5). Step by step: Pay for Claude with USDT.
- Test the key:
curl https://api.teamorouter.com/v1/models \
-H "x-api-key: sk-teamo-..."
Authorization: Bearer sk-teamo-... works too. A model list means the key is good.
- Point the SDK at it:
from anthropic import Anthropic
client = Anthropic(
api_key="sk-teamo-...",
base_url="https://api.teamorouter.com",
)
resp = client.messages.create(
model="claude-fable-5-1",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)
print(resp.content[0].text)
Leave /v1 off the base URL. The same key works for GPT on /v1/chat/completions and /v1/responses, and for Gemini. Details are in the API integration guide. For Claude Code, see Install and connect Claude Code.
FAQ
Is "invalid x-api-key" a billing problem?
No. Anthropic uses 402 billing_error for billing and payment problems, and a low prepaid balance has its own message. See Anthropic API "Your credit balance is too low".
What's the difference between 401 and 403?
A 401 authentication_error means the key itself was rejected. A 403 permission_error means the key is accepted but doesn't have permission for the resource you requested.
Will the error go away if I wait? No. A disabled key needs to be re-enabled. A deleted or expired key needs to be replaced with a new one.
Should I send x-api-key or Authorization?
Either works with Console API keys. Anthropic's docs present Authorization: Bearer as the main header and x-api-key as a legacy header that is still supported.