# Codex says "unexpected status 401 Unauthorized" or "Missing environment variable"

> Markdown mirror of https://router.one/codex-401-unauthorized for AI assistants and crawlers. Router One is a unified, OpenAI-compatible LLM API gateway.
> Last updated: 2026-11-02

Both messages are about the API key Codex sends to a model provider, at two different moments. Per the openai/codex source (rust-v0.166.0, checked 2026-11-02), a custom provider with env_key reads its key from the environment variable env_key names; when that variable is unset, empty or only whitespace, Codex sends no request and prints `Missing environment variable` with the variable's name. `unexpected status 401 Unauthorized` comes after a request, and the url in the same line names the server that answered HTTP 401. When that url is https://api.router.one/v1/responses, Router One refused the key: it answers a missing or invalid API key with 401 before any model is called. With the Router One provider block on this page, set ROUTER_ONE_API_KEY to your current key from Dashboard → API Keys in the shell or editor that starts Codex, or in ~/.codex/.env, which Codex loads when it starts and which wins over the shell, then start Codex again.

## What the two messages mean

In the Codex terminal UI both show as a red line after a ■. Per the openai/codex source (rust-v0.166.0, checked 2026-11-02), they are built like this, the first one shown with the env_key_instructions text the one-click install writes:

`Codex`

```text
■ Missing environment variable: `ROUTER_ONE_API_KEY`. Create an API key at https://router.one/dashboard/api-keys and set the ROUTER_ONE_API_KEY environment variable.
■ unexpected status 401 Unauthorized: <the server's error message>, url: <the address Codex called>, request id: <id>
```

- The first line names the variable that env_key gives, then adds the provider's env_key_instructions when the block sets one. Codex reads the variable while it prepares the request, counts a value that is empty or only whitespace as missing, and does not retry, so the request never reaches any server.
- In the second line, the text after the colon is the server's error message: error.message from a JSON error body, the body itself (up to 1,000 bytes) when it has no such field, or Unknown error when it is empty. Then come `, url: …`, the address Codex sent the request to, `, cf-ray: …` when the response carried a cf-ray header, `, request id: …` when it carried x-request-id (or else x-oai-request-id), and `, auth error: …` and `, auth error code: …` when it carried an x-openai-authorization-error header or an error code in an x-error-json header.

## Missing environment variable: set the variable Codex reads

Codex reads the key from the variable that env_key names in the [model_providers.<id>] block that the top-level model_provider line selects: ROUTER_ONE_API_KEY in the Router One configuration on this page, or whatever name your own block gives. Per the openai/codex source (rust-v0.166.0, checked 2026-11-02), Codex reads the environment it was started with, plus ~/.codex/.env (the .env file in CODEX_HOME), which it loads when it starts and whose lines win over the shell's values. A variable you set afterwards, or only in another terminal, is not there. Set the variable, then start Codex from a shell that has it:

`terminal`

```bash
# macOS / Linux, this terminal:
export ROUTER_ONE_API_KEY="sk-your-router-one-key"
# every new terminal (zsh; bash: ~/.bashrc, or ~/.bash_profile on macOS):
echo 'export ROUTER_ONE_API_KEY="sk-your-router-one-key"' >> ~/.zshrc
# check that it is exported and not blank, without printing the key:
printenv ROUTER_ONE_API_KEY | grep -q '[^[:space:]]' && echo "exported" || echo "not exported, or blank"
# Windows PowerShell, this window:
$env:ROUTER_ONE_API_KEY = "sk-your-router-one-key"
# every new window (saved for your user):
[Environment]::SetEnvironmentVariable("ROUTER_ONE_API_KEY", "sk-your-router-one-key", "User")
```

