Error Reference
Complete, verified catalog of every error the Voidnet can return.
Every gateway error uses one JSON shape. The error field is an object with code, message, and status — not a string.
{
"error": {
"code": "api_key_invalid",
"message": "Invalid token format. Expected: vnb-sk-xxx, vai-sk-xxx, or JWT",
"status": 401
}
}OAuth endpoints (POST /oauth/token, POST /authorize, /.well-known/*) use a different shape: {"error": "<code>", "error_description": "<message>"}. These are noted below.
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-, vai-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-/vai-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
Returned by POST /oauth/token, GET/POST /authorize, and the /.well-known/* metadata endpoints. 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
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
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
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
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
| 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
| 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
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 |
| 500 | publisher_not_payable | Paid call blocked — publisher has no connected payout account |
| 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 |
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) |
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 |