cocodot
← Back to guides
Local card declined for overseas AI? cocodot: one card + one key
TroubleshootingUpdated 2026-10

401 Unauthorized from an AI API: Incorrect API Key, invalid_api_key or invalid x-api-key — What Actually Broke

A 401 is decided before the model, your balance or rate limits are even considered. One curl call tells you whether the key is bad or simply never arrived intact; the table covers the rest.

TL;DR: A 401 means the server could not work out who you are, so it refused the request at the very first checkpoint. Authentication runs before model selection, billing and rate limiting, which is why a 401 almost never has anything to do with your prompt, your model name or your balance. There are only two families of cause. Either the key itself is wrong — truncated when copied, carrying an invisible space, newline or quote, still a placeholder, or revoked or expired — or the key never reached the server intact: it was sent to a different provider's base_url, placed in the wrong header, or your process loaded an old or empty value. A fast way to tell which is to send one minimal curl request outside your application, using the same key and the same base_url. If curl also returns 401, look at the key and at whether the key and the endpoint belong to the same provider. If curl succeeds while your app fails, the problem lives in your code, your environment variables or a client's saved settings. The table below maps each symptom to its cause and fix; many cases can be pinned down in a couple of minutes.

1. Where a 401 sits in the request pipeline

Think of an API request as passing a series of gates: identity first (who are you), then permission (may you use this resource), then billing and rate limits, and only then does a model do any work. A 401 stops at the first gate. That single fact rules out a lot of wasted effort: switching models, trimming the prompt or topping up will not make it go away. Anthropic's official error reference spells the distinctions out: 401 is `authentication_error`, meaning there is a problem with the API key — for example it is malformed, revoked or expired; 403 `permission_error` means the key lacks permission for the resource; 402 `billing_error` points at billing or payment details; 429 `rate_limit_error` means a rate limit was hit. Each code tells you which gate stopped the request, so read the status and the error type before you touch anything. For OpenAI's wording and categories, go by its official error-codes page as it reads today (platform.openai.com/docs/guides/error-codes).

2. The first cut: bad key, or key never delivered?

The reflex response to a 401 is to generate another key. When the new key fails too, people start blaming the provider. A faster move is to cut the problem in half: from a terminal, outside your application, send the smallest possible request with the same key to the same base_url. If curl also returns 401, the key is bad or it belongs to a different provider than the endpoint. If curl succeeds and your app still returns 401, the key is fine and the fault is in how your app loads it, in an environment variable, or in settings a client has saved. Make sure the test call genuinely requires authentication: on some endpoints the model list is public and answers without any key, so getting a list back proves nothing about the key.

Test the key outside your app — one OpenAI-style call and one Anthropic-style call (swap in your own key and address)
# OpenAI-compatible: key goes in Authorization: Bearer
curl -sS https://cocodot.co/api/ai/v1/chat/completions \
  -H "Authorization: Bearer $MY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"claude-sonnet-5","messages":[{"role":"user","content":"ping"}],"max_tokens":16}'

# Anthropic-style: x-api-key plus anthropic-version, as in Anthropic's docs
curl -sS https://cocodot.co/api/ai/v1/messages \
  -H "x-api-key: $MY_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{"model":"claude-sonnet-5","max_tokens":16,"messages":[{"role":"user","content":"ping"}]}'

3. "The key is definitely correct" — usually it is not the same string

When someone insists the key is right, the usual truth is that the string they are looking at is not the string their code sends. Three causes come up again and again. The copy lost a few characters at the start or end — and because many consoles show a key only once, a bad copy means creating a new key. An invisible character slipped in: a trailing newline, a space, or quotes picked up when the key travelled through a chat app, a document or a config file. Or the placeholder from sample code, such as `sk-YOUR_KEY`, was never replaced, or was replaced in a different file from the one that actually runs. The check is to log a fingerprint, not the key: its length and boolean checks for leading or trailing whitespace. A length that does not match the console, or whitespace at either end, is your answer. Never paste a full key into logs, tickets or screenshots; if a key has been exposed anywhere public, revoke it rather than hoping nobody noticed.

Log a key fingerprint without leaking the key
import os
k = os.environ.get("MY_KEY", "")
print("len =", len(k))
print("outer_whitespace =", k != k.strip())
print("contains_newline =", "\n" in k or "\r" in k)

4. The sneakiest 401: key and base_url from different providers

A key is only valid at the provider that issued it. Send provider A's key to provider B's address and B's authentication system has simply never seen that string, so the only possible answer is 401. It is confusing precisely because the key is healthy and still works where it came from. The common versions: you use an official SDK against a third-party endpoint but the base_url setting never took effect, and most SDKs fall back to the vendor's default address when none is given, so the third-party key goes to the vendor; the reverse, where a vendor key is pasted into a gateway that only accepts its own keys; or a tool where you swapped the key but not the address, or the address but not the key. The check takes seconds: log the URL your code actually calls and confirm it belongs to the same provider as the key. On cocodot, for example, OpenAI SDKs and clients use `https://cocodot.co/api/ai/v1`, Anthropic SDKs use `https://cocodot.co/api/ai` with no trailing /v1, and keys are created in the console and start with cocodot-. Mixing up those two addresses typically gives a 404; a key sent to the wrong provider is what gives a 401.

5. Headers: Bearer or x-api-key

Protocols disagree about where the key goes. OpenAI-compatible APIs expect `Authorization: Bearer <key>`. The requests in Anthropic's documentation send `x-api-key: <key>` together with an `anthropic-version` header. If you build HTTP requests by hand, watch for a missing `Bearer ` prefix, a doubled `Bearer Bearer ...` (the SDK already added the prefix and you added another), the key placed in a query string or the JSON body, or a misspelled header name. Official SDKs rarely get the header wrong; hand-rolled requests with requests, fetch or axios are where it breaks. Some compatible endpoints accept either style — cocodot's /v1/messages accepts x-api-key or Bearer — but do not assume every provider does; follow the provider's own docs.