- Shell profiles are read when a terminal starts: after editing ~/.zshrc, ~/.bashrc or ~/.bash_profile, open a new terminal or source the file in this one. A variable saved with SetEnvironmentVariable reaches PowerShell windows opened afterwards, which is why the window you are in needs $env: as well.
- IDE extension: Codex's config basics page (checked 2026-11-02) says the CLI and IDE extension share the same configuration layers, so the Router One provider needs the same variable in the editor, and the extension's Codex has the environment the editor started with. After you set the variable, quit the editor completely and open it again.
- ~/.codex/.env does not depend on the shell or editor that starts Codex, because Codex reads it itself: a line `ROUTER_ONE_API_KEY=sk-your-router-one-key` there reaches the CLI and the editor's Codex alike once they restart. If the file already has a ROUTER_ONE_API_KEY line, that is the key Codex uses, so keep it current or delete it.
- A provider block with no credential setting sends no key at all. Per the openai/codex source (rust-v0.166.0, checked 2026-11-02), a custom provider that leaves requires_openai_auth off sends only what its own block configures: a key from env_key, an experimental_bearer_token (which Codex's configuration reference discourages), or a token from the block's auth or gateway_oauth settings, and never a sign-in stored with codex login. Router One answers a request without a key with 401.
- If the message names another variable, such as OPENAI_API_KEY, the block your session uses says so. For Router One, give the key a variable of its own, as the Router One block below does with ROUTER_ONE_API_KEY, so tools that read OPENAI_API_KEY do not pick up your Router One key.

## unexpected status 401: read the url first

The url in the line is where the request went, so it tells you whose 401 it is. Per the openai/codex source (rust-v0.166.0, checked 2026-11-02), Codex builds that address from the active provider's base_url plus /responses; the built-in OpenAI provider uses https://api.openai.com/v1 with an API-key sign-in and https://chatgpt.com/backend-api/codex with a ChatGPT sign-in, unless openai_base_url in ~/.codex/config.toml gives it another address, which then receives the built-in provider's credentials, ChatGPT sign-in tokens included. The built-in provider starts on WebSockets, so its Reconnecting... and fallback notices can show the same endpoint as a wss:// address:

| url in the message | What it means | What to do |
| --- | --- | --- |
| https://api.router.one/v1/responses | Router One refused the key. It answers a missing or invalid API key with 401, a revoked, expired or unknown key included, before any model is called, so the request has no row in Dashboard → Logs | Copy the key again from Dashboard → API Keys, check that it is still active and not expired, set ROUTER_ONE_API_KEY to it and start Codex again (next section) |
| https://api.openai.com/v1/responses | The session ran on Codex's built-in OpenAI provider with an API-key sign-in, so the request went to OpenAI's API, not to Router One | Put model_provider = "router" above the first [table] header in ~/.codex/config.toml, with the provider block below |
| https://chatgpt.com/backend-api/codex/responses | The session ran on the built-in provider with ChatGPT sign-in, and that sign-in was refused | Sign in again (codex logout, then codex login), or move local sessions to the Router One provider below |
| Another address | base_url in the active provider block, or openai_base_url for the built-in provider (which sends its credentials there, ChatGPT sign-in tokens included), points somewhere else, such as another service or a mistyped host | Set base_url = "https://api.router.one/v1" in the [model_providers.router] block and select it with model_provider = "router" |

## When Router One refused the key

A 401 whose url is https://api.router.one/v1/responses is about the key Codex sent, not the model. Check these, then start Codex again:

- The key is current: it still exists in Dashboard → API Keys and has not expired, and the variable holds that key, not one you have since replaced.
- The value is only the key. Quotes pasted with it, Bearer typed in front of it, or a space inside the key get it refused. Per the openai/codex source (rust-v0.166.0, checked 2026-11-02), Codex sends the value after Bearer exactly as it is, and a value with a line break cannot go into the Authorization header at all, so that request leaves without a key.
- It is a Router One key. A key from another service, such as an OpenAI API key, gets a 401 here; create one in Dashboard → API Keys.
- No old key sits in ~/.codex/.env. Per the openai/codex source (rust-v0.166.0, checked 2026-11-02), Codex loads that file when it starts, and a ROUTER_ONE_API_KEY line there wins over the value in your shell, so update or delete an old line.
- The session uses the Router One provider block. Per the openai/codex source (rust-v0.166.0, checked 2026-11-02), if ~/.codex/config.toml instead points the built-in OpenAI provider at Router One with openai_base_url, Codex sends its own OpenAI sign-in, normally the one stored with codex login, not ROUTER_ONE_API_KEY: switch to the provider block below, or sign Codex in with your current Router One key using codex login --with-api-key, never with ChatGPT.
- Programs that are already running keep the old value: an editor or terminal started before you changed the variable still sends the old key.
- A 401 from Router One has no row in Dashboard → Logs, because the gateway refuses the request before any model is called. If the key keeps being refused, write to support@router.one with the time of the error and, if the line has one, the request id, never the key itself.

## Check the key without starting Codex

Router One's API reference documents GET /v1/models with a 401 for an invalid or missing API key, so a request to it tests the key the way Codex sends it, without calling a model. Run it in the shell that starts Codex: it tests that shell's ROUTER_ONE_API_KEY, but if ~/.codex/.env sets the variable, Codex sends the value there instead. It prints only the HTTP status: 200 means the key was accepted, 401 that it was not.

`terminal`

```bash
curl -s -o /dev/null -w "%{http_code}\n" https://api.router.one/v1/models -H "Authorization: Bearer $ROUTER_ONE_API_KEY"
# Windows PowerShell:
curl.exe -s -o NUL -w "%{http_code}" https://api.router.one/v1/models -H "Authorization: Bearer $env:ROUTER_ONE_API_KEY"
```

## Set up Codex with Router One

Run the one-click command with your Router One key. It installs Codex CLI if it is missing, replaces ~/.codex/config.toml with the Router One configuration (env_key = ROUTER_ONE_API_KEY and its env_key_instructions; an existing file is kept as `config.toml.bak.<time>`, so copy any other settings you need back from it), saves the key in your shell profile (~/.zshrc for zsh, ~/.bashrc for bash, otherwise ~/.profile) and sends one test request, which reports a 401 if the key is not valid. On Windows, use the PowerShell line, which saves the key as a user environment variable instead. Then run codex in a new terminal, or in an editor started after the install. With bash on macOS, a new Terminal window reads ~/.bash_profile, so add the same export line there unless that file already loads ~/.bashrc.

`terminal`

```bash
curl -fsSL https://router.one/install/codex.sh | bash -s -- sk-your-api-key
# Windows PowerShell:
$env:ROUTER_ONE_API_KEY = "sk-your-api-key"; irm https://router.one/install/codex.ps1 | iex
```

## The Router One provider block

Keep the top-level model and model_provider = "router" lines above the first [table] header and the base_url, env_key and wire_api lines in the block as shown, all in ~/.codex/config.toml: per Codex's advanced configuration docs (checked 2026-11-02), Codex ignores model_provider and model_providers in a project-local .codex/config.toml. Leave requires_openai_auth off on this provider: it turns on OpenAI sign-in, with ChatGPT or an OpenAI API key, for the provider (Codex's authentication docs, checked 2026-11-02), and the Router One provider takes its key from env_key instead. The file the one-click install writes points at the same provider and adds env_key_instructions, the text Codex prints after the missing-variable sentence.

