A Codex API key is more than a string. It determines whose account is billed, which models can be called, and how much damage a leak can do. Most 401 errors are not caused by an invalid key; they come from a mismatched variable name, a rewritten request header, the wrong configuration layer, or a stale credential cached locally.
This guide follows a practical order: key types, creation, environment variables and configuration files, profiles, rotation, least privilege, CI, and 401 troubleshooting. Commands and configuration keys follow the current Codex version and the documentation of the gateway you use.
Note: OpenAI, Codex, and ChatGPT are trademarks or registered trademarks of OpenAI. This article is a technical guide only and is not affiliated with OpenAI. Key permissions, endpoint scope, and billing rules follow each platform's official documentation and site pages.
1. Separate the three key types and the two storage layers
Codex-related credentials are easy to confuse. Start by separating them by purpose:
| Name | Typical use | Storage | Notes |
|---|---|---|---|
| OpenAI Platform API key | codex login --with-api-key |
auth.json or the system keychain |
Billed at standard API rates |
CODEX_API_KEY |
One-off runs such as codex exec and SDK calls |
Only the current process environment | Never written to disk; suitable for CI |
| Custom provider key | Referenced by env_key |
Shell environment or a secrets manager | Common for team gateways and easier to cap |
The first storage layer is "login credentials," controlled by cli_auth_credentials_store: file writes auth.json under CODEX_HOME, keyring uses the system keychain, auto tries the keychain first and falls back to a file, and ephemeral keeps credentials only in the current process memory. The second layer is the "provider key," where env_key names an environment variable that Codex reads on every request. Remember: env_key is only the variable name, not the variable itself.
CODEX_HOME defaults to ~/.codex and can be moved to a controlled directory. OPENAI_API_KEY is the source variable read by the login command; CODEX_API_KEY is a one-off credential for non-interactive runs. The names look similar, but their purposes differ.
2. Create a key in five steps
- Open the ZSQH AI Gateway or the platform you use, register, and sign in to the console.
- Go to the API keys page and create a key. Name it after the environment and purpose, for example
codex-work-mac. - Set a quota cap and an expiration date. Give test keys a low quota first and adjust after you see real consumption.
- If the platform supports model allowlists or project scopes, select only the models Codex needs.
- Copy the full key and store it immediately in a password manager or secrets manager. Most consoles show it only once.
Keys often start with sk-, but the prefix proves neither authenticity nor permissions. Watch for leading and trailing spaces, line breaks, and extra quotation marks when copying; these are frequent causes of 401 errors. After creating a key, validate it with a minimal request before distributing it to team members.
3. How environment variables and config.toml work together
The recommended pattern is simple: the configuration file contains only the variable name, while the real key lives in an environment variable or a secrets tool. Here is a ~/.codex/config.toml example:
# ~/.codex/config.toml
model = "your-model-id"
model_provider = "zsqh"
[model_providers.zsqh]
name = "ZSQH AI Gateway"
base_url = "https://ai.zhongshangqihang.com/v1"
wire_api = "responses"
env_key = "ZSQH_API_KEY"
env_key_instructions = "Create a key in the gateway console and export it as an environment variable"
Then export it in the same shell that launches Codex:
export ZSQH_API_KEY="sk-your-key"
On macOS and Linux, you can put that line in a private file such as ~/.zshrc.local, while the main configuration only references the variable. On Windows PowerShell, use $env:ZSQH_API_KEY="sk-your-key". Processes launched from the Dock, an IDE, or a desktop app may not read variables from your shell profile; in that case, inject them through a launch script or system environment variables.
If the gateway requires custom headers, use env_http_headers = { "X-API-Key" = "ZSQH_API_KEY" } instead of env_key, and configure a local model catalog. If keys are issued dynamically by tools such as Vault or KMS, use the command field under [model_providers.zsqh.auth] to fetch a token, but do not combine it with env_key. The experimental_bearer_token field can hold a key directly, but the official documentation does not recommend it, and team environments should avoid it.
Individuals can also use the login flow: printenv OPENAI_API_KEY | codex login --with-api-key. Credentials then go into auth.json or the keychain and can be cleared with codex logout. For team gateways, env_key is preferable because quotas, model permissions, and revocation can all be managed centrally on the gateway side.
4. Multiple configurations and profiles
Keep at least three credential sets: daily personal use, project-specific use, and automation. Their quotas, permissions, and rotation cycles differ; mixing them makes both usage and failures hard to attribute.
Recent Codex versions split profiles into separate files. Define two providers in the main configuration, each reading a different environment variable:
# ~/.codex/config.toml
model = "your-model-id"
model_provider = "zsqh_work"
[model_providers.zsqh_work]
base_url = "https://ai.zhongshangqihang.com/v1"
wire_api = "responses"
env_key = "ZSQH_WORK_KEY"
[model_providers.zsqh_test]
base_url = "https://ai.zhongshangqihang.com/v1"
wire_api = "responses"
env_key = "ZSQH_TEST_KEY"
Then create ~/.codex/test.config.toml containing only the top-level keys you want to override:
model = "your-test-model-id"
model_provider = "zsqh_test"
Run codex --profile test to switch. Note that starting with version 0.134.0, the old [profiles.xxx] tables and the top-level profile = "..." setting are no longer read, and profile files should not nest [profiles.xxx] either; use top-level keys directly. Older versions may still accept the old syntax, so check the release notes before upgrading.
A .codex/config.toml inside a trusted project is only suitable for non-sensitive conventions. Its model_provider, model_providers, and profile definitions are ignored, and you should never commit a key there.
5. Key rotation: when and how
The purpose of rotation is not to follow a ritual but to shorten the window in which a leaked key is useful. Employee departures, lost devices, suspected leaks, vendor requirements, and fixed intervals (for example, every 90 days) are all good triggers.
- Create a new key in the console and include a date in the name, for example
codex-prod-2026-10. - Write the new key into your secrets tool or environment first, and keep the old key for now.
- Validate once with
codex execand once with a minimal request, confirming the model, quota, and permissions. - Watch the old key's usage logs to confirm that no forgotten script or team member still uses it.
- Disable the old key, then monitor error rates and failed requests for one to two days.
- Record the rotation date, owner, and scope so handovers and audits are easier.
Do not rotate during peak production hours. Running two keys in parallel for a while is safer than deleting and recreating. If you rely only on login credentials, run codex logout and sign in again after rotation to avoid stale credentials lingering in auth.json. Treat auth.json with password-level protection and never sync it to public cloud storage or a code repository.
6. Least privilege and leak prevention
- One key per person, split further by environment; use a separate key with its own quota for CI.
- Set expiration dates, budgets, and concurrency caps on keys, and grant only the model permissions that are needed.
- If the platform supports IP allowlists, restrict access to the office network or runner egress; if not, rely on quotas and alerts as a backstop.
- In enterprise environments,
forced_login_method = "api"or"chatgpt"can enforce a single login method and prevent members from mixing credentials. - Prefer
cli_auth_credentials_store = "keyring"on personal machines; useephemeralon shared machines or in CI to reduce on-disk exposure. - Make
.gitignorecover.env,.env.*,auth.json,*.pem, and similar files; run secret scanning or a pre-commit hook forsk-patterns before committing. - Never hard-code a default key, do not debug with
echo $ZSQH_API_KEY, and mask theAuthorizationheader in logs.
If a leak is confirmed, the order is: disable or delete the key first, create a new one, then inspect usage logs and repository history. Deleting a file is not enough, because the old key may already have been copied elsewhere.
7. CI and non-interactive use
The principle for CI is that keys must not enter code, images, or logs.
- For one-off runs, use
CODEX_API_KEY=<key> codex exec --json "...", injecting the variable only on that command rather than setting it as a job-level environment variable. - In GitHub Actions, prefer the official Codex Action over putting the key directly into a shell step.
- Use a dedicated CI key with a low quota and narrow permissions, available only on protected branches or trusted workflows.
- Use
--ephemeralto avoid writing session files to disk; add--skip-git-repo-checkwhen running outside a Git repository. - Do not use ChatGPT-managed credentials on public repositories or untrusted runners.
8. 401 troubleshooting: check in this order
A 401 means authentication failed; it does not necessarily mean the key is wrong. Work through these steps:
- Run
codex login statusand confirm the authentication method actually in effect. - Confirm that the variable name referenced by
env_keyis spelled consistently and is non-empty in the current process. Check only the length, not the full value. - Confirm which environment launched Codex. IDE terminals, Docker, systemd, and launchd may not inherit shell variables.
- Copy the key from the console again to rule out leading or trailing spaces, line breaks, and quotes.
- If you use login credentials, run
codex logoutand sign in again to rule out stale data inauth.json. - Check whether
forced_login_methodconflicts with the current credential type; a mismatch can make Codex exit or reject requests. - Check whether the key has expired, been disabled, run out of balance, or lacks permission for that model.
- Confirm that the Base URL and
/v1path are correct and that no proxy is stripping or rewriting theAuthorizationheader; use gateway logs to verify the request if necessary.
Common error reference:
| Symptom | Common cause | Fix |
|---|---|---|
| 401 authentication failure | Variable not exported, name mismatch, expired credential | Check env_key; reopen the terminal; sign in again |
| 403 Forbidden | No model permission, IP restriction, frozen quota | Check console permissions and allowlists |
| 404 Not Found | Base URL has an extra or missing /v1, wrong path |
Check the address and endpoint scope segment by segment |
| 429 Too Many Requests | Concurrency or quota exceeded | Reduce concurrency; check key limits |
| model not found | Wrong model identifier | Copy the exact identifier from the pricing page |
FAQ
Is a Codex API key the same thing as a ChatGPT subscription?
No. A ChatGPT subscription covers the web and client products; Codex can sign in with an account or use an API key with pay-as-you-go billing. Their quotas, invoices, and permission scopes differ, so teams should account for them separately.
How does env_key relate to OPENAI_API_KEY?
env_key is a configuration field whose value is an environment variable name. OPENAI_API_KEY is one common variable name and the source read by codex login --with-api-key. You can set env_key = "OPENAI_API_KEY", but it is better to use a dedicated name for the gateway so it does not mix with login credentials.
Can I write the key directly into config.toml?
Not recommended. A plaintext key can leak through configuration backups, screenshots, or repositories. Use env_key to reference an environment variable, or call an external token command through [model_providers.xxx.auth].
Does rotating a key require downtime?
No. Create and validate the new key first, then switch the configuration, and finally disable the old key. Running both keys in parallel for a period avoids interrupted requests.
Why do requests still return 401 when codex login status looks fine?
A healthy login status only means local credentials exist; it does not mean the custom provider's key works. Check the env_key variable, Base URL, gateway balance, and model permissions. If requests pass through a proxy, also check whether headers are being rewritten.
Next steps
If you do not have a key yet, create a low-quota test key from the sign-up page and configure and validate it in the order described here. Models, quotas, and billing are covered on the model pricing page; prices change with model versions and upstream policies, so always use the live information on that page.
Related reading:
