Core Concepts
Errors
Every 4xx / 5xx response from the public API follows RFC 7807 — a single stable envelope with a machine-readable code you can branch on.
Envelope
All error bodies look like this:
json{ "type": "https://api.alloovium.com/errors/insufficient_scope", "title": "Insufficient scope", "status": 403, "detail": "API key is missing required scope: vault:write", "instance": "/api/v2/vault/projects", "code": "insufficient_scope", "request_id": "req_01HQZ4R9VYTC5H6NPRDFG8EJUS" }
| Field | Always present | Description |
|---|---|---|
| type | yes | URI identifying the error category. Based on the code. |
| title | yes | Short human-readable summary. May be translated in the future. |
| status | yes | HTTP status code, duplicated for clients that only parse the body. |
| detail | yes | Longer description with specifics. Safe to log; never contains stack traces. |
| instance | no | Request path that produced the error. |
| code | yes | Stable machine-readable identifier. Branch your code on this. |
| request_id | yes (5xx) | Attach to support tickets for end-to-end trace. |
Branch on code, not title
The
Branch on code, not title and detail fields are human-readable and may change. The code field is covered by our stability contract and will never silently rename.Error code reference
401 — authentication
| Code | Meaning |
|---|---|
| missing_api_key | No Authorization header, or not Bearer scheme |
| invalid_api_key | Key does not match any active credential |
| invalid_token | OAuth Bearer JWT is malformed, expired, or signature invalid |
| api_key_revoked | Key was revoked via the dashboard |
| api_key_expired | Key passed its expiry date |
| user_inactive | Owning user has been deactivated |
| tenant_disabled | Owning tenant is suspended |
402 — billing
| Code | Meaning |
|---|---|
| payment_required | Tenant billing is past due. Resolve in the dashboard. |
403 — authorization
| Code | Meaning |
|---|---|
| insufficient_scope | The credential is valid but doesn't carry the required scope. Response includes a missing_scopes array. |
| permission_denied | The user lacks a project- or feature-level permission (e.g. vault:deny_create_project). |
| oauth_not_supported | This capability does not accept OAuth tokens. Call it with an API key. |
json{ "type": "https://api.alloovium.com/errors/insufficient_scope", "title": "Insufficient scope", "status": 403, "detail": "API key is missing required scope: vault:write", "code": "insufficient_scope", "missing_scopes": ["vault:write"] }
404 — not found
| Code | Meaning |
|---|---|
| not_found | Resource does not exist, or the caller cannot see it. The API deliberately does not distinguish these two cases so attackers cannot probe cross-tenant IDs. |
409 — conflict
| Code | Meaning |
|---|---|
| idempotency_key_reused | Same Idempotency-Key was seen with a different request body in the 24-hour window. |
| idempotency_in_progress | Earlier request with this key is still executing. Retry in a moment. |
413 / 400 — uploads
| Code | Meaning |
|---|---|
| file_too_large | Uploaded file exceeds the maximum allowed size. |
| unsupported_file_type | File extension is not in the supported set (pdf, docx, xlsx, pptx, images, etc.). |
| invalid_document_id | A referenced document_id could not be resolved. |
422 — validation
| Code | Meaning |
|---|---|
| validation_error | Request payload failed validation. Body includes a field_errors array listing offending fields. |
| invalid_cursor | Pagination cursor was malformed, truncated, or tampered with. |
| invalid_idempotency_key | Idempotency-Key header did not match the allowed pattern (1-255 chars, A-Z a-z 0-9 . _ -). |
429 — rate limit
| Code | Meaning |
|---|---|
| rate_limited | Token bucket exhausted. Body includes retry_after_seconds. See the Rate Limits page for the full retry strategy. |
5xx — server
| Code | Meaning |
|---|---|
| internal_error | Unexpected server error. Retry with exponential backoff and attach request_id to any support ticket. |
| not_implemented | Endpoint was removed or is not yet implemented in your environment. |
Request IDs
Every response — error or success — carries an header header. On 5xx, the same ID is also embedded in the body. Include it when contacting support:
textX-Request-Id: req_01HQZ4R9VYTC5H6NPRDFG8EJUS
Support can trace the request through every layer — load balancer, FastAPI handler, database queries, downstream LLM calls — using just that ID.
Pydantic validation errors (422)
When FastAPI's Pydantic validator rejects a request body before it reaches a handler, the same envelope carries a field list mapping to the failing field paths:
json{ "type": "https://api.alloovium.com/errors/validation_error", "title": "Request validation failed", "status": 422, "detail": "1 field failed validation", "code": "validation_error", "field_errors": [ {"loc": ["body", "name"], "msg": "Field required", "type": "missing"} ] }
Handling errors in code
pythonimport httpx class AllooviumError(Exception): def __init__(self, payload): self.code = payload.get("code") self.status = payload.get("status") self.detail = payload.get("detail") self.request_id = payload.get("request_id") super().__init__(f"{self.code}: {self.detail}") def raise_for_problem(resp: httpx.Response): if resp.is_success: return try: payload = resp.json() except Exception: resp.raise_for_status() return raise AllooviumError(payload) try: resp = httpx.get(url, headers=headers) raise_for_problem(resp) except AllooviumError as e: if e.code == "insufficient_scope": prompt_user_to_grant_scope() elif e.code == "rate_limited": sleep_and_retry() else: log.error("Alloovium call failed", code=e.code, request_id=e.request_id) raise
See also
- Rate Limits — full retry strategy for 429 and 5xx.
- Idempotency — how to keep retries from creating duplicates.