看到 401,不代表 API Key 本身一定写错;遇到 403,也未必是权限不足。不同服务商会在凭据完全有效的情况下返回这两种状态码。排查前有一个例外值得优先确认:如果报错回显了脱敏后的密钥,而它显示出的首尾字符与你手里的密钥对不上,那么真正到达服务器的就不是你打算发送的那一把。
看起来一样的三种 401
下面三种情况都会返回 HTTP 401 和认证错误类型,但修复方向恰好不同。表中的信息来自我向两个端点发送六次刻意构造的无效密钥请求后得到的响应。错误凭据会在授权流程开始前被拒绝,因此无需真实密钥也能复现。
| 实际发生的情况 | Anthropic /v1/messages | OpenAI /v1/responses |
|---|---|---|
| 密钥已送达,但被拒绝 | API key is invalid. | Incorrect API key provided: sk-proj-**********-key.,并带有 "code": "invalid_api_key" |
| 根本没有凭据送达 | x-api-key header is required | Missing bearer or basic authentication in header |
| 凭据送到了错误的请求头 | Invalid bearer token | Missing bearer or basic authentication in header,与上一行完全相同 |
Anthropic 会明确区分这三种情况。OpenAI 端点则会把“什么都没发”和“发到了它不读取的请求头”归为同一条报错。因此,使用中转服务的人可能明明能证明 shell 里设置了密钥,却仍然盯着“缺少请求头”的错误无从下手。
响应头无法进一步区分这两种 OpenAI 情况,但能帮助判断凭据是被拒绝,还是根本没进入授权流程。与其只看报错文案,不如运行这六个请求并观察响应头:
show(){ shift; curl -sS -D - -o /dev/stdout "$@" \
| grep -iE "^HTTP|www-authenticate|x-openai-authorization-error|request-id|x-should-retry|message"; }
A=(-X POST https://api.anthropic.com/v1/messages -H "anthropic-version: 2023-06-01"
-H "content-type: application/json"
-d '{"model":"claude-sonnet-4-5","max_tokens":8,"messages":[{"role":"user","content":"hi"}]}')
O=(-X POST https://api.openai.com/v1/responses -H "content-type: application/json"
-d '{"model":"gpt-5.6","max_output_tokens":16,"input":"hi"}')
show a1 "${A[@]}" -H "x-api-key: sk-ant-api03-not-a-real-key" # rejected
show a2 "${A[@]}" # never arrived
show a3 "${A[@]}" -H "Authorization: Bearer sk-ant-api03-not-a-real-key" # wrong header
show o1 "${O[@]}" -H "Authorization: Bearer sk-proj-not-a-real-key" # rejected
show o2 "${O[@]}" # never arrived
show o3 "${O[@]}" -H "x-api-key: sk-proj-not-a-real-key" # wrong header
以下是 2026-07-31 得到的关键差异字段,其余内容已省略:
o1 rejected HTTP/2 401 x-openai-authorization-error: 401 "code": "invalid_api_key"
o2 never arrived HTTP/2 401 www-authenticate: Bearer realm="OpenAI API"
o3 wrong header HTTP/2 401 www-authenticate: Bearer realm="OpenAI API"
a1 rejected HTTP/2 401 "request_id": null (no request-id, no x-should-retry)
a2 never arrived HTTP/2 401 request-id: req_011CdZnC9j… x-should-retry: false
a3 wrong header HTTP/2 401 request-id: req_011CdZnCDN… x-should-retry: false
在这两个端点上,被拒绝的凭据都会带有服务商自己的授权字段,同时不再返回 challenge 请求头;未进入授权流程的请求则正好相反。重复请求的结果也是如此。不过,这是针对两个端点在特定日期的观察,不是文档承诺的行为。不要直接套用到其他服务商,最好运行上面的命令自行验证。若结果落在“密钥已送达但被拒绝”这一行,就去 OpenAI 或 Claude 的密钥页面检查四件事:值的开头或结尾是否带了空白字符、密钥是否已删除或撤销、密钥所属项目或组织是否与当前调用目标一致,以及客户端是否仍缓存着旧副本。前三项确认无误后,再考虑重新生成密钥。
CLI 实际发送的是哪一份凭据
如果编程 CLI 提示某个你从未配置过的密钥无效,它通常不是缓存出了问题,而是有更高优先级的凭据覆盖了你的选择。Claude Code 会按固定顺序解析六类凭据来源;这套官方优先级规则也决定了每种凭据会使用哪个请求头发送。
| 优先级 | 来源 | 发送的请求头 |
|---|---|---|
| 1 | 云服务商凭据(CLAUDE_CODE_USE_BEDROCK、_VERTEX、_FOUNDRY) | 服务商专用 |
| 2 | ANTHROPIC_AUTH_TOKEN | Authorization: Bearer |
| 3 | ANTHROPIC_API_KEY | X-Api-Key |
| 4 | apiKeyHelper 脚本输出 | 按脚本返回内容发送 |
| 5 | CLAUDE_CODE_OAUTH_TOKEN | OAuth |
| 6 | 通过 /login 登录的订阅账户 | OAuth |
即使你已经登录订阅账户,只要存在有效的 ANTHROPIC_API_KEY,它仍会优先于订阅登录;因此 /login 不会覆盖它。使用 -p 参数时,只要该密钥存在,就一定会优先使用。Anthropic 官方给出的排查顺序是:先在启动 Claude Code 的 shell 中执行 env | grep ANTHROPIC,再运行 /status,如果本意是使用订阅账户,就取消设置相关变量。需要注意,env 只能检查表中的第 2、3 行;云服务商、辅助脚本或登录状态究竟哪个被选中,要靠 /status 查看。
那个你确信从未设置过的凭据
下面这份问题报告很能说明实际场景:
“Claude 一直卡在 API 计费模式,尽管没有设置任何 API 环境变量,而且我已经成功把 claude code 关联到 pro max 账户。”
维护者首先询问的就是是否配置了 apiKeyHelper。答案是配置过,而脚本全部内容只会输出 PLACEHOLDER_NOT_IMPLEMENTED_ON_MAC_YET。辅助脚本若返回垃圾内容、以非零状态退出,或什么都不输出,都会让客户端发送一个占位凭据;API 随后返回 401,错误指向的是密钥本身而不是脚本。Claude Code 现在会在三次尝试内按名称报告这一故障。同一条目也说明,在 v2.1.208 之前,辅助脚本失败大约会经历十次静默重试,最终只显示一个普通 401。
环境变量也可能在你不知情时被写入。Anthropic 在错误文档中列出 direnv、dotenv shell 插件和 IDE 终端等来源:它们可能从项目的 .env 文件加载旧密钥。此外,辅助脚本会在五分钟后或遇到 HTTP 401 时再次执行,所以坏掉的脚本会定时卷土重来。
Base URL 指向网关时
如果 ANTHROPIC_BASE_URL 指向某个 LLM 网关,401 后面的报错文字来自网关,而不是 Anthropic;此时 /login 无法改变结果。反过来也是一样:将 OpenAI 兼容客户端指向中转服务时,认证由中转服务决定。下面是一份 Codex 报告,其中错误字符串以 url: https://openrouter.ai/api/v1/responses 结尾:
“我试过清除所有和 API Key 有关的环境变量,也删了认证文件,重新登录很多次,错误还是一直存在。”
清理本地凭据解决不了这个问题,因为真正决定谁来验证请求的是 Base URL。应当为该端点背后的服务匹配对应凭据。Claude Code 文档规定,对于使用 bearer token 认证的网关,应使用 ANTHROPIC_AUTH_TOKEN;若把同一个值放进 ANTHROPIC_API_KEY,它会改用 X-Api-Key 发出,也就是上表第 3 行的行为。其他客户端遵循各自的规则,需要确认它实际发送的是哪个请求头。Claude Code on the Web 是例外:它始终使用订阅凭据,在沙箱中设置任一变量都不会覆盖这一行为。
密钥没问题也会出现的 403
这一部分并非我的第一手复现,因为每种情况都需要特定的账户状态或地理位置;但服务商文档说得很明确:有效密钥同样可能返回 403。
OpenAI 的错误参考列出了 403 - Country, region, or territory not supported。这是地理限制检查,与密钥无关。其上方两行还有一个相反的情形:401 - IP not authorized,当请求 IP 不在项目或组织的允许列表内时触发。密钥有效,调用方却不被允许。
如果成功登录后收到 API Error: 403 {"error":{"type":"forbidden","message":"Request not allowed"}},Anthropic 在排障文档中给出了三种原因,均不是密钥输错:Pro 或 Max 订阅未激活、Console 账户缺少“Claude Code”或“Developer”角色,或企业代理干扰了请求。对于平台 API,403 - permission_error 表示该密钥无权访问对应资源,权限取决于组织和工作区设置;而在合规端点上,拥有有效密钥但 scopes 不匹配时,按设计会返回 403 而非 401。
常见问题
Claude API 的 403 错误码是什么意思?
可能是两种不同情况。permission_error 表示你的密钥没有访问该资源的权限,需要检查组织或工作区设置。登录后出现的 Request not allowed 则通常指向订阅状态、缺少 Console 角色,或代理问题。
401 值得重试吗?
单独看 401,不值得。凭据被拒绝或根本未送达时,再试一次只会得到相同结果;这和 429 不同,后者的正确处理是先遵守 Retry-After,再进行退避重试。唯一可能自行恢复的 401 是 apiKeyHelper 的情况,而 Claude Code 在报错前本来就会额外重试两次。