Docs
Errors

Errors

Error response shape, the full error-code reference, HTTP status mapping, and when to retry vs not.

Construction AI returns errors in a consistent JSON shape with a machine-readable error code and a human-readable message. This page documents the shape, lists the codes, and gives guidance on retry semantics.

The error shape

Every error response follows this shape:

{
  "success": false,
  "error": "<code>",
  "message": "<human-readable message>",
  "details": "<optional context>"
}
  • success — always false for errors. The presence of this field lets you discriminate success/error at the top level without reading the HTTP status.
  • error — machine-readable code, lowercase snake_case (unauthorized, forbidden, not_found, etc.). Stable; we won't change a code without a versioned migration.
  • message — human-readable, intended for log output or surfacing to a developer. Don't string-match on this — it may change.
  • details — optional. May carry additional context (e.g. validation field names, missing scope names).

Note on RFC 7807

The OpenAPI spec describes errors using RFC 7807 Problem Details semantics. Today the platform returns the shape above (a Construction AI-specific convention that predates the OpenAPI spec). Future work will harmonise the two — either by emitting RFC 7807 documents directly or by adding a content-negotiation layer. Until then, treat the OpenAPI Problem schema as aspirational and code against the shape on this page.

The error-code reference

CodeTypical HTTP statusMeaningRetry?
unauthorized401No valid authentication. Token missing, malformed, expired, or revoked.No — fix the credential
forbidden403Authenticated but not allowed. Missing scope, project restriction, or impersonation gate.No — fix the scope grant or remove X-User-Id
not_found404The requested resource doesn't exist or isn't visible to your tenant.No — verify the ID
invalid_parameter400A path param, query string, or request body field was invalid. Details typically include the field name.No — fix the request
validation_error400Request body failed Zod schema validation. details includes the Zod error report.No — fix the request body
rate_limited429Per-key RPM or daily limit exceeded. Retry-After header indicates wait time.Yes — honour Retry-After, retry once
feature_disabled403Feature requires a configuration (e.g. BYOK Anthropic key) that isn't set up.No — configure in dashboard
no_api_key403BYOK API key (Anthropic, Voyage) not configured for the tenant.No — set up in AI Settings
api_error502Upstream service (Anthropic, Voyage) returned an error we couldn't handle.Yes — retry after a short delay
internal_error500Unexpected server-side failure. Logged for our review.Maybe — retry once after backoff; if it persists, raise via contact form

HTTP status semantics

Use the HTTP status as a coarse filter, the error code for precise handling:

  • 2xx — success. Body has success: true and the actual response.
  • 400 — your request was malformed. Fix it.
  • 401 — authentication missing or invalid. Mint or rotate the token.
  • 403 — authentication valid but not authorised. Adjust scope grants or drop the impersonation attempt.
  • 404 — resource doesn't exist or your tenant can't see it.
  • 429 — rate limit. Honour Retry-After.
  • 5xx — server problem. Retry with backoff; raise if persistent.

Retry semantics

A practical rule of thumb for automated retries:

Status / codeRetry?Strategy
429 rate_limitedYesHonour Retry-After. Retry once. If still 429, surface to user / log and escalate.
502 api_errorYesExponential backoff: 1s, 2s, 4s. Give up after 3 attempts.
500 internal_errorYes (carefully)Same backoff; treat persistent 500s as a platform incident, not a client problem.
401, 403, 404, 400NoThese are client-side issues. Retrying won't fix them. Fix the request and try again.

Example responses

Missing scope (strict mode):

{
  "success": false,
  "error": "forbidden",
  "message": "API key missing required scope: read:financial-detail"
}

Impersonation gate:

{
  "success": false,
  "error": "forbidden",
  "message": "X-User-Id specifies a different user than the key is linked to; the impersonate:user scope is required to act as another user."
}

Invalid UUID param:

{
  "success": false,
  "error": "invalid_parameter",
  "message": "projectId must be a valid UUID"
}

Validation error (request body):

{
  "success": false,
  "error": "validation_error",
  "message": "Invalid request body",
  "details": [
    { "path": ["priority"], "message": "must be one of: low, normal, high, urgent" }
  ]
}

Rate-limited:

{
  "success": false,
  "error": "rate_limited",
  "message": "Rate limit exceeded (rpm)"
}

Plus the Retry-After: 47 header on that one.

Audit trail

Authenticated failures (especially scope and impersonation denials) are logged with structured detail to support security review. The audit log captures:

  • Key ID and name
  • Requested path and method
  • Required scope vs scopes the key actually held
  • Whether impersonation was attempted

These logs are visible in the dashboard (admin only) and useful when debugging a vendor integration that's hitting unexpected 403s.

What to do when the docs and the API disagree

If you encounter behaviour that contradicts what's documented here:

  1. Check the actual response body. The on-the-wire response is the source of truth for client behaviour.
  2. Check the OpenAPI spec (coming soon) — the spec describes the contract route-by-route.
  3. Raise it via the dashboard contact form. We'd rather know about docs that lag the implementation than have you guess.

Where to go next