⚡ New — Kimi K3 is live: bring your own Moonshot key →
Documentation

Errors

Every error, on every endpoint, uses one JSON envelope:

{
  "error": {
    "message": "Human-readable explanation",
    "type": "rate_limit_error",
    "code": "daily_limit_reached"
  }
}

Branch on code (stable, machine-oriented); show message to humans. 429 responses carry a retry-after header (seconds).

Absence of retry-after is meaningful. We send it only when waiting actually helps. A 503 provider_unavailable_billing has noretry-after because no amount of waiting fixes it — it clears when we top the provider account up. Treat a retryable failure and an unrecoverable one differently rather than backing off blindly into both.

One caveat worth knowing: no_route is emitted at three statuses (400, 404, 422). The code means the same thing each time — nothing was dialed — and the status tells you why the pool was empty. Branch on the status if you need to distinguish them; see the table below.

Empty-body 502/503/504 — the request never reached us

Everything on this page below describes an application error: the gateway ran, decided, and answered with the JSON envelope above. Occasionally you will instead get a5xx with an empty body. That did not come from the gateway. It came from the edge proxy in front of it, and it means your request was never processed.

The distinction is not cosmetic — branching on error.code gives youundefined, so code that switches on our codes falls through to its default branch and usually reports something misleading.

Application errorInfrastructure error
BodyJSON envelope with error.codeEmpty (content-length: 0)
server header—Caddy
Reached the gateway?Yes — it chose this responseNo
Retry?Per the table below; absence of retry-after means waiting won't helpYes, with backoff. No retry-after is sent, but here that means "we can't estimate", not "don't retry"

Detect it on the body, not the status. 502, 503 and 504 all appear as documented application codes too, so the status alone cannot tell you which kind you have. An empty body with no parseable error object is the reliable signal:

if (!res.ok) {
  const text = await res.text();
  if (!text) {
    // Edge/infrastructure: never reached the gateway. Retry with backoff.
    // Nothing was routed, so nothing was metered.
    throw new RetryableError(`gateway unreachable (HTTP ${res.status})`);
  }
  const { error } = JSON.parse(text);   // application error: branch on error.code
}

Where to look: status.bharatrouter.com, which is probed from outside our infrastructure and so keeps working when the gateway does not. Note that GET /health is not useful for this class of failure: if the edge cannot reach the gateway, it cannot serve /health either, so you will get the same empty 503 from the endpoint you are using to diagnose the empty 503.

All error codes

Inference (chat & embeddings)

