# Error Reference (/docs/reference/error-reference)



Every gateway error uses one JSON shape. The `error` field is an **object** with `code`, `message`, and `status` — not a string.

```json
{
  "error": {
    "code": "api_key_invalid",
    "message": "Invalid token format. Expected: vnb-sk-xxx or JWT",
    "status": 401
  }
}
```

OAuth endpoints (`POST /oauth/token`, `GET/POST /authorize`, `/.well-known/*`) use a **different** shape: `{"error": "<code>", "error_description": "<message>"}`. These are noted below.

***

## Authentication — `401` [#authentication--401]

Returned when the `Authorization` header is missing, malformed, or the credential is unknown/expired.

| Code              | Condition                                                           | Fix                                                 |
| ----------------- | ------------------------------------------------------------------- | --------------------------------------------------- |
| `api_key_missing` | No `Authorization` header                                           | Add `Authorization: Bearer <credential>`            |
| `api_key_invalid` | Key format is wrong, JWT header/signature invalid, or key not found | Key prefixes: `vnb-sk-` or a 3-segment JWT          |
| `api_key_expired` | JWT access token expired                                            | Request a new token from `POST /oauth/token`        |
| `api_key_revoked` | Key was revoked in the Console                                      | Generate a new key                                  |
| `buyer_not_found` | JWT is valid but the buyer no longer exists                         | Request a new token; contact support if it persists |

The gateway auto-detects the credential type: a 3-segment (dot-separated) value is treated as a JWT; a `vnb-sk-` prefix is treated as an API key.

WWW-Authenticate header is set on `401` (missing/invalid/expired key) and `403 insufficient_scope` responses so OAuth-aware clients can bootstrap discovery.

## OAuth — `400` / `401` / `405` / `500` [#oauth--400--401--405--500]

Returned by `POST /oauth/token`, `GET/POST /authorize`, and the `/.well-known/*` metadata endpoints. &#x2A;*These use the OAuth error shape, not the gateway envelope.**

| Status | Code                        | Condition                                                                                       |
| ------ | --------------------------- | ----------------------------------------------------------------------------------------------- |
| 400    | `invalid_request`           | Body unreadable or Content-Type is not `application/x-www-form-urlencoded` / `application/json` |
| 400    | `unsupported_grant_type`    | `grant_type` is not `client_credentials`, `authorization_code`, or `refresh_token`              |
| 400    | `invalid_client`            | `client_id` empty, bad prefix, or `client_secret` mismatch                                      |
| 400    | `invalid_grant`             | Authorization code invalid/expired, PKCE mismatch, refresh token reused/rotated                 |
| 400    | `unsupported_response_type` | `response_type` is not `code`                                                                   |
| 400    | `invalid_request`           | Missing required params (`client_id`, `redirect_uri`, `code_challenge`, etc.)                   |
| 401    | `invalid_client`            | Credential lookup failed (key not found)                                                        |
| 405    | `unsupported_method`        | Wrong HTTP method on a `/.well-known/*`, `/oauth/token`, or `/authorize` endpoint               |
| 500    | `server_error`              | Token signing not configured or signing failed                                                  |
| 404    | `not_found`                 | JWKS requested but signing not configured (on `/.well-known/jwks.json`)                         |

## Protocol — `400` [#protocol--400]

Returned for MCP protocol version mismatches and header validation.

| Status | Code                              | Condition                                                                   | Fix                                                                               |
| ------ | --------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| 400    | `UnsupportedProtocolVersion`      | `MCP-Protocol-Version` header not `2026-07-28`                              | Use `2026-07-28` (MCP only)                                                       |
| 400    | `HeaderMismatch`                  | `MCP-Protocol-Version` header != `_meta.protocolVersion` in request body    | Make both match `2026-07-28` (MCP only)                                           |
| 400    | `MissingRequiredClientCapability` | Client lacks a capability required by the method                            | Add capability in `_meta.clientCapabilities` (MCP only)                           |
| 400    | `invalid_request`                 | Missing required headers (`MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name`) | Add all three headers (MCP only)                                                  |
| 400    | `invalid_request`                 | LLM `model` or `messages` missing, or invalid JSON                          | Include `{"model":"...","messages":[{"role":"user","content":"..."}]}` (LLM only) |
| 400    | `invalid_request`                 | LLM multimodal `image_url/audio/video` present                              | TEXT only — send `content: string` only (LLM)                                     |
| 502    | `publisher_error`                 | LLM stream ended with no `usage.total_tokens` chunk                         | Publisher must send a final usage chunk (LLM streaming)                           |

## Billing — `402` / `403` / `429` [#billing--402--403--429]

Returned after authentication, when the purchase/subscription check or wallet deduct fails.

| Status | Code                     | Condition                                 | Fix                                                              |
| ------ | ------------------------ | ----------------------------------------- | ---------------------------------------------------------------- |
| 402    | `insufficient_balance`   | Wallet balance can't cover the call price | Top up at **Console → Wallet** ($5–$1000 via Stripe), then retry |
| 403    | `app_not_purchased`      | No purchase or subscription record        | Purchase (or "Get" a free tier) in the Marketplace               |
| 403    | `subscription_inactive`  | Subscription exists but isn't active      | Check billing status in the Console                              |
| 403    | `subscription_expired`   | Subscription period ended                 | Renew in the Console                                             |
| 403    | `subscription_cancelled` | Subscription was cancelled                | Contact support or repurchase                                    |
| 429    | `meter_expired`          | Billing period expired, meter needs reset | Wait for meter reset                                             |

