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 |
Rate / quota limit hit — see Rate limits |
5xx |
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 |
wallet_locked |
423 | Wallet is paused |
State & validation
| Code | Status | Meaning |
|---|---|---|
bad_request |
400 | Malformed request |
not_found |
404 | Resource not in tenant |
conflict |
409 | Uniqueness or state conflict |
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 |
precondition_failed |
412 | Precondition not met |
payload_too_large |
413 | Request body exceeds the size cap (details.observed_bytes, details.limit_bytes) |
unprocessable_entity |
422 | Value rejected by a business rule |
Rate limits & server
| Code | Status | Meaning |
|---|---|---|
rate_limited |
429 | Per-caller quota exceeded (details.limit, details.window_seconds) |
too_many_requests |
429 | Auth-route limit exceeded |
service_unavailable |
503 | Deploy window or brownout — retry |
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 — exponential backoff |
5xx |
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.