---
title: Errors
description: Status codes, the error envelope, the stable error-code catalog, and how to debug.
---

# Errors

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:

```json
{
  "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` | Rate / quota limit hit — see [Rate limits](./rate-limits.md) |
| `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 `HTTPException`s 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

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](https://status.arcnm.io)** for incidents.
4. **File a ticket** at `hello@arcnm.io` 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](./idempotency.md) 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](./rate-limits.md#retry-strategy).

---

## See also

- [Idempotency](./idempotency.md) — safe-retry contract.
- [Rate limits](./rate-limits.md) — the 429 envelope.
- [Authentication](./authentication.md) — `invalid_api_key`, `insufficient_scope`.
