Invalid API key in opencode: why the key is rejected and how to check
The Invalid API key error in opencode almost always comes down to four causes. We test the key outside opencode and find where the chain breaks.
5 min · · errors · IDE
You connected the gateway, started opencode, sent the first request and got "Invalid API key" or just "doesn't work". The key was copied from the dashboard and looks right. This article explains why invalid api key in opencode shows up with a working key and walks through a step-by-step check: from a request without opencode to the config and environment variables.
The addresses and key format below are for Zukko: the key starts with fetch_ and the opencode address is https://api.zukko.pro/v1. If you use another service the logic of the check is the same; substitute your own values.
Step 1. Test the key without opencode
Before editing the config, find out whether the key is at fault. Make a direct request:
curl -i https://api.zukko.pro/v1/models \
-H "Authorization: Bearer fetch_your_key"Read the first line of the response. Below are the gateway codes you meet both in this check and later in normal work (403 and the model-name 404 show up on requests to a specific model):
| Response | Meaning |
|---|---|
200 and a list of models | the key works, the problem is in opencode |
401 | the key is wrong: typo, truncated, revoked, or simply a different key |
402 | the key is fine, but the balance is too low |
403 | the key is fine, but the model is not allowed for it (model_not_allowed_for_key) |
404 | wrong address or model name |
429 | the key's daily or monthly limit is used up |
On 200 go to step 2. On 401, create a new key on the keys page and retry: the full key is shown only once, at creation, so losing its tail while copying is easier than it sounds.
Step 2. Make sure the key went to the right provider
In opencode a key is tied to a provider. You enter it with the /connect command in the interface and it is saved in ~/.local/share/opencode/auth.json (this is described in the opencode documentation). A typical mistake: a fetch_... key is pasted into a provider that looks at another server, for example "OpenCode Zen" or OpenAI, which have their own keys. For them your fetch_... really is invalid.
Check:
opencode auth listThe command shows which providers have stored credentials. Then open the model list (/models) and see which provider the selected model belongs to. If the request goes to a provider whose baseURL is not overridden to point at the gateway, it will travel to the official server carrying your gateway key, and you will be refused.
If you set the key both in /connect and in opencode.json (the apiKey field), there are two sources. Which one wins in your version is worth checking in the opencode documentation, and it is safer to keep a single source.
Step 3. Check baseURL: it needs /v1
For opencode through the anthropic provider the address is given with /v1. This differs from Claude Code and the official Anthropic SDK, where /v1 is not needed. Without /v1, requests go to /messages and return 404. That 404 is easy to mistake for a key problem, especially when the interface shows a generic error text.
A working config, either global ~/.config/opencode/opencode.json or opencode.json in the project root:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"anthropic": {
"options": {
"baseURL": "https://api.zukko.pro/v1",
"apiKey": "{env:ZUKKO_API_KEY}"
},
"models": {
"claude-sonnet-5-5": {},
"claude-opus-5-5": {}
}
}
}
}The {env:NAME} construct substitutes an environment variable and is described in the opencode config documentation. If the model you need is missing from /models, make sure its id in the models block matches GET /v1/models exactly. How opencode builds the model catalog for a provider is something to confirm in its documentation.
Step 4. The environment variable is not picked up
If you use {env:ZUKKO_API_KEY} and the variable is empty, opencode gets an empty key. This is the most common reason behind "but the key is right". Check:
echo "${ZUKKO_API_KEY:0:8}"It should print fetch_... (the first eight characters). Empty output means the variable is not in the current environment. Typical causes:
- the variable is set in
~/.zshrcbut the terminal was opened before the edit: runsource ~/.zshrcor open a new one; - you use a different shell (bash instead of zsh) and edited the wrong file;
- opencode is launched from an app or an editor that does not read shell files: set the variable where the process starts;
- quotes, a space or a line break came along when pasting from a messenger.
To test, temporarily put the key straight into apiKey in the config, start opencode and see whether the error is gone. If so, the environment is the culprit. Then restore the substitution and keep the key out of files that end up in git.
Step 5. Alternative: a provider over the OpenAI-compatible API
If you need more than Claude, say gpt-6.1-sol or deepseek-v4.1-flash, you can define a separate provider. According to the opencode documentation, OpenAI-compatible servers use the @ai-sdk/openai-compatible package:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"zukko": {
"npm": "@ai-sdk/openai-compatible",
"name": "Zukko",
"options": {
"baseURL": "https://api.zukko.pro/v1",
"apiKey": "{env:ZUKKO_API_KEY}"
},
"models": {
"gpt-6.1-sol": { "name": "GPT-6.1 Sol" },
"deepseek-v4.1-flash": { "name": "DeepSeek V4.1 Flash" }
}
}
}
}The provider id (zukko) must match what you entered in /connect (the "Other" entry) if you use that route. This is stated in the Troubleshooting section of the opencode providers page.
When the error is not about the key
Once curl returns 200 and the config is checked, look at the neighboring causes:
- 402: the balance ran out. Top it up in the dashboard; the status is visible under "Usage".
- 403
model_not_allowed_for_key: the key has a list of allowed models and you picked one outside it. - 429
key_daily_limit_reachedorkey_monthly_limit_reached: the daily or monthly limit of this particular key was hit. - 503
model_disabled: the model is temporarily switched off, pick another fromGET /v1/models.
For the general picture of connection errors see Error 401 invalid API key and Connecting opencode, and for the detailed setup see the opencode documentation.
FAQ
The key works in curl but not in opencode. Where do I start?
Start with opencode auth list and the provider of the selected model. Then make sure baseURL contains /v1 and that the environment variable your config refers to is not empty.
Why do I see a key error instead of a 404 when /v1 is missing?
The client interface may show a generic error text regardless of the response code. So test the key separately with curl -i first: the status code shows where the problem is.
Can I store the key right in opencode.json?
Technically yes, but the file often lives in the project repository. An environment variable or a separate file outside the repository is safer. If a key did end up in git, revoke it in the dashboard and create a new one.
Do I need a separate key for opencode?
It is recommended. A dedicated key with a daily dollar limit and a list of allowed models limits the damage if an agent loops or the key leaks. You can create one in the dashboard in a minute.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- JetBrains AI Assistant not working: causes and workaroundsAI Assistant will not activate, stays silent or is off for some files. We go through the causes in the JetBrains docs and connect your own models via a gateway.
- Codex error 403: causes and how to keep workingA 403 in Codex comes in three kinds: a region refusal, an account problem and gateway key restrictions. How to tell them apart by the error text and what to fix.
- Cursor not working: common problems and how to fix themCursor froze, shows Reconnecting or does not answer. We go from simple causes to complex ones and cover problems with your own key and a gateway separately.
- Cursor AI limits: how they work and what to do when you run outA Cursor limit is consumption of an included amount of usage, not a message counter. What eats it and four ways to keep working when it runs out.