## Metering — `400` / `501` [#metering--400--501]

Returned when the gateway can't record usage for the call. Rare — the call itself reached the publisher.

| Status | Code                     | Condition                                | Fix                                          |
| ------ | ------------------------ | ---------------------------------------- | -------------------------------------------- |
| 400    | `invalid_record`         | Metering record failed validation        | Retry; contact support if it persists        |
| 400    | `invalid_timestamp`      | Metering timestamp rejected              | Retry; contact support if it persists        |
| 501    | `protocol_not_supported` | A2A metering attempted — A2A is not live | A2A is not available yet; use `mcp` or `llm` |

## Rate limiting — `429` [#rate-limiting--429]

Returned when a usage limit is exceeded. All limits use fixed-window counters.

| Status | Code                   | Condition                                     | Fix                                                   |
| ------ | ---------------------- | --------------------------------------------- | ----------------------------------------------------- |
| 429    | `rate_limit_exceeded`  | Request rate exceeded (per-minute or per-day) | Wait for the window to reset, then retry with backoff |
| 429    | `meter_limit_exceeded` | Monthly request quota exhausted               | Wait for the rolling 30-day reset, or upgrade tier    |

`429` responses do **not** include a `details` payload. There are no `X-RateLimit-*` response headers. Track your usage in the Console.

## Suspension / Scope — `403` [#suspension--scope--403]

| Status | Code                  | Condition                              |
| ------ | --------------------- | -------------------------------------- |
| 403    | `buyer_suspended`     | Buyer account suspended                |
| 403    | `publisher_suspended` | Publisher account suspended            |
| 403    | `insufficient_scope`  | JWT scope doesn't permit the operation |

## Routing — `400` / `404` [#routing--400--404]

| Status | Code                          | Condition                                                                                        | Fix                                                                  |
| ------ | ----------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| 400    | `invalid_path`                | URL doesn't match `/v1-beta/{adapter}/{username}/{appname}`                                      | Check the path                                                       |
| 400    | `invalid_adapter_type`        | Defensive only — unknown adapters never reach validation (routing returns `404 not_found` first) | Adapter segment is `mcp` or `llm`                                    |
| 400    | `invalid_adapter`             | Defensive only — route and adapter always match after routing                                    | Use `mcp` for tools, `llm` for chat                                  |
| 400    | `invalid_request`             | LLM missing `model`/`messages` or multimodal                                                     | `model` and `messages: [{role, content:string}]` required, TEXT only |
| 400    | `invalid_request`             | Missing fields or invalid JSON-RPC structure                                                     | Check the JSON-RPC body                                              |
| 404    | `app_not_found`               | No app registered for that username/appname                                                      | Verify both names                                                    |
| 404    | `publisher_not_found`         | Publisher username doesn't exist                                                                 | Check the username                                                   |
| 404    | `publisher_api_key_not_found` | Publisher has no active API key                                                                  | Publisher must regenerate their key                                  |
| 404    | `purchase_not_found`          | Purchase record missing during usage query                                                       | Confirm the purchase                                                 |

## Publisher — `4xx` / `5xx` [#publisher--4xx--5xx]

Returned when the gateway proxies to the publisher and the publisher's server responds with an error.

| Status | Code                     | Condition                                                       |                                                                                                   |
| ------ | ------------------------ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| 4xx    | `publisher_client_error` | Publisher returned a 400–499 (their error — check their docs)   |                                                                                                   |
| 5xx    | `publisher_server_error` | Publisher returned a 500+ (their server error)                  |                                                                                                   |
| 502    | `publisher_error`        | Publisher transport/initialization failure                      |                                                                                                   |
| 502    | `app_offline`            | App server unreachable — circuit opened after repeated failures | Retry shortly; contact the publisher if it persists                                               |
| 500    | `publisher_not_payable`  | Paid call blocked — publisher has no connected payout account   | Contact the publisher                                                                             |
| 504    | `gateway_timeout`        | Publisher didn't respond in time                                |                                                                                                   |
| 501    | `adapter_not_supported`  | Requested adapter isn't available (available: `mcp`, `llm`)     |                                                                                                   |
| 502    | `publisher_error` (LLM)  | Publisher returned invalid JSON or missing `usage.total_tokens` | Publisher must return `{"choices":[{"message":{"content":"..."} }],"usage":{"total_tokens":...}}` |

## Request — `400` / `404` / `405` [#request--400--404--405]

| Status | Code                 | Condition                                                                               |                                       |
| ------ | -------------------- | --------------------------------------------------------------------------------------- | ------------------------------------- |
| 400    | `bad_request`        | Unparseable request                                                                     |                                       |
| 413    | `body_too_large`     | Request body exceeds the size limit                                                     |                                       |
| 404    | `not_found`          | No route matches the URL                                                                |                                       |
| 405    | `method_not_allowed` | PUT/DELETE on a known route (GET routes to the pipeline; POST is the only write method) | Use POST (PUT/DELETE are never valid) |

## Internal — `500` [#internal--500]

| Status | Code                 | Condition                                       |
| ------ | -------------------- | ----------------------------------------------- |
| 500    | `internal_error`     | Server-side error — retry, then contact support |
| 500    | `database_error`     | Database query/write failed                     |
| 500    | `redis_error`        | Rate-limit cache operation failed               |
| 500    | `trace_error`        | Failed to start request tracing (non-critical)  |
| 500    | `invalid_app_config` | App configuration is invalid                    |
