Minctrl Docs
Reference

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

StatusMeaningTypical cause
401UnauthorizedMissing or expired bearer token; invalid login credentials.
403ForbiddenPlatform-admin or role required; a rejected/waitlisted email.
404Not foundUnknown id, or the resource is in another company (isolation).
409ConflictDuplicate resource (email/slug already exists); resuming a run that isn't awaiting a human.
410GoneA removed endpoint (e.g. the old GitHub OAuth routes).
422Unprocessable entityValidation failure — bad body, malformed id, disallowed email.
429Too many requestsA 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.

On this page