ARCNM

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 on message.
  • 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 of error, and also the X-Request-ID response 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

  1. Capture X-Request-ID from the response — we log every request with this id.
  2. Read error.details for the offending field / limit.
  3. Check the status page for incidents.
  4. 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