Troubleshooting

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.

DataLLM Lab article cover: Upstream Error 403: Which Server Refused You, and How to Tell

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.

Three layers, three different 403sRead the body, not just the status code, to see which layer refused.Your appEdge / firewallCDN, WAFLLM gatewaynew-api, one-apiUpstream providerOpenAI, OpenRouter, ...403 as an HTML pageblocked IP, bot rule403 insufficient_user_quotayour balance on the gateway403 from the providerkey, region, moderationrelayed with type upstream_error; retried on other channels only if the operator enabled retriesDiagram, not to scale. Gateway behaviour from new-api source, commit of 2026-10-01.
The status code is the same 403 at each layer. The response body tells you which layer sent it.

Four places a 403 comes from

WhereWhat the body looks likeWhat it usually means
Edge firewall or CDNAn HTML page, not JSONYour IP, country or request pattern is blocked before the gateway sees it
The gateway itselfJSON with code insufficient_user_quota or pre_consume_token_quota_failedYour balance or your token's quota on the gateway is too low for this request
The provider, relayedJSON with type upstream_error and the provider's own messageThe gateway's key for that provider lacks permission, is region-blocked, or the input was refused
The provider, unreadablebad response status code 403The 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 seeWho refusedTry
403 with an HTML bodyA firewall or CDNCheck the base URL, your IP or proxy, and the user agent
insufficient_user_quotaThe gateway, your account balanceTop up on the gateway, not the provider
pre_consume_token_quota_failedThe gateway, the reservation against your key's quotaShorten the prompt, or raise that key's quota
upstream_error with a provider messageThe provider, using the gateway's keySend the message to the gateway operator; try another model
Message includes moderation reasonsA provider's moderation via OpenRouterChange the input or the model
bad response status code 403Something upstream that sent non-JSONOperator: 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

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

Evidence: dated measurements and catalogue fields; method, latency statistics and limitations. Measurements describe these runs, not all providers or future versions.

Written by

Founder of DataLLM Lab, the unified LLM gateway. Kevin tests models the boring way — same prompts, real costs, unedited outputs — and writes up what the runs actually show. Articles are drafted with AI assistance and published under his name; every first-party number comes from an executed run.

One API for every model

One API, every model.

Get a single API key for Claude Opus 4.7, GPT-5.4, and 300+ more — with automatic price comparison and routing to the best model for every request.