401 Unauthorized — it's usually not the key you just regenerated
- Check which header the endpoint wants: ANTHROPIC_AUTH_TOKEN sends
Authorization: Bearer, ANTHROPIC_API_KEY sendsx-api-key— set the wrong one and a valid key still 401s. - Run
env | grep -i anthropicfor an older value shadowing the new one, then restart the client (it reads the environment once, at startup). - Repeat the request with curl: curl works, client 401s = the client isn't reading your variable; curl 401s too = credential or endpoint.
When a client talks to a compatible endpoint there are two separate authentication layers, and a 401 can come from either one. The error text looks identical whichever layer rejected it. Regenerating your own key only addresses one of them — and in practice it's the less common one.
What you're seeing
API Error: 401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}
401 Unauthorized
Could not resolve authentication method. Expected either apiKey or authToken to be set.
{"error":{"message":"Invalid bearer token","type":"authentication_error"}}
All four mean the request arrived without credentials the server accepted. None of them says which layer rejected it — but the wording narrows it down.
| Message | What it tells you | Most likely cause |
|---|---|---|
invalid x-api-key | Server read an x-api-key header and rejected the value | Wrong key — or the endpoint wanted a Bearer token |
Invalid bearer token | Server read Authorization: Bearer and rejected the value | Wrong token — or the endpoint wanted x-api-key |
Could not resolve authentication method | The client gave up before sending anything | Neither variable set, or not the one this client reads |
Bare 401, no body | Something rejected it without explaining | A proxy or gateway hop you didn't know was there |
The two layers
| Who authenticates to whom | How failure behaves | Who can fix it | |
|---|---|---|---|
| Layer 1 | Your client → the endpoint | Fails immediately, on the first request, every time | You |
| Layer 2 | The endpoint → its upstream provider | Often intermittent, or starts suddenly with no config change | The endpoint operator |
Most write-ups only cover layer 1 — "check your key." That advice is fine, but it cannot explain the case that actually confuses people: it worked an hour ago and you changed nothing. When that happens, layer 1 is not where your problem is.
Check in this order
1 · Is the credential actually reaching the request?#
Copy-paste carries invisible passengers — trailing newlines, non-breaking spaces, smart quotes from a chat window.
printf '%s' "$ANTHROPIC_AUTH_TOKEN" | wc -c
printf '%s' "$ANTHROPIC_AUTH_TOKEN" | tail -c 20 | xxd | tail -2
A byte count one higher than the visible length means a trailing newline came along for the ride.
2 · Are you setting the variable that produces the header the endpoint expects?#
This is the quietest failure of the four, because nothing in the message hints at it.
| Variable | Header the client sends |
|---|---|
ANTHROPIC_API_KEY | x-api-key: <value> |
ANTHROPIC_AUTH_TOKEN | Authorization: Bearer <value> |
If the endpoint authenticates on Authorization: Bearer and you set ANTHROPIC_API_KEY, the request arrives with no Authorization header at all. The server answers 401 with a message about an invalid key. The key is fine; the header is wrong. The reverse happens too.
With 9Coding, set ANTHROPIC_AUTH_TOKEN — the key begins with sk-9c-.
3 · Does your base URL end where the client expects it to?#
Clients append their own path (/v1/messages, /v1/chat/completions) to the base URL you give them. Set the base URL with /v1 already on the end and the request goes to /v1/v1/messages. That usually returns 404 — but a gateway that authenticates before routing can return 401 instead.
echo "$ANTHROPIC_BASE_URL"
The correct value is exactly https://api.9coding.com — no trailing slash, no /v1.
4 · Is an older value shadowing the one you just set?#
A value exported in a shell profile, a value in a project config file, and a value in the current shell are three different things. The client reads one of them; you may be editing another.
env | grep -i anthropic
Check every hit, not just the one you remember setting. Then restart the client — Claude Code reads environment variables once at startup, so a long-running process keeps the environment it began with.
5 · Take the client out of the picture#
curl -s -w '\n%{http_code}\n' https://api.9coding.com/v1/models \
-H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN"
- curl returns 200, client returns 401 → the client isn't reading the variable you think it is. Back to step 4.
- curl returns 401 too → the credential or the endpoint is the problem, not the client.
- curl fails before returning a status → this isn't an auth problem. See connection errors.
When it isn't your problem
If all of these are true, you're looking at layer 2 and there is nothing to fix on your machine:
- it worked earlier today,
- you changed no configuration,
- other clients pointed at the same endpoint fail the same way,
- and it recovers on its own after a while.
Check the operator's status page and changelog before spending another hour on your own setup. An endpoint that publishes neither is an endpoint you cannot debug — worth knowing in itself. Ours is at status.
When reporting it, include the request id — 9Coding error responses carry one in the form (request id: 2026...). That id plus the timestamp, the model name and the full error text lets the exact call be traced. A report that only says it doesn't work cannot be investigated.
401 or 403 — not the same problem
A 403 means the request was authenticated and then refused: the account lacks access to that model, that region, or that feature. Rotating the key is guaranteed not to help. See 403 Forbidden.
Related
- 403 Forbidden — authenticated but not permitted
- TLS certificate errors — why they fail on the first try and never retry
- Connection errors — separating network, proxy, and endpoint
- All Claude Code errors — the quick reference table