Skip to content

Errors

Format

FastAPI's standard envelope:

{"detail": "Project 'billing-servce' not found"}

Validation errors (422) carry the structured Pydantic form instead:

{
  "detail": [
    {
      "type": "string_too_long",
      "loc": ["body", "content"],
      "msg": "String should have at most 8000 characters",
      "input": "…"
    }
  ]
}

So detail is either a string or a list. Handle both:

def message(resp):
    d = resp.json().get("detail")
    if isinstance(d, list):
        return "; ".join(f"{'.'.join(map(str, e['loc']))}: {e['msg']}" for e in d)
    return str(d)

Status codes

Code Meaning Typical cause
200 OK
201 Created POST /api/projects, /api/invitations, /api/handoffs
401 Unauthenticated No key, bad key, revoked key, API key on a session-only endpoint
404 Not found Unknown project key or context id — or one owned by another organization
409 Conflict Email already registered
422 Validation failed Bad type, oversized content, out-of-range floats
429 Rate limited Over 300/min authenticated or 60/min anonymous
500 Server error A bug — report it
503 Not ready /api/health could not reach the database

The ones that surprise people

404 on a project you just wrote to

Project keys are created on write only. GET /api/context/billing-service returns 404 until something has been POSTed to that exact key. A one-character difference is a different project.

GET /api/projects lists what actually exists — start there.

404 instead of 403

A context belonging to another organization returns 404, not 403. This is deliberate: 403 would confirm the id exists. Do not read 404 as "deleted".

401 where you expected an empty list

An unauthenticated retrieval is 401, never {"memories": []}. If you receive an empty list you are authenticated and the project genuinely has no matches.

422 on a type you thought was valid

The six types are exact, lowercase, singular: fact decision preference event constraint relationship. "Decision" and "decisions" are both 422.

401 with a valid-looking key on /api/auth/*

{"detail": "This endpoint requires a dashboard session; API keys are not accepted here."}

Key management and team endpoints need a session cookie. A key cannot mint keys. The message is explicit rather than a generic "invalid token", which would send you looking for the wrong problem.

Handling 429

Retry-After carries seconds. Honour it — a retry loop that ignores it will stay rate-limited.

import time, requests

def with_retry(fn, attempts=3):
    """Retry only on 429, honouring Retry-After. Other errors surface immediately —
    retrying a 422 will never work."""
    for i in range(attempts):
        r = fn()
        if r.status_code != 429:
            r.raise_for_status()
            return r
        time.sleep(int(r.headers.get("Retry-After", 2 ** i)))
    r.raise_for_status()

Rate limits apply only when the server runs with VENKAI_ENV=production. A self-hosted development instance will never return 429, so test this path against a production-mode instance or you will not exercise it.

Client-side failure modes

Not HTTP errors, but the ones that actually cost time:

Symptom Cause
Connection refused against the hosted API Python SDK base_url defaults to http://127.0.0.1:8000. Pass it explicitly.
Writes succeed, reads return nothing Wrote and read different project keys, or MCP defaulted to forge-default.
VenkaiAuthError: No project specified SDK call with no project= and no client-level project.
Retrieval returns irrelevant memories No relevance floor exists. Filter on score yourself.
Timeouts on a large project Scoring is O(candidates). Check VENKAI_RETRIEVAL_MAX_CANDIDATES and project size.