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
| status | body | when |
|---|---|---|
| 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 |
| 422 | validation error list | records id is not a UUID |
| 422 | validation error list | records create body misses text |
| 422 | validation error list | records 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.
| status | body | when |
|---|---|---|
| 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:
| status | body | when |
|---|---|---|
| 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:
| header | meaning |
|---|---|
Retry-After | seconds to wait before the next call |
x-mivia-ratelimit-limit | your current requests/second rate |
x-mivia-ratelimit-remaining | calls 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
| header | meaning |
|---|---|
x-mivia-debit | the tier that billed you: included (the pool) or paid (the wallet) |
x-mivia-free-remaining | included 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:
{
"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.