`config.toml`

```toml
# ~/.codex/config.toml
model = "gpt-5.6-sol"
model_provider = "router"

[model_providers.router]
name = "router"
base_url = "https://api.router.one/v1"
env_key = "ROUTER_ONE_API_KEY"
wire_api = "responses"

# in your shell:
# export ROUTER_ONE_API_KEY=sk-your-router-one-key
```

## FAQ

### Is "unexpected status 401 Unauthorized" a Router One error?

It is Codex's wording for an HTTP 401 from whichever server it called. Per the openai/codex source (rust-v0.166.0, checked 2026-11-02), Codex prints unexpected status, the HTTP status and the server's error message, then the url it called. If that url is https://api.router.one/v1/responses, Router One refused the key Codex sent. With the Router One provider block that is the key in ROUTER_ONE_API_KEY: set it to a current key from Dashboard → API Keys and start Codex again. If openai_base_url points the built-in provider at Router One instead, it is Codex's own OpenAI sign-in, ChatGPT sign-in tokens included, not ROUTER_ONE_API_KEY (see When Router One refused the key). Any other url means the request did not go to Router One.

### Does Codex retry a 401?

Yes, but it sends the same key each time. Per the openai/codex source (rust-v0.166.0, checked 2026-11-02), the HTTP layer never retries a 401, and Codex's 401 handling can reload and refresh a stored ChatGPT sign-in but never changes a key read through env_key. The sampling loop then retries the request like other unexpected statuses: on an HTTPS provider such as the Router One provider, up to stream_max_retries times (5 by default) with growing waits, so Reconnecting... lines can appear before the message. The built-in OpenAI provider starts on WebSockets and, once those retries run out, falls back to HTTPS and can retry that many times again. More retries cannot fix the key: correct the variable and start Codex again.

