The API key header: Authorization Bearer or x-api-key and where each goes

OpenAI clients send the key in Authorization, Anthropic clients in x-api-key. What the gateway accepts and how to tell from the response where the key got lost.

5 min · · errors · API keys

The first 401 error when connecting a new client is often about how the key is passed, not the key itself. Different APIs have different conventions: in one the key goes in the Authorization header, in another in x-api-key. Below we cover which OpenAI API key header is needed, what x-api-key is in Claude and which of them the gateway accepts. At the end there is a diagnostic order if the request still gets 401.

Two conventions

OpenAI: Authorization Bearer

The official OpenAI API and everything that imitates its format expects the key in the standard HTTP header:

Authorization: Bearer <key>

The word Bearer, a space, then the key itself. The OpenAI SDK does this on its own: you pass api_key and it fills in the header. OpenAI has no separate x-api-key header, so queries like "openai x api key" usually arise when someone mixes up the two formats.

Anthropic: x-api-key and anthropic-version

The Anthropic API uses its own header:

x-api-key: <key> anthropic-version: 2023-06-01

The second header sets the API version. The official Anthropic SDK sends both on its own. So "anthropic x api key" is the normal way to pass the key in the Anthropic format, not something extra.

HTTP header names are case-insensitive, so X-API-Key, X-Api-Key and x-api-key are the same header.

What the Zukko gateway accepts

On Zukko one key of the form fetch_... works in all formats, and both variants are accepted:

Authorization: Bearer fetch_YOUR_KEY x-api-key: fetch_YOUR_KEY

According to the authentication documentation, Authorization: Bearer is used by the OpenAI SDK and most IDEs, while x-api-key is sent by the Anthropic SDK and other Anthropic clients (including Roo Code, although that extension was discontinued on May 15, 2026). You do not need to change their settings. The request format and the key header are unrelated: any model can be called in either format, and the key is accepted either way.

What is not supported: passing the key in the query string (?api_key=...); such a request gets 401. Other header names (for example api-key) are not described in the documentation, so do not count on them.

Examples for both formats

OpenAI format (/v1/chat/completions), Bearer header:

bash
curl https://api.zukko.pro/v1/chat/completions \
  -H "Authorization: Bearer $ZUKKO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-6.1-sol","messages":[{"role":"user","content":"Hello"}]}'

Anthropic format (/v1/messages), x-api-key header:

bash
curl https://api.zukko.pro/v1/messages \
  -H "x-api-key: $ZUKKO_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{"model":"claude-sonnet-5-5","max_tokens":256,"messages":[{"role":"user","content":"Hello"}]}'

For the Anthropic format, send anthropic-version: 2023-06-01: SDKs set it themselves, and in curl add it by hand, as in the example above and in Anthropic's official examples.

Now the same through SDKs. In Python each SDK has its own base URL: the OpenAI SDK takes it with /v1 and sends the key in Authorization: Bearer; the Anthropic SDK takes it without /v1 (it adds it itself) and sends the key in x-api-key.

bash
import os
from openai import OpenAI
import anthropic

oa = OpenAI(base_url="https://api.zukko.pro/v1", api_key=os.environ["ZUKKO_API_KEY"])
print(oa.chat.completions.create(
    model="gpt-6.1-sol",
    messages=[{"role": "user", "content": "Hello"}],
    max_tokens=50,
).choices[0].message.content)

an = anthropic.Anthropic(base_url="https://api.zukko.pro", api_key=os.environ["ZUKKO_API_KEY"])
print(an.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=50,
    messages=[{"role": "user", "content": "Hello"}],
).content[0].text)

Note the addresses: clients that append /v1 themselves get the address without it, the others with it. If you mix them up, the error will be about the path (most often 404), not the header. More in Base URL with or without /v1.

ClientWhat it sends
OpenAI SDK, LangChain (OpenAI), Cursor, Cline in OpenAI modeAuthorization: Bearer
Anthropic SDKx-api-key and anthropic-version
Roo Code in Anthropic mode (discontinued on May 15, 2026, but installed copies are still around)x-api-key
Claude Code with the ANTHROPIC_AUTH_TOKEN variableAuthorization: Bearer

The last row matters: in Claude Code the token from ANTHROPIC_AUTH_TOKEN goes out as Bearer, and that is the variable we recommend for Zukko.

Diagnosis: the key went to the wrong place

A 401 response from the gateway looks like this:

bash
{
  "type": "error",
  "error": {
    "type": "authentication_error",
    "message": "Invalid API key. Use Authorization: Bearer fetch_*. ..."
  }
}

Go through the causes from top to bottom:

  1. The key did not reach the header. The environment variable is empty (echo ${#ZUKKO_API_KEY} prints the length; 0 means the variable is empty). In curl the quotes are needed, otherwise the shell may eat part of the string.
  2. A space or newline at the end. A common problem when copying from a messenger. A header with such a key will not match.
  3. No Bearer word or a double prefix. Authorization: fetch_... without Bearer and Authorization: Bearer Bearer fetch_... both give 401.
  4. The key in the query string. A ?api_key=... string is not supported.
  5. The key is revoked or deleted. Check the key list in the dashboard. The full key is shown once at creation, so a lost key has to be created again.
  6. A key from another system. An OpenAI or Anthropic key will not work: the gateway has its own fetch_... keys.

A trick with /v1/models

By the documentation, GET /v1/models is open and the key is ignored there. So the check "the model list comes back, so the key is valid" is wrong. To test a key, call an endpoint that needs it, for example GET /v1/balance:

bash
curl https://api.zukko.pro/v1/balance -H "Authorization: Bearer $ZUKKO_API_KEY"

A 200 means the key is recognized. If you get 401 here, the problem is the key or the header itself.

Not 401 but another error

The header can be fine and the error elsewhere:

  • 402: not enough balance;
  • 403 with the code email_unverified: the email is not confirmed;
  • 403 with the code model_not_allowed_for_key: the model is not in this key's allowed list;
  • 404: wrong base URL or an unknown model name (model_not_found);
  • 429: the key's daily or monthly limit is exhausted.

We covered the general error table in 401, 402, model not found.

Header security

  • Never send the key over plain HTTP: use only https://api.zukko.pro.
  • Do not put the key in code that runs in a visitor's browser: the header is visible in developer tools. Keep the key on the server.
  • Create a separate key per application and give it a daily limit. See the key limits documentation.

If you need a key, create it in the Zukko dashboard and test it with the requests above.

FAQ

Is x-api-key needed for OpenAI-compatible requests?

No. For /v1/chat/completions the standard way is Authorization: Bearer. The gateway accepts x-api-key too, but the OpenAI SDK does not send it and you do not need to change that.

Can I send Bearer in the Anthropic format?

Yes. The gateway accepts Bearer for /v1/messages as well, which is why ANTHROPIC_AUTH_TOKEN works in Claude Code. The official Anthropic SDK uses x-api-key by default, and that works too.

Is the anthropic-version header needed?

For the Anthropic format, yes. SDKs add it themselves, and in curl specify anthropic-version: 2023-06-01 by hand.

Why does the model list respond while chat gives 401?

Because GET /v1/models does not require a key. Test the key with GET /v1/balance or a real model request.

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.

Get API keyDocumentation

api.zukko.pro · fetch_*

OpenAI API key header: Bearer vs x-api-key · Zukko