6. Works on my machine, 401 in production

When curl works and the app does not, the app is very likely holding a different value than you think. The usual suspects: an old key exported in your shell while the .env file has the new one — many .env loaders do not overwrite variables that already exist, so the old value silently wins; a changed .env or system variable while the IDE, terminal or dev server was never restarted, so the running process still has the old value; code running in a container, CI job or serverless function where your local variables were never passed in, so the key is an empty string; or two places in the codebase reading the key under different variable names, with only one of them updated. Production adds its own twist — a secret updated in one environment but not another, or a deployment that is still running the previous configuration. The fix is the fingerprint from section 3, logged on the line just before the request is sent, in the environment that fails. That shows you the value that actually goes out.

7. 401 inside Claude Code, Cursor and other clients

In desktop tools and CLIs, a 401 is usually layered configuration: the tool has a key saved in its settings, another sits in an environment variable, a login session holds a third, and the one in effect is not the one you just edited. Find out which address and key the tool is really using (its settings screen or debug output), remove every extra set so only one remains, then quit the tool completely and start it again. For Claude Code pointed at cocodot, the /claude-code page uses two environment variables: `ANTHROPIC_BASE_URL=https://cocodot.co/api/ai`, and `ANTHROPIC_AUTH_TOKEN` set to your cocodot key. If you previously configured another provider's key or address, clear it in the current shell before launching. Full Claude Code setup is at /hub/claude-code; connecting Cursor to a custom endpoint is covered at /hub/cursor-china-claude-gpt-api.

8. Still 401? Rotate the key and bring the request ID

If everything above checks out and the 401 persists, stop fighting that key: revoke it in the console, create a new one, and update every place that uses it — servers, CI secrets and teammates' machines included — then repeat the curl test from section 2. If that still fails, contact the provider's support with the request ID, the time of the request and the exact error body. Anthropic's documentation notes that every API response carries a unique `request-id` header, and error bodies repeat it as a `request_id` field, which lets support find that exact request. Once the 401 is gone, a different error means the request cleared the identity gate and you need a different playbook: wrong model names at /hub/api-model-not-found-fix, rate limits and concurrency at /hub/api-429-rate-limit-concurrency-china, Claude overload at /hub/claude-api-529-overloaded, oversized context at /hub/context-length-exceeded-fix, OpenRouter region blocks at /hub/openrouter-403-china-alternative, and timeouts or connection failures at /hub/ai-api-errors-china-fix.

401 symptom → root cause → fix

SymptomRoot causeFix
A freshly created key also returns 401The key was truncated, or picked up a leading or trailing space, newline or quoteCompare the key's length and first and last characters with the console; copy it again
curl works, the app returns 401The app loads an old key, an empty string or a placeholderLog a fingerprint of the key (never the full key) right next to the request
401 started right after switching providersProvider A's key is being sent to provider B's addressKey and base_url must come from the same provider; log the real request URL
Only one SDK or client returns 401Wrong header: Bearer where x-api-key is expected, or the reverseUse the header style the endpoint documents
Everything worked, then every call returns 401The key was revoked, deleted or expiredCheck the key's status in the console, rotate it, update every place it is used
The status is 403 or 402, not 401You were identified; the problem is permissions or billingTroubleshoot that status code — a new key will not help

FAQ

Why do I get 401 Incorrect API key provided even though the key is correct?

In practice it is usually one of three things: the app sends a different string than the one you are looking at (an old key, an empty value, or hidden whitespace), the key and the base_url belong to different providers, or the header is wrong. Run one curl call with the same key and address outside the app — if it works, inspect the app and its environment; if it fails, check the key against the endpoint.

What is the difference between a 401 and a 403?

Per Anthropic's error reference, 401 `authentication_error` means the key itself is the problem (malformed, revoked or expired) and the server could not identify you. 403 `permission_error` means you were identified but the key lacks permission for that resource. A 401 sends you to the key; a 403 sends you to account, workspace or access settings, and a new key will not fix it.

Can running out of credit cause a 401?

Usually not — billing problems have their own errors. In Anthropic's reference, billing or payment issues are 402 `billing_error`. OpenAI reports quota problems as insufficient_quota; check OpenAI's official error-codes page for the current status code and handling, and see /hub/openai-api-key-cost-free-credit for how keys and credit relate. Faced with a 401, check the key before paying for anything.

The model list loads, so is my key fine?

Not necessarily. On some endpoints the model list is public and answers without a key — cocodot's /v1/models is one example. To validate a key, send a request that genuinely requires authentication, such as a chat request with a tiny max_tokens.

Claude Code says Invalid API key — what should I check?

Work out which address and key Claude Code is actually using: environment variables, saved settings and a login session can all coexist. Keep one set, quit fully and relaunch. For cocodot, set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN as the /claude-code page shows; the full walkthrough is at /hub/claude-code.

About cocodot

cocodot is a payment and AI access service for developers and cross-border teams in mainland China. It provides US-BIN virtual cards issued by a licensed institution — used to pay for overseas subscriptions and ad accounts — and an OpenAI-compatible AI API gateway for calling Claude, GPT and Gemini from within mainland China. Both share one wallet, funded by Alipay / WeChat Pay (RMB), USDT / USDC and accounted in USD. Card: $9.9 to open, 3% to load, $1 per active card per month; spending: $0.60 settlement fee on purchases under $20; a corresponding fee applies when the issuer charges one.

Service scope, pricing and limits →