Resources

Error codes

Every structured refusal, and whether retrying will help.

Contents

Errors come in two shapes. Most are FastAPI's default. A detail string meant for a log. The ones a client has to branch on carry an object instead, with a stable code.

Both shapes
// Unstructured: read it, log it, show it.
{ "detail": "Too many failed login attempts. Try again later." }

// Structured: branch on detail.code, render detail.message.
{
  "detail": {
    "code": "feature_limit_reached",
    "feature": "tcc",
    "used": 40,
    "limit": 40,
    "tier": "basic"
  }
}

The codes#

codeHTTPMeaning and what to do
email_unverified403The account has not confirmed its email. Includes the email it was sent to. Resend with POST /auth/resend-verification.
payment_past_due403The last invoice failed. Send the customer to POST /billing/portal.
allowance_exhausted403The plan's generation allowance is spent for this period. Top up or wait. Not retryable.
feature_limit_reached403A per-feature cap. Carries feature, used, limit and tier, so the message can be exact.
database_locked403/db/* requires Unlimited. Carries upgrade: true.
contracts_locked403Lead Scanner and contract endpoints require Unlimited.
premium_required403Pre-Job Analyzer and coordination require Premium or above.
ai_busy503Generation capacity is full. The request never started. Retryable: honour Retry-After.

Status codes#

CodeMeans
200 / 201 / 204Success. 201 on create, 204 on delete with no body.
401Not authenticated, or the session has ended. See why a request returns 401.
403Authenticated but not permitted: plan, role, verification or billing. Re-authenticating will not help.
404No such resource for this organization. Also what you get for another tenant's id.
422The body failed schema validation. detail is FastAPI's array of field errors.
429A credential throttle. Wait out the window; do not retry immediately.
503Generation capacity full. The only status on this list worth an automatic retry.
errors.ts
// detail is a string for unstructured errors and an object for the
// ones worth branching on, so narrow before reading .code.
function errorCode(body: unknown): string | null {
  const d = (body as any)?.detail
  return d && typeof d === 'object' ? d.code ?? null : null
}

switch (errorCode(await res.json())) {
  case 'ai_busy':              return retryAfter(res)
  case 'database_locked':
  case 'premium_required':      return promptUpgrade()
  case 'payment_past_due':      return openBillingPortal()
  case 'allowance_exhausted':   return promptTopUp()
  default:                     throw new Error('unhandled')
}
errors.py
def error_code(resp):
    """None for unstructured errors, detail is a str there."""
    try:
        detail = resp.json().get("detail")
    except ValueError:
        return None
    return detail.get("code") if isinstance(detail, dict) else None

code = error_code(resp)

if code == "ai_busy":
    # Never started, so a retry cannot duplicate work.
    time.sleep(int(resp.headers.get("Retry-After", 10)))
elif code in ("database_locked", "premium_required"):
    prompt_upgrade()
elif code == "allowance_exhausted":
    prompt_top_up()   # retrying will not help