ErrLookup › Background articles › "API key is required" / "API key not found" / "No API key was set": the missing-api-key error family across 16 libraries
"API key is required" / "API key not found" / "No API key was set": the missing-api-key error family across 16 libraries
Missing API key errors — "API key is required", "API key not found", "No API key was set", "<PROVIDER>_API_KEY is not set" — come from a library's own credential resolution code before any request leaves the machine. This article maps the family across 16 open-source repositories: the argument, config, auth-store, and environment chains libraries walk to find a key, why a key that satisfies one subsystem still fails another, and where the key must actually live for Docker, systemd, hooks, serverless, and .env-based setups.
Distilled from 105 documented records across 16 repositories.
Background
Every error in this family is raised by client-side code inside the library, not by the remote API. The check sits in a constructor (AnythingLLM's provider classes, Chroma's embedding functions), a validate_environment hook (LiteLLM's Topaz and Gemini image handlers), a signing utility (easywechat's createV2Signature), or a guardrail initializer (LiteLLM's Cato Networks check), and it fires only after a credential resolution chain comes up empty. The chain has a consistent shape across the family: explicit argument first, then library configuration, then a durable store or persisted config, then one or more environment variables. CodeWhale's provider routes walk override, config table, auth store, environment variable, and external consent in order; LiteLLM's Evals handler checks litellm_params.api_key, then litellm.api_key and litellm.openai_key, then OPENAI_API_KEY. Because these checks run before any network I/O, there is no status code and no server log to inspect; the error text itself, naming an env var or a config table, is the diagnostic.
From the caller's side, the surface varies by ecosystem more than the cause does. The Python members mostly raise ValueError (Chroma, LiteLLM's Voyage, Topaz, and WatsonX handlers), easywechat raises InvalidConfigException before any crypto runs, claude-mem fails with a typed error kind (missing_api_key) that callers can catch to degrade to local-only operation, and chatwoot raises a skip exception (CurationSkipped) that callers are told to treat as expected control flow rather than a crash. CodeWhale bails with a formatted remediation message that interpolates the console URL, the auth-set command, the env var label, and the config table name. Timing varies as well: some members validate eagerly at construction or startup, so selecting the provider fails immediately, while others check lazily at first request or when building headers, so the failure surfaces mid-operation.
Where the key is expected to live is the biggest axis of variation. Environment variables dominate, but each library picks its own names and precedence: LiteLLM's WatsonX token exchange accepts WX_API_KEY, WATSONX_API_KEY, WATSONX_APIKEY, or WATSONX_ZENAPIKEY; its Gemini chat and interactions paths try GOOGLE_API_KEY before GEMINI_API_KEY; its Gemini image_edit path consults GEMINI_API_KEY only. Other members read config files (~/.codewhale/config.toml provider tables, chatwoot's InstallationConfig rows, GitButler's git config), UI-stored settings (AnythingLLM's Community Hub connection key, OpenHuman's Connections panel), or per-request fields (LiteLLM's litellm_params_template on Gemini managed-agent routes, where the proxy's env fallback is restricted to admins by design). Persisted configurations add their own wrinkle: Chroma embedding functions store only the env var name and never persist the api_key argument, so rehydrating a saved config on a machine without the env var fails by design.
Two cross-cutting traps explain most real-world hits. First, the process that needs the key is rarely the shell where it was exported: hooks run from the host application's launch environment, systemd and PM2 services do not inherit the interactive shell, Docker needs explicit pass-through, and .env loaders have quirks — one parser in the family reads only the script directory, fills undefined variables only, and rejects lines with spaces around the equals sign. Second, keys are scoped per subsystem: a working WeChat Pay v3 setup still trips the separate v2 secret key check, multimodal embedding has its own key check even when text embeddings work, the Gemini embedding variable differs from the Gemini chat variable, and Kimi Code membership-plan routes never import Kimi CLI credentials or accept a generic Moonshot platform key. Where records disagree, such as whether GOOGLE_API_KEY satisfies a given Gemini path, the behavior is path- and library-specific, and the error message is the authority.
Common causes
- Env var unset in the runtime that actually runs the code. The key is exported in an interactive shell but absent where the process really executes: Docker containers without pass-through, systemd/PM2 units, CI jobs, serverless functions, and hook processes that inherit a different environment. Chroma, anything-llm, litellm, and claude-mem all document this shape.
- Wrong or mismatched variable name. Typos (CHROMA_APIKEY), near-misses (GEMINI_API_KEY where GEMINI_EMBEDDING_API_KEY is required), a custom api_key_env_var that does not match the exported name, or a gateway key mistaken for a provider credential (CAVE_API_KEY does not count as one in caveman-code).
- Config file or settings row never set. The key belongs in a config artifact rather than the environment: a missing [providers.x] api_key table in ~/.codewhale/config.toml, an unset CAPTAIN_FIRECRAWL_API_KEY InstallationConfig in chatwoot, no GitButler AI provider in git config, or an unsaved AnythingLLM Hub connection key.
- Subsystem needs its own separate key. Credentials are scoped: the WeChat Pay v2 secret key is separate from v3, embedding keys from chat keys, multimodal embedding checks from text ones, SiliconFlow-CN keys from global ones, and Kimi Code plan keys from Moonshot platform keys and Kimi CLI logins. A fully working setup in one subsystem proves nothing about another.
- This code path reads a different variable than sibling paths. Within one library, key resolution is per path: LiteLLM's WatsonX IAM exchange accepts four env spellings, its Gemini chat/interactions paths try GOOGLE_API_KEY then GEMINI_API_KEY, and its Gemini image_edit path ignores GOOGLE_API_KEY entirely. The variable that worked for chat can be invisible to image or evals calls.
- .env loading quirks. The file is never loaded before the client or embedding function is constructed, the loader reads only the script directory, it populates undefined variables only, or the parser rejects 'KEY = value' spacing and requires bare KEY=value lines.
- Persisted config rehydrated without the secret. Some persisted configs deliberately exclude secrets: Chroma embedding functions store only the env var name and treat the api_key constructor argument as ephemeral, so build_from_config on a machine without the env var raises the missing-key error by design.
- Request simply carries no key. No resolution chain is involved: a proxy with master_key configured receives a call with no Authorization header at all, a wx_credentials dict is passed under a wrong inner key name ('key' instead of 'apikey'), or a non-admin calls a route where the env fallback is restricted to admins.
What usually fixes it
- Trace the failing path's resolution chain — explicit argument, library config, auth store, environment variable — and supply the key at the earliest link you control; the error message names the exact source that path checks.
- Verify the variable inside the runtime that fails, not the shell you configured: docker exec printenv, the service unit's Environment block, the hook's launch environment, or a startup assertion such as bool(os.environ.get('...')) that checks presence without printing the value.
- Copy credential names exactly from the error, and do not assume one provider has one variable: paths inside a single library can read different vars (GEMINI_API_KEY vs GOOGLE_API_KEY), and a custom api_key_env_var must match the exported name character-for-character.
- Prefer durable key sources over ambient ones: provider auth stores ('codewhale auth set'), secret managers that inject env at boot, or config tables — paired with a fail-fast startup check so gaps surface before traffic instead of mid-request.
- Wire graceful degradation where the feature is optional: catch fallback-eligible error kinds, treat skip exceptions as expected control flow instead of job failures, or invoke the command without the AI-dependent flag.
- Keep sibling keys together and smoke-test each one: v2 next to v3 pay keys, embedding next to chat keys, plan next to platform keys, with a canary call in deployment tests for every credential a subsystem needs.
Go deeper
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Documented occurrences
- Missing v2 secret key. (w7corp/easywechat)
- API key not provided and {self.api_key_env_var} environment variable is not set. (chroma-core/chroma)
- missing_api_key: Server API key is not configured (CLAUDE_MEM_SERVER_API_KEY). (thedotmack/claude-mem)
- Gemini managed-agent endpoints require a caller-supplied Gemini api_key (via 'litellm_params_template'). Falling back to the proxy's GOOGLE_API_KEY / GEMINI_API_KEY env vars is only permitted for proxy admins. (BerriAI/litellm)
- OPENROUTER_API_KEY not found. Copy .env.example to .env and add your API key. Free key: https://openrouter.ai (santifer/career-ops)
- Firecrawl not configured (chatwoot/chatwoot)
- No usable credentials for '{slug}', which OpenHuman selected for the {} workload. Your chat model is local ('{}') and does not serve this workload, so it fell back to your cloud provider — but '{slug}' has no API key configured. Add a key for '{slug}' in Connections → LLM, set {}_provider to a provider that is configured, or enable the managed OpenHuman backend. (tinyhumansai/openhuman)
- Error: Watsonx API key not set. Set WATSONX_API_KEY in environment variables or pass in as parameter - 'api_key='. (BerriAI/litellm)
- {} API key not found.{} Run 'codewhale auth set --provider {}', set {}, or add [{}] api_key in ~/.codewhale/config.toml. (Hmbown/CodeWhale)
- No AI credentials found. Configure in GitButler settings or set OPENAI_API_KEY environment variable. (gitbutlerapp/gitbutler)
- Kimi Code membership-plan API key not found. Get a plan key: {}. This route uses api.kimi.com/coding/v1 and does not import Kimi CLI credentials. Run 'codewhale auth set --provider moonshot', set {}, or add [{}] api_key. (Hmbown/CodeWhale)
- DeepSeek API key not found. 1. Get a key: https://platform.deepseek.com/api_keys 2. Save it (works in every folder, no OS prompts): codewhale auth set --provider deepseek Alternatives: • export DEEPSEEK_API_KEY=<your-key> (current shell only; also note: zsh users — exports in ~/.zshrc only reach interactive shells, prefer ~/.zshenv for everything) • api_key = "<your-key>" in ~/.codewhale/config.toml • already configured DeepSeek Harness? grant read-only access: codewhale auth external-consent --provider deepseek --mode read-only (Hmbown/CodeWhale)
- API key not found in environment variable {api_key_env_var} or in any existing client instances (chroma-core/chroma)
- API key is required (BerriAI/litellm)
- API key is required for Topaz image variations. Set via `TOPAZ_API_KEY` or `api_key=..` (BerriAI/litellm)
- No Gemini API key was set. (Mintplex-Labs/anything-llm)
- Voyage API key is required for multimodal embeddings. Set VOYAGE_API_KEY / VOYAGE_AI_API_KEY / VOYAGE_AI_TOKEN or pass `api_key` explicitly. (BerriAI/litellm)
- Google API key is required (BerriAI/litellm)
- Ollama Cloud API key not found. Get a key: {}. Run 'codewhale auth set --provider ollama', set OLLAMA_API_KEY, or add [providers.ollama] api_key in ~/.codewhale/config.toml. (Hmbown/CodeWhale)
- No usable credentials for '{slug}', which OpenHuman selected for the {} workload. Add a key for '{slug}' in Connections → LLM, set {}_provider to a provider that is configured, or enable the managed OpenHuman backend. (tinyhumansai/openhuman)
…and 85 more across the corpus — use search.
Honest provenance: generated on 2026-08-22 from AI-assisted analysis of the linked records. See how records are made.