Un 401 indica que se rechazó una credencial, no necesariamente que hayas escrito mal la API key. Y un 403 tampoco implica siempre que falten permisos. Ambos proveedores devuelven esos códigos incluso cuando la credencial es totalmente válida. Antes de revisar nada más, hay una excepción muy útil: si el rechazo muestra una clave enmascarada cuyos primeros y últimos caracteres no coinciden con los de tu clave, el servidor está recibiendo una credencial distinta de la que creías enviar.
Tres errores 401 que parecen el mismo
Los siguientes casos devuelven HTTP 401 con un error de autenticación, aunque cada uno exige una solución diferente. Los mensajes corresponden a seis solicitudes enviadas a estos dos endpoints con claves inválidas a propósito. Como una credencial incorrecta se rechaza antes de la fase de autorización, no necesitas una clave real para reproducirlas.
| Qué ocurrió realmente | Anthropic /v1/messages | OpenAI /v1/responses |
|---|---|---|
| La clave llegó y fue rechazada | API key is invalid. | Incorrect API key provided: sk-proj-**********-key. con "code": "invalid_api_key" |
| No llegó ninguna credencial | x-api-key header is required | Missing bearer or basic authentication in header |
| La credencial llegó en la cabecera equivocada | Invalid bearer token | Missing bearer or basic authentication in header, idéntico a la fila anterior |
Anthropic diferencia los tres escenarios. En cambio, el endpoint de OpenAI responde igual cuando no envías nada y cuando mandas la clave en una cabecera que no interpreta. Por eso, quien usa un relay puede encontrarse un error de cabecera ausente aunque la clave esté claramente definida en la shell.
Las cabeceras de respuesta no eliminan esa ambigüedad, pero sí permiten separar una credencial rechazada de otra que ni siquiera llegó a la autorización. Ejecuta las seis llamadas y fíjate en las cabeceras, no solo en el texto del error:
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
Estas fueron las líneas que permitían distinguirlos el 2026-07-31, reducidas a los campos que cambian:
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
En ambos endpoints, una credencial rechazada incluía el campo de autorización propio del proveedor y no llevaba la cabecera de desafío; cuando la credencial no alcanzaba la autorización, ocurría lo contrario. Las llamadas repetidas mostraron la misma división, pero se trata de una observación fechada en dos endpoints, no de un comportamiento documentado. Vuelve a ejecutar el bloque anterior en lugar de asumir que se aplicará a tu proveedor. Si caes en la fila de clave rechazada, entra en la página de claves de OpenAI o Claude y revisa cuatro puntos: espacios al principio o al final del valor, una clave eliminada o revocada, una clave creada para un proyecto u organización diferente del que estás consultando, y una copia antigua que el cliente aún conserva en caché. Regenera la clave solo cuando los tres primeros estén descartados.
Qué credencial envió realmente tu CLI
Si una CLI de programación informa de una clave inválida que tú no configuraste, probablemente no se deba a la caché: otra credencial ha tenido prioridad. Claude Code resuelve seis fuentes en un orden fijo, y esa precedencia documentada también determina la cabecera por la que viaja cada una.
| Prioridad | Origen | Cabecera enviada |
|---|---|---|
| 1 | Credenciales de proveedor cloud (CLAUDE_CODE_USE_BEDROCK, _VERTEX, _FOUNDRY) | específica del proveedor |
| 2 | ANTHROPIC_AUTH_TOKEN | Authorization: Bearer |
| 3 | ANTHROPIC_API_KEY | X-Api-Key |
| 4 | Salida del script apiKeyHelper | tal como se devuelve |
| 5 | CLAUDE_CODE_OAUTH_TOKEN | OAuth |
| 6 | Inicio de sesión de suscripción desde /login | OAuth |
Una ANTHROPIC_API_KEY válida tiene prioridad sobre el inicio de sesión de tu suscripción, así que /login no la desplaza. Con la opción -p, la clave siempre se utiliza si está presente. La propia lista de comprobaciones de Anthropic recomienda ejecutar env | grep ANTHROPIC en la shell desde la que lanzas el comando, después /status y, si pretendías usar la suscripción, eliminar la variable. La comprobación con env solo cubre las filas 2 y 3 de la tabla; /status es lo que revela si la fuente resuelta es un proveedor cloud, un script auxiliar o un inicio de sesión.
La credencial que juras no haber configurado
Este informe muestra bien el patrón:
"Claude se queda atascado en el modo de facturación de la API aunque no hay ninguna variable de entorno de API configurada y he vinculado correctamente Claude Code a mi cuenta Pro Max"
La primera pregunta de un mantenedor fue si había un apiKeyHelper configurado. Lo había, y todo su contenido imprimía PLACEHOLDER_NOT_IMPLEMENTED_ON_MAC_YET. Un helper que devuelve contenido basura, termina con un código distinto de cero o no imprime nada envía una credencial de marcador de posición. La API la rechaza con un 401 que habla de la clave, no del script. Claude Code ahora identifica ese fallo explícitamente tras tres intentos; la misma entrada indica que, antes de v2.1.208, los fallos del helper aparecían como un 401 genérico tras unos diez reintentos silenciosos.
Las variables de entorno también pueden aparecer sin que las hayas definido tú. Anthropic menciona direnv, plugins de shell para dotenv y terminales de IDE como fuentes que cargan una clave antigua desde un archivo .env del proyecto. Además, el helper se ejecuta de nuevo después de cinco minutos o tras un HTTP 401, de modo que uno defectuoso puede reaparecer por temporizador.
Cuando la URL base apunta a un gateway
Si ANTHROPIC_BASE_URL apunta a un gateway de LLM, el texto que acompaña al 401 lo genera el gateway, no Anthropic, y /login no puede cambiarlo. Lo mismo sucede a la inversa con clientes compatibles con OpenAI que apuntan a un relay. En un informe de Codex cuyo error termina con url: https://openrouter.ai/api/v1/responses:
"Intenté borrar todas las variables de entorno relacionadas con las API keys, también eliminé el archivo de autenticación; no funcionó. Volví a iniciar sesión muchas veces y el error persiste"
Borrar las credenciales locales no puede resolverlo: la URL base decide quién evalúa la solicitud. Usa la credencial correspondiente al servicio situado en ese endpoint. Claude Code documenta ANTHROPIC_AUTH_TOKEN para gateways que se autentican mediante tokens bearer; si colocas el mismo valor en ANTHROPIC_API_KEY, saldrá como X-Api-Key, que corresponde a la tercera fila de la tabla anterior. Otros clientes siguen su propio contrato: comprueba qué cabecera envía el tuyo. Claude Code on the Web es la excepción: siempre usa las credenciales de suscripción y definir cualquiera de las dos variables dentro del sandbox no las reemplaza.
Casos 403 en los que la clave está bien
Ninguno de estos casos se ha reproducido de primera mano, ya que todos requieren un estado de cuenta o una ubicación geográfica específicos. Se basan en la documentación de los proveedores, que deja claro que una clave válida puede devolver un 403.
La referencia de errores de OpenAI incluye 403 - Country, region, or territory not supported: es una comprobación geográfica y no afecta a la clave. Dos filas más arriba aparece el caso inverso, 401 - IP not authorized, que se produce cuando la IP de la solicitud queda fuera de la lista de permitidas del proyecto o la organización. La clave es válida; quien llama no lo es.
Ante API Error: 403 {"error":{"type":"forbidden","message":"Request not allowed"}} después de iniciar sesión correctamente, Anthropic señala tres causas, ninguna relacionada con una clave mal escrita: una suscripción Pro o Max inactiva, una cuenta de Console sin el rol "Claude Code" o "Developer", o un proxy corporativo que interfiere con la solicitud. En la API de plataforma, 403 - permission_error significa que la clave no tiene permiso para ese recurso, según la configuración de la organización y el espacio de trabajo. En los endpoints de compliance, una clave válida con los scopes equivocados devuelve 403 en lugar de 401 por diseño.
Preguntas frecuentes
¿Qué significa el código de error 403 en la API de Claude?
Puede significar dos cosas distintas. permission_error indica que tu clave no tiene permiso para ese recurso, algo que depende de la organización o del espacio de trabajo. En cambio, Request not allowed después del inicio de sesión apunta al estado de la suscripción, a la falta de un rol de Console o a un proxy.
¿Merece la pena reintentar un 401?
No por sí solo. Una credencial rechazada o ausente devolverá el mismo resultado en el siguiente intento, a diferencia de un 429, donde la solución es respetar Retry-After antes de aplicar backoff. El único 401 que puede resolverse solo es el caso de apiKeyHelper, que Claude Code ya reintenta dos veces más antes de mostrar el error.