Upstream Error 403: Which Server Refused You, and How to Tell
Upstream error 403 means a server behind the one you called refused the request. The hard part is working out which one. Behind an LLM gateway there are at least three servers that can send a 403: a firewall in front, the gateway itself, and the model provider behind it. We read the error handling in new-api, the open-source gateway in the one-api family, at its commit of 2026-10-01. It has one detail that explains a lot of confused bug reports. When your balance on the gateway runs out, new-api answers with a 403, not the 402 that most providers use for payment problems.
A 403 is the HTTP code for “I understood you, and the answer is no”. Unlike a 401, retrying with the same credentials will not help. Unlike a 429, waiting will not help either. Something has to change. The word upstream tells you the refusal was passed on from further back. This page shows how to find the server that said no. It is based on reading the code of the gateway software that produces this wording. If you run a gateway, the last section is for you.
What upstream means here
In proxy terms, the upstream is the server your server forwards to. With LLM APIs the chain usually looks like this: your app → an AI gateway or reseller, often built on one-api or new-api → the model provider. That last step can be OpenAI, Anthropic, Google, or a router like OpenRouter that has its own upstream providers behind it. Each hop can refuse, and each one wraps the error it received in its own format.
Four places a 403 comes from
| Where | What the body looks like | What it usually means |
|---|---|---|
| Edge firewall or CDN | An HTML page, not JSON | Your IP, country or request pattern is blocked before the gateway sees it |
| The gateway itself | JSON with code insufficient_user_quota or pre_consume_token_quota_failed | Your balance or your token's quota on the gateway is too low for this request |
| The provider, relayed | JSON with type upstream_error and the provider's own message | The gateway's key for that provider lacks permission, is region-blocked, or the input was refused |
| The provider, unreadable | bad response status code 403 | The provider (or a firewall in front of it) returned something that was not a JSON error, often an HTML block page |
The gateway's own 403: quota
This is the one most people do not expect. In new-api's billing code (service/billing_session.go), the checks that find your balance too low return http.StatusForbidden, which is 403. We found no use of 402 anywhere in its Go source. The error codes are insufficient_user_quota and pre_consume_token_quota_failed. One of those messages, in Chinese, says the subscription quota is insufficient or not configured. These errors are also marked to skip retries, so you get them at once.
OpenRouter's documentation, by contrast, uses 402 for insufficient credits, and so do many providers, as our error code guide shows. So if you switch from calling a provider directly to calling a new-api-based gateway, the same problem, no money left, changes from 402 to 403. Error handling written for one will misread the other. Check the code field before you blame the provider. A top-up fixes this 403, and nothing else will.
The pre-consume variant has a twist. new-api reserves an estimated cost before it sends the request. In relay/helper/price.go that estimate is the prompt's token count times a multiplier the operator sets, priced at the model's rate. So when your balance is low, a long prompt, such as a big context or a long chat history, can fail the reservation while a short one still gets through. max_tokens does not enter that estimate in this version.
How a provider's 403 is relayed
When the provider returns an error, new-api's RelayErrorHandler (service/error.go) reads the body. If it parses as a normal OpenAI-, Anthropic- or Gemini-style error, the gateway keeps the provider's message and status. If that error had no type, it is labelled upstream_error (relaykit/types/error.go). If the upstream is OpenRouter, any metadata it sent is added to the message in brackets, so moderation details survive the relay. If the body is not JSON at all, the client sees bad response status code 403, and the raw body goes to the gateway's log instead of to you.
So the message you see is the best clue you have. A provider message like a permission or region error points at the gateway operator's key, which you cannot fix from the client side. A bare bad response status code 403 usually means an HTML page came back. In practice that is often a firewall in front of the provider or a wrong base URL. The same file shows the older one-api, last updated 2025-02-21, using the same upstream_error type and the same bare status-code message.
Retries and auto-disable are off by default
new-api ships with retries turned off. RetryTimes is 0 in common/constants.go. If an operator turns retries on, the status ranges in setting/operation_setting/status_code_ranges.go decide which errors are retried. By default those are 401 to 407, 409 to 499, 500 to 503 and 505 to 523, but not 400, 408, 504 or 524. A provider 403 is in that range. So on a gateway with retries on, the request is tried on another channel first, meaning another key or another provider for the same model. A 403 that reaches you then means every channel tried refused. On a gateway with the default of zero retries, one bad key is enough to give you a 403. You cannot tell from outside which setting a gateway uses, so it is worth asking the operator.
Automatic channel disabling is also off by default: AutomaticDisableChannelEnabled is false. When an operator turns it on, a channel is disabled on a 401. It is also disabled when the error message contains a phrase from a default keyword list. That list includes Permission denied, Your account is not authorized, Operation not allowed and This organization has been disabled. So with auto-disable on, a provider 403 that uses one of those phrases takes the channel out of rotation. A 403 with other wording does not.
When the upstream is OpenRouter
Many gateways forward to OpenRouter. Its error reference, read on 2026-10-03, describes 403 as insufficient permissions, a guardrail block or a moderation flag. A moderation 403 carries metadata. reasons explains why the input was flagged, flagged_input holds up to 100 characters of the offending text, and provider_name and model_slug say who asked for moderation. Because new-api keeps OpenRouter's metadata in the message, a moderation 403 that reaches you through such a gateway should include those fields. If they are there, change the input or the model. Rotating keys will not help.
Do not confuse that 403 with the 404 OpenRouter returns when no provider supports your parameters. We triggered that 404 dozens of times in our Chat Completions parameter test. It is a routing failure, not a refusal.
A diagnosis table
| You see | Who refused | Try |
|---|---|---|
| 403 with an HTML body | A firewall or CDN | Check the base URL, your IP or proxy, and the user agent |
insufficient_user_quota | The gateway, your account balance | Top up on the gateway, not the provider |
pre_consume_token_quota_failed | The gateway, the reservation against your key's quota | Shorten the prompt, or raise that key's quota |
upstream_error with a provider message | The provider, using the gateway's key | Send the message to the gateway operator; try another model |
| Message includes moderation reasons | A provider's moderation via OpenRouter | Change the input or the model |
bad response status code 403 | Something upstream that sent non-JSON | Operator: check the gateway log for the raw body |
If what you see is a 429 or 5xx rather than a 403, see our rate limit guide and the overloaded error guide.
For gateway operators
- The channel test shows the upstream's words. In new-api's admin channel test (
controller/channel-test.go), a response body that contains an error is reported asupstream error:followed by the provider's message. Run it on the channel your users hit, and you will see the same refusal they do. - Decide on retries and auto-disable deliberately. Both are off by default. With retries off, one revoked or region-blocked key gives users 403s that another channel could have served. With auto-disable on, check whether your providers' 403 wording matches the keyword list, and add phrases that it misses.
- Tell users what a quota 403 means. Your users may read a 403 as “the provider blocked me”. A line in your docs saying that 403 with
insufficient_user_quotameans top up will save support tickets. - Log the raw upstream body. new-api already logs it when the body cannot be parsed. Keep those logs long enough to match them with user reports.
We run a gateway in this family ourselves, which is why we read the source. Our self-hosted gateway guide and routing and failover guide cover the setup side.
What this cannot tell you
- Other gateway software. We read new-api at commit 1a4166d (2026-10-01) and the archived one-api. LiteLLM, Portkey and commercial resellers word their errors differently.
- Operator settings. Retry and auto-disable ranges can be changed from the admin panel. We describe the defaults in the source.
- Each provider's 403 rules. Region blocks and permission models differ from provider to provider and change over time. We describe how the error travels, not every reason a provider might refuse.
- Apps that hide the body. Some chat front-ends show only the status line. Without the body, the table above cannot tell the layers apart.
Evidence: dated measurements and catalogue fields; method, latency statistics and limitations. Measurements describe these runs, not all providers or future versions.
DataLLM Lab