Skip to content

401 and 403 From a Model API: The Six Real Causes

8 min read · updated August 4, 2026

A 401 Unauthorized from a model API means the credential was rejected: it was missing, malformed, revoked, or sent in the wrong header. A 403 Forbidden means the credential was accepted and then refused permission for this specific thing. Reading which one you have cuts the candidate list in half before you change anything.

401 and 403 are different problems

The bodies look similar — both are JSON with an error object carrying a message and often a code such as invalid_api_key or permission_denied — and it is easy to treat them as one error called “auth broken”. They are not.

StatusDescription
401The server could not authenticate you at all. Nothing about the model, the endpoint or the payload is relevant yet. Look at the credential and the header carrying it.
403You are authenticated. This key is not permitted to do this. Look at what is different about this request: the model, the region, the endpoint, the project, the spend limit.

A useful corollary: if the same key works for one model and 403s for another, stop investigating the key. If it 401s for everything including a trivial list-models call, stop investigating permissions.

A few providers return 401 where 403 would be correct, and at least one returns 403 for an exhausted quota that is really a billing state. The split above is right often enough to lead with, and the curl test below settles it either way.

The first check, which takes ten seconds

Before anything else, prove the key that is in your process is the key you think it is. This is the cause more often than every other cause combined, and almost nobody checks it first because it feels too stupid to be the answer.

# Python — never print the key itself
import os
k = os.environ.get("PROVIDER_API_KEY")
print(repr(k)[:8], "...", repr(k)[-6:] if k else None, "len:", len(k or ""))

# Node
const k = process.env.PROVIDER_API_KEY;
console.log(k?.slice(0, 6), "...", k?.slice(-4), "len:", k?.length);

Three things fall out of that one line. None or undefined means the variable never loaded — wrong .env file, a dotenv loader that runs after the client is constructed, a shell that was open before the variable was exported, a container that was not given the secret, a CI runner where the secret is not exposed to pull requests from forks. A length one or two characters longer than expected means a trailing newline or space, which happens whenever a key is written with echo instead of printf or pasted from an email client. A completely different prefix means you are looking at another provider’s key, which is routine once a repository holds four of them.

The six causes, most common first

  1. The key is not in the process. Diagnosed above. Includes the case where it is present locally and absent in production, which presents as “works on my machine” and is the same bug.
  2. Whitespace or invisible characters in the key. A trailing \n, a leading space, a zero-width character from a web page, or CRLF from a Windows-authored secrets file. The server sees a different string. Strip on read: os.environ[...].strip() costs nothing and removes an entire class of afternoon.
  3. The wrong header, or the wrong scheme in the right header. The three shapes in circulation are Authorization: Bearer <key>, x-api-key: <key>, and an api-key header used by some enterprise deployments. Sending a bearer token to a service expecting x-api-key is a 401 with a message that says nothing useful. Also in this bucket: writing Bearer twice, or omitting it entirely.
  4. The key was revoked, rotated or expired. Keys get rotated on a schedule, revoked automatically when a scanner finds one in a public repository, and deleted when the person who created them leaves. If the key worked yesterday and the code did not change, check the provider console’s key list for last-used timestamps before doing anything else.
  5. Right key, wrong scope — the classic 403. Restricted keys, project-scoped keys, and organisation membership all produce this. So does a model that requires explicit enablement on the account, and a key created in one project being used against a resource in another. The message often names the missing permission; read it literally rather than assuming it is generic.
  6. Right key, wrong endpoint. A key issued for one regional or enterprise deployment sent to the public endpoint authenticates against a service that has never heard of it. Check the base_url your client is using — print it, do not assume it — especially if an environment variable can override it, which for most SDKs it can.

Two further causes are rarer but worth knowing because they are invisible from the code. A redirect can drop the Authorization header: curl -L and several HTTP clients strip credentials when a redirect crosses hosts, which turns a working request into a 401 the moment a URL changes. And an exhausted balance or a hit spending cap is rendered as 403 by some providers, where the fix is on the billing page rather than in the code.

Isolating it with one request

Take your application entirely out of the picture. If this succeeds, the credential and the account are fine and the bug is in your code or your environment; if it fails, they are not and no amount of code reading will help.

# The smallest authenticated request most providers offer.
curl -sS -i https://api.example-provider.com/v1/models \
  -H "Authorization: Bearer $PROVIDER_API_KEY" | head -20

# Compare with a deliberately bad key to see what a real 401 looks like
curl -sS -i https://api.example-provider.com/v1/models \
  -H "Authorization: Bearer definitely-not-a-key" | head -5

Run both. The second gives you the provider’s baseline 401 body, which tells you whether the error you are chasing is the same failure or a different one wearing the same status code. Use -i rather than -v so the key is not echoed into your terminal history.

When a proxy is between you and the API

If requests pass through a corporate proxy, an API gateway, a service mesh or your own LLM gateway, there are two credentials in play and the 401 could be about either one. Establish which by sending the same request directly to the provider from the same host. If the direct call succeeds and the proxied call 401s, the proxy is the problem — commonly it strips or overwrites the Authorization header for its own auth, or it expects its own key and forwards yours, or it forwards yours and expects its own.

The generic version of this is worth building once: normalise upstream auth failures into your own error type so a 401 from a provider and a 401 from your gateway are distinguishable in logs. Normalising provider errors covers the mapping, and key handling covers keeping the credential out of the places that leak it in the first place. If the failure is a TLS error rather than an auth error it will present quite differently — certificate and proxy failures is the page for that.