Errors
The API error model and the status codes you'll actually see — 401, 403, 404, 409, 422 — with example bodies and what causes each.
Errors are returned as JSON with a single detail field. FastAPI validation errors use
detail as a structured list; everything else uses a string:
{ "detail": "Unknown run" }Every response carries an X-Request-ID header — include it when reporting a problem.
Status codes
| Status | Meaning | Typical cause |
|---|---|---|
| 401 | Unauthorized | Missing or expired bearer token; invalid login credentials. |
| 403 | Forbidden | Platform-admin or role required; a rejected/waitlisted email. |
| 404 | Not found | Unknown id, or the resource is in another company (isolation). |
| 409 | Conflict | Duplicate resource (email/slug already exists); resuming a run that isn't awaiting a human. |
| 410 | Gone | A removed endpoint (e.g. the old GitHub OAuth routes). |
| 422 | Unprocessable entity | Validation failure — bad body, malformed id, disallowed email. |
| 429 | Too many requests | A rate limit was exceeded. See Rate limits. |
401 — missing/expired token
Any endpoint outside /auth/* and /health without a valid token:
{ "detail": "Not authenticated" }Re-authenticate — tokens expire after 24h (see Authentication).
403 — admin or role required
Platform-admin endpoints (e.g. GET /auth/users, the whitelist routes) for a non-admin:
{ "detail": "Admin role required." }A registration attempt from a rejected email also returns 403.
404 — unknown, or not in your company
Isolation is enforced by returning 404 rather than leaking another company's data. A run
you don't own looks identical to one that doesn't exist:
{ "detail": "Unknown run" }409 — conflict
Two common cases. A duplicate insert (an email or company slug that already exists) is
normalised from the database's unique-violation to a 409:
{ "detail": "Resource already exists" }And resuming a run that isn't parked — it already completed or failed — returns:
{ "detail": "run is completed, not awaiting a human" }422 — validation
Request-body validation failures come back as FastAPI's structured list:
{
"detail": [
{ "loc": ["body", "vertical"], "msg": "field required", "type": "value_error.missing" }
]
}The API also normalises two server-side failure modes to 422 instead of a 500:
- A malformed path/query value (e.g. a badly-formed UUID that reaches
uuid.UUID(...)) →{ "detail": "Invalid input: <reason>" }. - An invalid database value (asyncpg
DataError) →{ "detail": "Invalid input value" }.
A disallowed email (personal domain when the whitelist gate is on, or a syntactically
invalid address) also returns 422.
The 404-not-403 choice for cross-company access is deliberate: it never confirms that a
resource exists in some other company. See Multi-tenancy.