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— alwaysfalsefor 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
| Code | Typical HTTP status | Meaning | Retry? |
|---|---|---|---|
unauthorized | 401 | No valid authentication. Token missing, malformed, expired, or revoked. | No — fix the credential |
forbidden | 403 | Authenticated but not allowed. Missing scope, project restriction, or impersonation gate. | No — fix the scope grant or remove X-User-Id |
not_found | 404 | The requested resource doesn't exist or isn't visible to your tenant. | No — verify the ID |
invalid_parameter | 400 | A path param, query string, or request body field was invalid. Details typically include the field name. | No — fix the request |
validation_error | 400 | Request body failed Zod schema validation. details includes the Zod error report. | No — fix the request body |
rate_limited | 429 | Per-key RPM or daily limit exceeded. Retry-After header indicates wait time. | Yes — honour Retry-After, retry once |
feature_disabled | 403 | Feature requires a configuration (e.g. BYOK Anthropic key) that isn't set up. | No — configure in dashboard |
no_api_key | 403 | BYOK API key (Anthropic, Voyage) not configured for the tenant. | No — set up in AI Settings |
api_error | 502 | Upstream service (Anthropic, Voyage) returned an error we couldn't handle. | Yes — retry after a short delay |
internal_error | 500 | Unexpected 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: trueand 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 / code | Retry? | Strategy |
|---|---|---|
429 rate_limited | Yes | Honour Retry-After. Retry once. If still 429, surface to user / log and escalate. |
502 api_error | Yes | Exponential backoff: 1s, 2s, 4s. Give up after 3 attempts. |
500 internal_error | Yes (carefully) | Same backoff; treat persistent 500s as a platform incident, not a client problem. |
| 401, 403, 404, 400 | No | These 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:
- Check the actual response body. The on-the-wire response is the source of truth for client behaviour.
- Check the OpenAPI spec (coming soon) — the spec describes the contract route-by-route.
- 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
- Authentication — common 401/403 causes.
- Scopes & permissions — common 403 causes.
- Rate limits — 429 handling in depth.