Building blocks
Errors
Status codes, the error envelope, the stable error-code catalog, and how to debug.
Every ARCNM error returns the same JSON envelope — a stable
error.code, a human-readable message, and a request_id for
support. Branch on code, never the message:
{
"error": {
"code": "insufficient_scope",
"message": "API key / token missing required scope",
"details": {},
"doc_url": "https://api.arcnm.io/…"
},
"request_id": "req_9c5f-…"
}
code— stable machine-readable identifier. Switch on this, not onmessage.message— human-readable, English. Safe to log; translate before showing end-users.details— endpoint-specific context (offending field, valid values, limits). Always an object.doc_url— a link to the relevant docs for this code.request_id— a top-level sibling oferror, and also theX-Request-IDresponse header. Quote it in support tickets.
Status codes
| Code | Meaning |
|---|---|
200 OK |
Success, body returned |
201 Created |
Resource created |
202 Accepted |
Async work enqueued — poll for the result |
204 No Content |
Success, no body |
400 Bad Request |
Payload doesn't match the schema |
401 Unauthorized |
Missing / invalid / expired credential |
402 Payment Required |
Wallet can't cover the call, or plan entitlement exceeded |
403 Forbidden |
Authenticated but lacks scope / role / verification |
404 Not Found |
Resource doesn't exist in this tenant |
405 Method Not Allowed |
Wrong method for the route |
409 Conflict |
Uniqueness or state conflict (e.g. duplicate part_number) |
410 Gone |
Resource permanently removed |
412 Precondition Failed |
Precondition (e.g. If-Match) not met |
413 Payload Too Large |
Request body exceeds the size cap (details.observed_bytes, details.limit_bytes) |
415 Unsupported Media Type |
Wrong Content-Type (e.g. JSON for a CAD upload) |
422 Unprocessable Entity |
Schema valid but a value was rejected |
423 Locked |
Wallet administratively paused |
429 Too Many Requests |
Request rate limit hit — see Rate limits. Plan-quota exhaustion is 402, not 429 |
503 Service Unavailable |
Transient overload or deploy window — honour Retry-After, then retry |
5xx (other) |
Server / upstream error — retry with backoff |
Billed on success only: non-2xx responses cost €0, including 429s.
Error code catalog
These code values are emitted by the platform's typed errors and are
stable. Route-level HTTPExceptions that don't map to a typed error get
a code derived from the status (400→bad_request, 401→unauthorized,
403→forbidden, 404→not_found, 405→method_not_allowed,
409→conflict, 410→gone, 415→unsupported_media_type,
422→unprocessable_entity, 429→too_many_requests) — with the specific
reason in message (e.g. calculation_not_found,
max_attempts_exceeded).
Authentication & authorization
| Code | Status | Meaning |
|---|---|---|
unauthorized |
401 | Missing or invalid credential |
invalid_api_key |
401 | API key not found or revoked |
mfa_required |
401 | Session needs an MFA step |
replay_detected |
401 | Token / key replay — session revoked |
forbidden |
403 | Authenticated but not allowed |
insufficient_scope |
403 | Key/token lacks the required scope |
tenant_isolation_violation |
403 | Tenant context required / mismatch |
email_verification_required |
403 | Caller's email isn't verified |
recent_auth_required |
403 | Action needs fresh credentials |
Billing (402 / 423)
| Code | Status | Meaning |
|---|---|---|
insufficient_funds |
402 | Wallet can't cover the request |
entitlement_exceeded |
402 | Plan limit exceeded |
quota_exceeded |
402 | Plan quota for a feature exhausted. Calculation submits always carry details.cells_requested / details.cells_available (a single run asks for 1). A grid the remaining quota can't cover never runs in part: /calculations/batch rejects the whole request with nothing persisted, while /quote and /upload-and-quote save the grid's cells parked blocked first — their details then also carry batch_id and parked_cells, so once unblocked you resume that batch instead of re-submitting it. Shrink the grid, upgrade the plan, or top up your balance — the coverage block on the parked calculations says which |
wallet_locked |
423 | Wallet is paused |
State & validation
| Code | Status | Meaning |
|---|---|---|
bad_request |
400 | Malformed request |
invalid_cursor |
400 | Pagination cursor is malformed, was issued for a different query (filters or order changed mid-walk), or was sent together with offset — see Pagination |
not_found |
404 | Resource not in tenant |
method_not_allowed |
405 | Wrong HTTP method for the route |
conflict |
409 | Uniqueness or state conflict (e.g. a duplicate unique key, or a referenced resource that does not exist / is still in use). When a database constraint raises it, details.constraint names the violated constraint |
no_organization_context |
409 | User must create/join an org first |
idempotency_in_flight |
409 | Same Idempotency-Key still processing |
idempotency_conflict |
409 | Same key reused with a different body |
subscription_not_provisioned |
409 | Subscription not yet linked to billing |
gone |
410 | Resource permanently removed — don't retry, and drop the id |
precondition_failed |
412 | Precondition not met |
payload_too_large |
413 | Request body exceeds the size cap (details.observed_bytes, details.limit_bytes) |
unsupported_media_type |
415 | Wrong Content-Type for the route — e.g. JSON sent to a multipart CAD upload |
unprocessable_entity |
422 | Value rejected by a business rule |
value_too_long |
422 | A string field exceeds the column it is stored in. details.max_length is the limit; details.field names the field when it can be identified (e.g. part_number, description). Send a shorter value — retrying the same one always fails |
material_unresolved |
422 | The material_ref you sent matches no material in the catalogue, or the material_grade_id is not a known grade. details.candidates carries ranked near matches (same shape as POST /materials/lookup) — pick one and resend its material_grade_id. Raised identically by every calculation entry point: POST /calculations, /quote, /batch, /upload-and-quote(-json) and PATCH /calculations/{id}/material. Omit the material entirely to price what the part's own drawing declares |
Rate limits & server
| Code | Status | Meaning |
|---|---|---|
rate_limited |
429 | Burst rate limit — the per-caller weighted request budget for the current window is spent (details.limit, details.window_seconds). A pacing problem, not a plan problem: honour Retry-After and slow down. Plan-quota exhaustion is quota_exceeded (402). A different 429 payload appears when a submit would exceed your plan's pending-calculation bound (details.reason: "tenant_pending_bound", with pending, bound, retry_after_s, queue_position, and — for a grid — cells_requested): wait for in-flight runs to drain or submit a smaller grid, and don't branch on details.limit alone |
too_many_requests |
429 | Auth-route limit exceeded |
system_busy |
503 | Transient overload (connection pool / queue saturation) — the request was not processed and cost nothing. Honour Retry-After, then retry with backoff |
service_unavailable |
503 | Deploy window or brownout — retry |
storage_unavailable |
503 | File storage is temporarily unavailable — the upload/download was not processed and cost nothing. Retry with backoff |
internal_error |
500 | Unexpected server error — quote the request_id in a ticket. Retry with backoff |
http_error |
4xx/5xx | An HTTP error with no more specific code. Branch on the status, and read message for the reason |
Debugging a request
- Capture
X-Request-IDfrom the response — we log every request with this id. - Read
error.detailsfor the offending field / limit. - Check the status page for incidents.
- File a ticket at
[email protected]with the request id and timestamp (redact secrets).
Retry guidance
| Code | Safe to retry? |
|---|---|
429 rate_limited / too_many_requests |
Yes — honour Retry-After when present, else exponential backoff |
503 system_busy / service_unavailable / storage_unavailable |
Yes — wait Retry-After when present, then exponential backoff |
5xx (other) |
Yes — exponential backoff (start 1s, cap 30s, ~5 retries) |
402 insufficient_funds |
Only after topping up the wallet |
4xx (other) |
No — fix the request first |
Pair idempotency keys with retries so repeated attempts don't create duplicate billable calculations. For ready-to-paste backoff loops (Python / TypeScript / Bash), see Rate limits → Retry strategy.
See also
- Idempotency — safe-retry contract.
- Rate limits — the 429 envelope.
- Authentication —
invalid_api_key,insufficient_scope.