HTTPCodeWhenWhat to do
401invalid_api_keyMissing, malformed, revoked, or expired key.Check the Authorization: Bearer br-… header; mint a new key in the console.
404model_not_foundModel id isn't in the catalog and isn't a BYOK-discovered model. This is the status on chat, embeddings and /v1/models.List valid ids at GET /v1/models; for BYOK models use the provider/model-id form.
400model_not_foundThe same condition on the multimodal endpoints — images, audio and documents — where an unknown id is treated as a bad request parameter rather than a missing resource. Same code, same meaning; only the status differs by endpoint family.As above. If you branch on status rather than code, handle both.
400no_routePre-flight. The model exists but no eligible route could be resolved, so nothing was dialed — a pinned provider that doesn't serve the model, a BYOK-only model with no saved key, or a fallback chain whose every step is unresolvable.Relax the constraint, save a provider key, or pick a model with a platform route from the catalog.
404no_routePre-flight, via a preset. An auto:* preset or route: "best" resolved to no reachable model for this request. 404 rather than 400 because the preset itself is valid — it just has nothing to point at right now.Add a provider (BYOK) key, relax the preset, or request a concrete model id — see GET /v1/models.
422no_routePre-flight, residency. A hard constraint emptied the candidate pool — typically data_policy: india_only on a model or Sangam panel with no India-resident member. The request is well-formed but cannot be satisfied, so it is refused rather than silently served offshore.Choose an India-resident model or panel (e.g. bharatrouter/auto), or drop the residency pin for this call.
403model_gatedModel requires a non-zero data-retention window that's off by default (BharatRouter is zero-retention by default). Affects models like claude-fable-5, which needs 30-day retention.An owner/admin enables it once at Console → Organization (PUT /me/org/data-retention), or an agent on a management key calls the set_data_retention MCP tool, then retry.
402insufficient_creditsStandard key, balance is zero or negative.Top up. Not retryable until you do.
429rate_limit_exceededPer-minute limit hit (key's rpm_limit or the 120/min gateway cap).Back off per retry-after; raise the key's limit if self-imposed.
429daily_limit_reachedKey's daily request cap spent.Resets at midnight IST. Raise the cap, or upgrade off a trial key.
429budget_exceededThe key's or its workspace's monthly ₹ budget is spent.Raise the budget on the dashboard, or wait for the new month (IST).
502all_routes_failedRuntime. Routes existed and were dialed, but every one failed transiently; the message includes the last upstream error.Retry with backoff — circuits recover in ~30 s. Check status for incidents.
503provider_unavailable_billingRuntime, our side. The route was dialed and the provider refused on billing grounds — the account behind it is unfunded or suspended. Nothing to do with your key or your balance.Not retryable, and deliberately sent without a retry-after: only a top-up on our side clears it. Pick another model; GET /health names the affected provider.

Dashboard & account

HTTPCodeWhen
401no_sessionCalling a /me/* endpoint without being signed in.
400duplicate_key_nameAn active key with that name already exists in your org.
400trial_ceilingTrying to raise a trial key past 60 req/min or 200 req/day.
503provider_not_configuredThat OAuth sign-in method isn't configured on this deployment.

Billing

HTTPCodeWhen
400address_requiredCreating an order before adding a billing address.
400bad_addressBilling address failed validation (the message names the field).
400promo_invalidPromo code doesn't exist, is expired, or is exhausted.
400promo_redeemedYour org already redeemed this code.
503billing_disabledBilling isn't configured on this deployment.

BYOK

HTTPCodeWhen
400bad_keySubmitted key doesn't look valid for that provider.
404unknown_providerNo such BYOK provider id.
503byok_disabledBYOK isn't configured on this deployment.

Routing, collections, endpoints & teams

The management endpoints share a small set of validation codes:

HTTPCodeWhen
403forbiddenAction needs a role you don't have (most writes are owner/admin-only).
401key_disabled_inactiveThe key was soft-disabled after inactivity (org policy, default 90 days idle + 14-day notice; never deleted). An owner/admin re-enables it in the console or with PATCH /me/keys/:id {"enabled": true}; mark idle-by-design keys {"lifecycle_pin": "keep"} to exempt them. Threshold: PUT /me/org/key-inactivity.
404not_foundNo such collection, endpoint, chain, member, or invitation in your org.
400bad_steps / bad_modelA chain or collection has invalid steps, or targets a model that isn't a catalog id.
400bad_name / bad_readmeName or README fails validation (length or characters).
400cap / team_cap / member_capA per-org limit is reached (collections, team orgs, or members).
400bad_request / unavailableBYOE config is invalid (e.g. SSRF-blocked URL) or the feature isn't configured.
400bad_email / bad_role / already_member / already_invited / last_owner / personal_orgMembership errors — see Teams (you can't demote the last owner, or add members to a personal org).
400duplicate_workspaceA workspace with that name already exists in the org.

Edge 403 (Cloudflare error 1010) — not a BharatRouter error

A 403 with Cloudflare "error 1010" (an HTML body, not our JSON error shape) means the request was blocked at the edge WAF before reaching the gateway — it is triggered by bot-fighting rules on some default HTTP-client User-Agent strings (e.g. bare python-requests/python-urllib). It is not a rate limit and not an auth failure, though it is often misread as both. Fix: send a realUser-Agent header identifying your application (e.g. my-app/1.0) — the official OpenAI SDKs already do and are unaffected. If a legitimate integration still gets 1010, tell us and we'll allow-list its agent string.

Handling errors in code

With the OpenAI SDKs, BharatRouter errors surface as the SDK's standard exceptions — the envelope rides inside. A retry policy that covers everything above:

# Python
import time, openai

def chat(client, **kw):
    for attempt in range(4):
        try:
            return client.chat.completions.create(**kw)
        except openai.RateLimitError as e:        # 429: rate/daily/budget
            code = (getattr(e, "body", None) or {}).get("error", {}).get("code")
            if code in ("daily_limit_reached", "budget_exceeded"):
                raise                              # waiting seconds won't help
            time.sleep(2 ** attempt)
        except openai.APIStatusError as e:
            if e.status_code == 502:               # all_routes_failed
                time.sleep(2 ** attempt)           # circuits recover in ~30s
            else:
                raise                              # 400/401/402: fix, don't retry
    raise RuntimeError("retries exhausted")
// Node
try {
  await client.chat.completions.create({ model: "gemma-4-e4b-it", messages });
} catch (err) {
  const code = err?.error?.code ?? err?.code;
  if (code === "insufficient_credits") notifyOwnerToTopUp();
  else if (err.status === 429) scheduleRetry(err.headers?.["retry-after"]);
  else if (err.status === 502) scheduleRetry(30);   // all_routes_failed
  else throw err;
}

Rule of thumb: retry 429 (except daily_limit_reached /budget_exceeded) and 502 with backoff; never blind-retry 400/401/402 — they need a fix, not patience.