Claude Code 401: the header is the difference
You exported ANTHROPIC_API_KEY, pointed ANTHROPIC_BASE_URL at your endpoint, ran Claude Code — and got 401 unauthorized.
The first thing to know is that this client does not authenticate the way an OpenAI-style client does. OpenAI-compatible clients build an Authorization: Bearer <key> header. The Anthropic-style path uses two headers instead: x-api-key for the credential and anthropic-version for the API version. A request shape built for one will be rejected by the other even when the key is perfect.
That is why this error survives key regeneration. You are not changing the thing that failed.
https://aicomp.ai/v1).
Create one free →
Prove the key first, with the right headers
# Anthropic-style auth: x-api-key, not Authorization: Bearer
curl -s -o /dev/null -w "%{http_code}\n" --max-time 20 \
"$ANTHROPIC_BASE_URL/v1/models" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01"
Then the same call with the other header shape, so you can see the difference yourself:
# OpenAI-style auth against the same URL, this is what fails
curl -s -o /dev/null -w "%{http_code}\n" --max-time 20 \
"$ANTHROPIC_BASE_URL/v1/models" \
-H "Authorization: Bearer $ANTHROPIC_API_KEY"
If the first returns 200 and the second 401, your credential is fine and the header shape is the problem. The version string is a date; 2023-06-01 is the long-standing value, and the one to keep if your client does not set it (check your client's documentation if you are pinned to a different version).
Where the environment variables fail
The most common cause is not the value but the scope. Claude Code reads these from the environment of the process it runs in, and a shell variable is only visible to processes launched from that shell.
# Are they actually set in this shell?
env | grep -E "^ANTHROPIC_" || echo "not set in this shell"
# Then relaunch the client from the same shell that has them
Three ways this goes wrong:
- Exported in one terminal, launched from another. The editor or desktop launcher has a different environment.
- Set in a shell profile that a non-interactive shell does not read.
.bashrcand.zshrcare not read identically in every context. - Overridden by a project-level settings file. A value saved for one project wins over your global export, and it keeps winning after you fix the global one.
Check the third one before you regenerate anything. A stale project-level key produces a 401 in one directory and not another, which is the clearest signal you are fighting scope rather than credentials.
The base URL path problem
A 401 means the credential was rejected. A 404 means it was accepted and the path was wrong. Both show up while configuring this, and the fix for one makes the other worse.
for base in "$ANTHROPIC_BASE_URL" "${ANTHROPIC_BASE_URL%/v1}"; do
for path in "/v1/models" "/models"; do
printf "%-50s " "$base$path"
curl -s -o /dev/null -w "%{http_code}\n" --max-time 12 "$base$path" \
-H "x-api-key: $ANTHROPIC_API_KEY" -H "anthropic-version: 2023-06-01"
done
done
Exactly one combination returns 200. Set ANTHROPIC_BASE_URL to whatever produced it, minus the trailing /models. Some clients append /v1 themselves and some do not, the loop settles it in one run instead of four restarts.
The failure that costs money rather than erroring
Worth stating because it is silent: if the override never applies, requests go to the vendor instead of your endpoint. They succeed. Tasks finish. Nothing errors. The only evidence is that your endpoint's request log stays empty while you are working.
After any configuration change, confirm a request actually arrived. That log is the only place outside the client where the destination is visible.
FAQ
FAQ
Why does my key work in curl but not in Claude Code?
Usually header shape or environment scope. If you tested with Authorization: Bearer and the client sends x-api-key, or the reverse, one succeeds and the other 401s with the same key. If curl works from a terminal and the client does not, the client was probably launched from a different environment that never saw the export.
Do I need both x-api-key and anthropic-version?
Both are part of the Anthropic-style request shape. The credential and the version travel in separate headers rather than one token. A request missing the version header can be rejected even with a valid key, which is why a key that works elsewhere still fails here.
Should ANTHROPIC_BASE_URL end in /v1?
It should end where the endpoint's version path ends and before the resource path begins. Clients differ on whether they append /v1 themselves. Run the loop above: the combination returning 200 tells you which shape your client produces.
Is 401 the same as model_not_found?
No. A 401 is rejected before routing; model_not_found fires after the credential was accepted and the model string did not match. Fixing the key when you have a 404, or the URL when you have a 401, means editing the field that was already correct.
Can one key serve Claude Code and other tools?
Yes That is the practical reason to route through one endpoint. One credential, one bill, and spend visible per model instead of spread across vendor dashboards. Keep it out of committed files; export it in the shell you launch from.
Related
- Claude Code with a custom API — the base setup this page assumes
- Claude API errors — the other codes and what they mean
- Claude Code 429 rate limit — when the key works and the rate does not
- Base URL not working — doubled paths across clients
- OpenAI-compatible error codes — status codes in the order they fire
- How the gateway routes requests — what the endpoint does with the headers