### The 401 line says invalid token, Incorrect API key provided or detail Unauthorized. What does that mean?

It is the server's own error message, not Codex's. Per the openai/codex source (rust-v0.166.0, checked 2026-11-02), Codex prints error.message from a JSON error body, the body itself (up to 1,000 bytes) when there is no such field, or Unknown error when the body is empty, so the wording depends on the server in the url. OpenAI's error-codes guide (checked 2026-11-02), for example, lists 401 errors named Invalid Authentication and Incorrect API key provided, and Router One's API reference shows invalid api key as the message of its example 401 body. A body such as {"detail":"Unauthorized"} shows up as it is when a server answers in that shape. Whatever the wording, read the url first.

### The message names OPENAI_API_KEY, or a variable I never set. Why?

Codex names the variable that env_key gives in the provider block your session uses. Per the openai/codex source (rust-v0.166.0, checked 2026-11-02), the built-in OpenAI provider has no env_key, so a missing-variable message always comes from a custom provider block: one you wrote, one a setup script wrote, or one in a Codex profile file. Check the top-level model_provider line in ~/.codex/config.toml and the [model_providers.<id>] block it names. For Router One, point env_key at ROUTER_ONE_API_KEY, as in the block on this page, and set that variable, so tools that read OPENAI_API_KEY do not pick up your Router One key.

### I set the variable, and the VS Code extension still says Missing environment variable. Why?

The extension's Codex still has the environment the editor started with. Codex's config basics page (checked 2026-11-02) says the CLI and the IDE extension share the same configuration layers, so the Router One provider needs ROUTER_ONE_API_KEY in the editor too. Quit the editor completely, every window, and open it again. If the editor still does not get the variable, put the key in ~/.codex/.env as a line `ROUTER_ONE_API_KEY=sk-your-router-one-key`: per the openai/codex source (rust-v0.166.0, checked 2026-11-02), Codex loads that file itself when it starts, whatever started it, so restart the editor once more after adding it.

### codex login --with-api-key said API key saved. Why do I still get 401?

Per the openai/codex source (rust-v0.166.0, checked 2026-11-02), the command stores the key without checking it (it prints API key saved. Authentication is checked when you send a request.), and the stored key belongs to the built-in OpenAI provider: a custom provider that leaves requires_openai_auth off never sends it. If your session uses the Router One provider, the key that counts is the one in ROUTER_ONE_API_KEY. If it uses the built-in provider, the url in the 401 line shows where the stored key went; when openai_base_url points it at Router One, sign in again with your current Router One key using codex login --with-api-key, never with ChatGPT. codex login status reports only Codex's own OpenAI sign-in and never reads ROUTER_ONE_API_KEY, so it can print Not logged in while the Router One provider works; codex logout clears a stored sign-in.

### Does this setup work from Mainland China?

Yes. The gateway is reachable from Mainland China without a VPN, and the configuration is the same as anywhere else: the same config.toml and the same ROUTER_ONE_API_KEY variable. A 401 reads the same way too: check the url, then the key.

## See also

- Codex exceeded retry limit (429): https://router.one/codex-exceeded-retry-limit
- Codex model at capacity: https://router.one/codex-model-at-capacity
- Codex model not supported fix: https://router.one/codex-model-not-supported
- Codex usage limit: https://router.one/codex-usage-limit
- Codex Responses API: https://router.one/codex-responses-api
- Codex CLI in China: https://router.one/codex-china
- Error codes reference: https://router.one/llm-api-error-codes
- Claude Code 401 & 403 guide: https://router.one/claude-code-403
- CLI setup guide: https://router.one/docs/guides/cli-setup
- Troubleshooting FAQ: https://router.one/docs/guides/faq
- GET /v1/models reference: https://router.one/docs/models/listModels
- Codex authentication docs: https://learn.chatgpt.com/docs/auth
- Canonical page: https://router.one/codex-401-unauthorized
- Models and per-model token rates: https://router.one/models (markdown: https://router.one/models.md)
- Pricing: https://router.one/pricing
- API docs (markdown): https://router.one/docs.md
- Company facts: https://router.one/facts/company.md
