Код 401 означает лишь одно: сервер отклонил учётные данные. Это ещё не доказывает, что строка API-ключа набрана неверно. То же самое с 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 выдаёт одинаковое сообщение и когда вы ничего не отправили, и когда отправили ключ в заголовке, который он не читает. Поэтому при работе через relay можно видеть ошибку об отсутствующем заголовке, хотя ключ точно задан в shell.
Заголовки ответа не снимают эту неоднозначность полностью, но позволяют отличить отклонённые учётные данные от тех, что так и не дошли до этапа авторизации. Запустите шесть запросов и смотрите на заголовки, а не только на текст ошибки:
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
Если coding 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 сначала идёт env | grep ANTHROPIC в shell, из которого запускается клиент, затем /status, а после — удаление переменной, если вы рассчитывали работать по подписке. Проверка через env охватывает лишь строки 2 и 3 таблицы. Команда /status показывает, какой источник в итоге выбран: облачный провайдер, helper-скрипт или вход в аккаунт.
Откуда берётся ключ, который вы будто бы не задавали
Один отчёт хорошо иллюстрирует такой случай:
"Claude завис в режиме API billing, хотя переменной окружения API нет, а Claude Code успешно привязан к моему аккаунту Pro Max"
Первым делом мейнтейнер спросил, настроен ли apiKeyHelper. Он был настроен — и целиком выводил PLACEHOLDER_NOT_IMPLEMENTED_ON_MAC_YET. Если helper возвращает мусор, завершается с ненулевым кодом или вообще ничего не печатает, клиент отправляет placeholder вместо ключа. API отвечает 401, описывая проблему как ошибку ключа, а не скрипта. Теперь Claude Code сообщает об этой неисправности напрямую после трёх попыток. Там же отмечено, что до v2.1.208 сбой helper-скрипта превращался в общий 401 примерно после десяти тихих повторных попыток.
Переменные окружения могут появиться и без явной настройки с вашей стороны. Anthropic называет среди источников direnv, shell-плагины dotenv и терминалы IDE: они способны подгрузить устаревший ключ из проектного файла .env. Кроме того, helper-скрипт запускается заново через пять минут или после HTTP 401, поэтому неисправный источник возвращается по таймеру.
Если base URL ведёт на gateway
Когда ANTHROPIC_BASE_URL указывает на LLM-gateway, текст после 401 принадлежит gateway, а не Anthropic. Команда /login на это не влияет. Обратное верно и для OpenAI-совместимых клиентов, направленных на relay. Вот пример из отчёта Codex, где строка ошибки заканчивается на url: https://openrouter.ai/api/v1/responses:
"Я очистил все переменные окружения, связанные с API-ключами, удалил файл авторизации — ничего не помогло; много раз вошёл заново, но ошибка остаётся"
Очистка локальных учётных данных здесь ничего не исправит: проверяющего определяет base URL. Подберите учётные данные под сервис, который находится на этом эндпоинте. Claude Code документирует ANTHROPIC_AUTH_TOKEN для gateway, аутентифицирующих запросы bearer-токенами. Если записать то же значение в ANTHROPIC_API_KEY, оно уйдёт в X-Api-Key — это третья строка таблицы. У других клиентов свои правила: проверьте, какой заголовок отправляет именно ваш. Исключение — Claude Code on the Web: он всегда использует учётные данные подписки, а обе переменные в sandbox их не переопределяют.
Когда 403 не связан с ключом
Ни один из следующих случаев не воспроизводился вручную: для каждого нужен определённый статус аккаунта или география. Но документация провайдеров однозначно подтверждает, что валидный ключ тоже может привести к 403.
В справочнике ошибок OpenAI есть 403 - Country, region, or territory not supported. Это географическая проверка, не затрагивающая ключ. На две строки выше расположен зеркальный случай: 401 - IP not authorized. Он возникает, когда IP-адрес запроса не входит в allowlist проекта или организации. Ключ валиден, но недопустим сам вызывающий клиент.
Для ошибки API Error: 403 {"error":{"type":"forbidden","message":"Request not allowed"}} после успешного входа Anthropic указывает три причины — и среди них нет опечатки в ключе: неактивная подписка Pro или Max, отсутствие роли "Claude Code" или "Developer" у аккаунта Console либо корпоративный proxy, вмешивающийся в запрос. В Platform API 403 - permission_error означает, что у ключа нет прав на этот ресурс: они проверяются по настройкам организации и workspace. А на compliance-эндпоинтах валидный ключ с неподходящими scopes по замыслу возвращает 403, а не 401.
FAQ
Что означает ошибка 403 в Claude API?
Она может означать две разные вещи. permission_error говорит, что у ключа нет доступа к нужному ресурсу — это определяется настройками организации или workspace. А Request not allowed после входа указывает на статус подписки, отсутствующую роль Console или proxy.
Стоит ли повторять запрос при 401?
Сам по себе 401 повторять не стоит. Отклонённые или отсутствующие учётные данные при следующей попытке дадут тот же результат — в отличие от 429, где решением будет сначала учесть Retry-After, а затем увеличивать задержку. Единственный 401, который способен исчезнуть сам, связан с apiKeyHelper: Claude Code и так делает ещё две попытки, прежде чем сообщить об ошибке.