mivia

mivia / docs / errors

Errors

Two refusal kinds ride HTTP status codes on REST and inside the tool result on MCP. Nothing else fails silently.

REST status codes

statusbodywhen
401{"detail": "invalid api key"}bearer key malformed, unknown, or rotated out
401{"detail": "not authenticated"}session cookie missing, unknown, or user gone
404{"detail": "unknown method"}method name not in the api's method list
404{"detail": "record not found"}records id missing or foreign; deletes answer this too when the caller may not delete
400{"detail": "invalid cursor"}records list cursor is not an offset
422validation error listrecords id is not a UUID
422validation error listrecords create body misses text
422validation error listrecords list limit is not an integer
422{"detail": "field 'schema' must be a built-in schema name"}records create schema not built-in
413{"detail": "body too large"}records create text field over 64 KiB in UTF-8 bytes
429{"detail": "rate limited"}request rate over the limit; never billed
429{"detail": {"error": "balance empty", "upgrade_url": "/billing"}}the included pool is spent and the balance is zero; the link opens billing
429{"detail": "too many requests"}auth-surface throttle (throttle() in auth policy: login, register, reset); carries Retry-After, never billed
502{"detail": "model unavailable"}model surface only: model unreachable or at capacity; the call is refunded

Classify errors

POST /v1/classify/{classifier_id} adds its own rows on top of the shared ones. It uses the same bearer keys and the same two 429 kinds as the model surface.

statusbodywhen
404{"detail": "unknown classifier"}classifier id missing or owned by another tenant
410{"detail": "classifier archived"}the classifier was archived
409{"detail": "classifier not published"}no ready version sits behind the endpoint
422{"detail": "field 'text' must be a non-empty string"}text missing, empty, or not a string
413{"detail": "text exceeds 64 KiB"}text over 64 KiB UTF-8 bytes
502{"detail": "model unavailable"}model service down; the debit is refunded

There is no classifier-specific quota 429. Classify calls draw the same tenant pool as every product: one included call each until the pool is spent, then the wallet pays per call. When the pool is spent and the balance is zero, the call answers the balance empty 429 from the shared table above. See classifiers.

A low-confidence answer is not an error: it is a 200 with label: null, status: low_confidence, and the candidate labels.

Records errors with a classifier id

POST /records/create with classifier_id routes through your classifier. Routing failures answer before any store:

statusbodywhen
404{"detail": "unknown classifier"}classifier id missing or foreign
410{"detail": "classifier archived"} or {"detail": "classifier not published"}archived, or nothing published to route through
429{"detail": {"error": "balance empty", "upgrade_url": "/billing"}}the pool is spent and the balance is zero
502{"detail": "model unavailable"}model service down; the call is refunded

Without classifier_id, records create keeps its own rules: a down, slow, or unconfigured classifier never fails the create, it stores a fallback record with 200 and refunds to net zero.

Records routes always need the session cookie, so they return the not authenticated 401. Reads and deletes (GET /records/get, GET /records/get/{id}, DELETE /records/delete/{id}) ignore the bearer key entirely and never return the invalid api key 401. Only POST /records/create reads the bearer, to pick the billed tenant, so only create can return invalid api key when a bad key is sent. Only create can return the balance empty 429. The rate limited 429 fires on create only when a records bucket is configured.

An upstream model error passes through verbatim with its own status code and body on the model surface only (routers/model_api.py, mcp.py). The call still bills, because the model answered. Records never passes through: a down, slow, unconfigured, or bad-answer classifier stores a fallback record with 200 and refunds to net zero.

The two kinds of 429

rate limited carries headers:

headermeaning
Retry-Afterseconds to wait before the next call
x-mivia-ratelimit-limityour current requests/second rate
x-mivia-ratelimit-remainingcalls left in this window

balance empty carries none of those. It means money, not speed: the month's included pool is spent and the balance is zero. Top up and the same call goes through. See pricing.

Response headers on every billed call

headermeaning
x-mivia-debitthe tier that billed you: included (the pool) or paid (the wallet)
x-mivia-free-remainingincluded pool calls left this month; the name predates the pool, the meaning is pool-wide

MCP refusals

MCP never turns a business refusal into a protocol error. A refused tools/call answers HTTP 200 with the refusal inside the result:

codejson
{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "isError": true,
    "content": [{"type": "text", "text": "rate limited"}]
  }
}

The text is one of rate limited, balance empty; top up at /billing, or model unavailable. Agents should read isError and retry or stop on the same rules as the REST table above.