ARCNM

Building blocks

Errors

Status codes, the JSON error envelope, the stable error-code catalogue and the request id to quote to support. Branch on error.code, never on the message.

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
role_read_only 403 The signed-in user's role in this organization is read-only (details.role, e.g. viewer): it can read everything a member reads, but every change needs a member, admin or owner
tenant_isolation_violation 403 Tenant context required / mismatch
email_verification_required 403 Caller's email isn't verified
recent_auth_required 403 The action needs a step-up confirmation made within the last few minutes. details.reason is no_recent_auth (none on file), factor_not_satisfied (you confirmed with a factor this action does not accept) or verification_unavailable (we could not check — retry). details.factors_accepted lists what satisfies this action; details.factors_available lists what the caller has enrolled (null when it cannot be resolved); details.step_up names where to go — e.g. {"factor": "passkey", "endpoint": "/api/v1/sessions/sudo/passkey/options"}. Confirm there, then retry the original request within details.window_seconds. There is no password prompt: the factors are a passkey, an authenticator or recovery code, or a code emailed to you. Once an account uses an authenticator app, an emailed code is no longer accepted for step-up; details.factors_accepted then lists passkey and totp only

Billing (402 / 423)

Code Status Meaning
insufficient_funds 402 Wallet can't cover the request
entitlement_exceeded 402 Plan limit exceeded. With details.feature nesting_runs it is this month's job-nest runs (details.used, details.limit): a new start is kept as a draft instead, and POST /nest-runs/{run_id}/start answers this until a run is free
addon_required 402 The request needs the Nesting add-on: job nests (start, preview, revisions, release), a calculation's cutting file, and the nesting potential of a selection. Pricing never needs it — every sheet quote is nested true-shape on every plan. details.feature is nesting, details.required_plan scale, details.upgrade_url where to book it. Not a plan limit — book the add-on on Scale or Enterprise (or start its trial), then retry. Stored quotes and job nests stay readable without it — except the job nest an assembly calculation runs for its own sheet components, which is read or changed with the add-on only
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
environment_not_found 404 The costing_environment_id you sent is not a costing environment of your organization — another organization's id answers exactly like one that never existed. Raised identically by POST /calculations, /quote, /batch and /upload-and-quote(-json), and by GET /environments/{env_id}
cad_file_not_step 422 The 3D model is not a STEP file (no ISO-10303-21 header) — STEP is the only solid-model format a part is priced from. Export the part as STEP AP203, AP214 or AP242 and re-upload; a mesh (STL, OBJ) goes in as role=mesh and is priced on its envelope only

DXF flat patterns

Raised by the upload routes when the CAD file is a DXF flat pattern (POST /calculations/upload-and-quote(-json), POST /parts/{part_id}/revisions/{revision_id}/datasets?role=cad_3d) — at upload, before anything is stored. See Uploading files → DXF flat patterns.

Code Status Meaning
dxf_gauge_required 422 A DXF carries no thickness: send sheet_gauge_mm (millimetres, above 0)
dxf_material_required 422 A DXF names no material and has no title block to read one from: send material_grade_id or material_ref
dxf_unreadable 422 The file does not start as a DXF, or ezdxf cannot read it (details.reason). Export the flat pattern as DXF (ASCII or binary) and re-upload
dxf_units_unsupported 422 $INSUNITS (details.insunits) is neither 4 (millimetres) nor 1 (inches) — unitless files included. Export with the drawing unit set
dxf_no_contour 422 The file draws no closed contour (details.entities geometric entities were read)
dxf_open_loop 422 A contour of lines and arcs does not close: no entity continues from details.loose_end_mm (after details.chain_points points). Close the contour or join the gap
dxf_multiple_outer_contours 422 The file draws more than one outer contour (details.contours closed contours; details.reason is second_outer, crossing, island — a contour inside an opening — or openings_cross). One part per file: a nest of several parts is a job nest's output, not its input

Stock formats

Raised by /stock-formats, and — stock_format_unresolved — by every calculation entry point that accepts stock_format_id. Where a refusal is about one format, details.stock_format_id names it.

Code Status Meaning
stock_format_not_found 404 No format with this id is visible to your organization. Editing or retiring a standard format answers the same way: standard formats can be hidden, but not edited or retired
stock_format_key_taken 409 One of your current formats already uses this key (details.key). Choose another key, or edit that format
stock_format_superseded 409 The version you named is closed: it was retired, or replaced by a newer version when its size, gauge band or material changed. Past calculations were priced on it as it is, so it can't be changed, retired again or hidden — act on the current version, which GET /stock-formats lists. Its label can still be corrected
stock_format_last_remaining 409 The change would leave your organization no format to price with. Add a format of your own, or show a hidden standard format again (DELETE /stock-formats/{id}/suppression), then retry
stock_format_own_row_use_retire 409 Only standard formats can be hidden. To stop using one of your own formats, retire it with DELETE /stock-formats/{id} — it stays on record for the calculations priced on it
stock_format_unresolved 422 The stock_format_id you sent names no format your organization prices with today: it does not exist, was retired, or is a standard format you have hidden. An id from an older version of a format you still buy is not refused — it prices on the current version. Pick a format from GET /stock-formats, or omit the field to price on the format that uses the least material. Raised identically by POST /calculations, /quote, /batch and /upload-and-quote(-json), before anything is stored. On a re-run (derived_from_calculation_id) that omits the field, the format kept from the calculation re-run is checked the same way, and details.derived_from_calculation_id names that calculation — send stock_format_id to choose another format, or null to price without one

Scrap prices

Raised by PUT and DELETE /environments/{env_id}/rates/scrap/{material_category}/{scrap_class}, and — scrap_price_use_scrap_route — by the generic rate routes. Where a refusal is about one price, details names its material_category, scrap_class and grade_id.

Code Status Meaning
scrap_price_backdated 422 valid_from lies before today (details.valid_from, details.today). A scrap price starts today or later, so quotes already made keep the price they were costed at. Send today's date, a later one, or none
scrap_price_grade_category_mismatch 422 The grade_id you sent is not of the material_category in the path (details.grade_category). State the price under the grade's own category
scrap_price_not_found 404 The environment states no price for this material and kind of scrap from today on, so there is nothing to clear. The price in use comes from a parent environment or the platform — GET /environments/{env_id}/rates/scrap says which
scrap_price_use_scrap_route 422 POST /environments/{env_id}/rates and DELETE /environments/{env_id}/rates/{kind}/{rate_id} do not take kind scrap: they would stack a second price, or erase one quotes were costed at. Use PUT or DELETE /environments/{env_id}/rates/scrap/{material_category}/{scrap_class}
scrap_price_refresh_unconfigured 409 Platform operators only: no scrap price source is configured, so there is nothing to refresh
scrap_price_refresh_unparsable 422 Platform operators only: the source document could not be read (details.reason). Nothing was written

Job nesting and remnants

Raised by /nest-runs (nesting a backlog of parts together, sheet by sheet) and /remnants (the remnants a shop keeps in stock). POST /nest-runs/preview refuses exactly what starting the same job nest with POST /nest-runs would, by the same codes, and stores nothing.

Code Status Meaning
nest_run_not_found 404 No job nest with this id in your organization
nest_run_selection_not_found 404 The environment, batch or folder you named is not your organization's (details names which)
nest_run_items_unusable 422 Some items can't be nested. details.items names each by its index and calculation_id, with a reason: not_found, other_environment (priced in another environment than the one you named), not_succeeded (not priced yet), not_sheet (not cut from sheet), no_stored_layout (no layout was stored with its price), material_provided (the customer supplies the material) or missing_facts (its price does not state the thickness, the material price or the spacing a job nest reads); adding a part to a solved job nest's order also answers other_group (another material, thickness or margin than the material and thickness named, or a wider web than it was nested at). Nothing was queued; send the others
nest_run_nothing_to_nest 422 Nothing in the batch or folder can be nested in this environment. details.left_out names each calculation with its reason, as for nest_run_items_unusable
nest_run_selection_too_large 422 The batch or folder holds more calculations than one job nest takes (details.lines, details.max_lines). Nothing was queued; nest it in smaller selections
nest_run_line_too_large 422 A line asks for more copies than one job nest line takes (details.max_copies_per_line). details.lines names each such line by its index and calculation_id, with the quantity it asked for — stated, or its calculation's lot size. Nothing was queued; nest it with a smaller quantity
nest_run_too_many_copies 422 The lines together ask for more copies than one job nest takes (details.copies, details.max_copies_per_run). Nothing was queued; nest it in smaller selections
nest_run_formats_unknown 422 A format the job nest was told to buy from is not a sheet format your organization buys today (details.formats names each). Nothing was queued; name formats from GET /stock-formats?kind=sheet
nest_run_budget_too_long 422 group_budget_s asks for more computing time per material and thickness than your organization may (details.group_budget_s, details.max_group_budget_s). Nothing was queued; ask for the ceiling or less
nest_run_in_flight 409 A job nest of your organization is still queued or running (details.run_id, details.status): one at a time. Nothing was queued; wait until it ends, or cancel it
nest_run_too_many_nests 422 The selection would nest more materials and thicknesses than one job nest takes (details.nests, details.max_nests). Nothing was queued; split it
nest_run_too_many_drafts 409 This month's runs are used, so the start would be kept as a draft — and your organization already keeps as many drafts as it may (details.drafts, details.max_drafts). Start one once runs are available, or cancel one, then start this one
nest_run_not_draft 409 POST /nest-runs/{run_id}/start named a job nest that is not a draft (details.status): it was started already or has ended
nest_run_not_cancellable 409 The job nest has already ended (details.status)
nest_run_group_not_found 404 No material and thickness with this id in the job nest (details.group_id)
nest_run_revision_not_found 404 The material and thickness has no revision with this number (details.revision); GET …/revisions lists them
nest_run_not_editable 409 The job nest's sheets cannot be adjusted or released now: details.reason is not_ended (wait until it ends, or cancel it) or group_not_solved (this material and thickness is not nested)
nest_run_released 409 The job nest was released (details.counted_at): its stock is booked, its sheets are frozen, and it cannot be a simulation again. Start a new job nest to lay them out differently
nest_run_remnant_unavailable 409 A remnant the job nest planned to cut is no longer in stock as planned (details.remnant_id, details.pieces, details.available, details.status). Nothing was booked; adjust the sheets or nest again, then release
nest_run_internal 409 The job nest is an assembly calculation's own (details.run_id): it prices the assembly's sheet components, so it is never released, kept a simulation, cancelled, started or given a new layout. Nothing was changed; start a job nest of the parts to cut them together
nest_layout_invalid 422 The posted layout cannot be cut, or breaks your shop practice: details is the check POST …/layout-check answers — every sheet with its clashes (a, b, gap_mm), outside placements, unknown_lines, whether its container is unknown, and against_shop_practice (placement, reason — grain_direction with the allowed_rot_deg that keep it, or mirroring); missing and extra lines (stated, never a refusal: a layout short of a line's quantity or over it is stored and priced on the copies it cuts); containers_unknown; remnants_over. Nothing was stored
nest_line_not_found 404 No line with this id in the job nest (details.line_id)
nest_line_on_sheets 409 The job nest's active sheets still place the line (details.line_id, details.placed): take its copies off the sheets and save them as a revision, then take the line out
nest_line_priced 409 A calculation was priced on the line (details.line_id, details.calculations), so it stays in the job nest
nest_run_nothing_to_export 409 No material and thickness of the job nest is solved yet (details.status), so there is no sheet to export. Wait until one is, or until it ends
nest_run_sheet_not_found 404 sheet names no sheet of the job nest (details.sheet). The nest plan (format=json) names every sheet as <group>:<index>
sheet_layout_not_found 404 The calculation's price stored no sheet layout to export
sheet_nest_preview_unavailable 409 POST /calculations/{id}/sheet-nest/preview: the calculation cannot be laid out on another sheet. details.reason is not_succeeded, not_sheet (not cut from sheet stock), no_stored_layout (its price stored no layout), material_provided (the customer supplies the material) or missing_facts
stock_format_out_of_bounds 422 A sheet size of your own cannot be ordered: details.reason is below_minimum (details.min_mm), above_maximum (details.max_mm), machine_cannot_hold (details.machine_bed_mm, the selected machine's bed) or off_increment_grid (details.increment_mm, the environment's cut-to-size ordering step). details.length_mm and details.width_mm restate the size
nest_export_timeout 503 The cutting file is still being built: the file route waited 60 seconds. Ask again after details.retry_after_s seconds (also the Retry-After header), or ask the …/export-url route, which answers at once (202 while the file is built)
nest_export_failed 500 The cutting file could not be built (409: the layout changed while it was built). Ask again with fresh=true to build it again
nest_export_unavailable 503 Cutting files cannot be queued for building right now. Ask again in a moment
nest_item_unresolved 422 nest_item_id names no job-nest line that can price this calculation. details.reason says why: not_found (no line of your organization has this id), other_environment (it was nested in another costing environment), not_solved (its job nest has not solved its material and thickness), not_placed (no copy of it is on a sheet), other_part_revision (it was nested as another part revision — every upload creates a new one, so the upload forms refuse every line), other_currency (it was priced in another currency than the environment's), material_provided (the customer supplies the material) or stock_format_pinned (a stock format is pinned too; send one of the two). On a re-run that kept its parent's line, details.derived_from_calculation_id names the parent; send nest_item_id null to price the part on its own sheet. Nothing was stored
remnant_not_found 404 No remnant with this id in your organization
remnant_not_in_stock 409 The remnant can't be scrapped: a running job nest holds it, it was consumed or scrapped already, or fewer pieces are in stock than you named (details.status, details.quantity)
remnant_unresolved 422 The environment or the material grade you named is not one of your organization's (details.env_id or details.material_grade_id)
remnant_code_invalid 422 GET /remnants/by-code/{code}: the code is not a remnant code — hexadecimal digits of the id, at least 8 (details.min_digits), or the whole id
remnant_code_ambiguous 409 More than one remnant of your organization begins with this code (details.matches). Scan or type more of the id; a label's 12-digit code is unambiguous in practice
nest_run_no_rests 404 GET /nest-runs/{run_id}/remnant-labels: the job nest put no rest into stock — it is not released yet (details.released), or its sheets leave no reusable rest, or the environment does not credit remnants

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

Cost sheet, what-if preview and scenarios

Every refusal on GET /calculations/{id}/cost-sheet, POST /calculations/{id}/preview and /calculations/{id}/scenarios carries its code in error.details.reason and the sentence below in error.message. A preview refusal caused by one of the adjustments comes back as invalid_adjustments with the per-adjustment codes nested in error.details.refused[] (unknown_factor, out_of_band with band, not_a_number, unstamped_key, not_an_adjustment, no_channel_for_factor).

Reason Status Meaning
calculation_not_priced 409 The calculation has no price yet (queued, running, failed or cancelled), so there is nothing to read or change at any lot size. Run it first
priced_under_earlier_model 409 The calculation was priced by an earlier version of the costing model, so a new price worked out from it would move by everything that version changed, not only by your change; create a new calculation for the part revision and run it — that one can be adjusted
quantity_not_on_curve 404 The calculation was not priced at that quantity and records no closed form to restate it from; read it at a quantity its lot-size curve lists
kernel_corrupt 404 The calculation records a closed form that cannot be read; the curve's own quantities still answer — run it again to rebuild the rest
cost_sheet_unavailable 409 The run recorded too little to itemise its price at that quantity. Run it again for a full cost sheet
invalid_adjustments 422 Nothing was priced: at least one change cannot be — details.refused[] names each one and why
too_many_adjustments 422 More changes than one what-if answers exactly; details.limit is the most at once
lot_size_out_of_band 422 The quantity is outside what a what-if prices; details.limit is the ceiling (the same one every calculation accepts)
nothing_to_preview 422 Nothing was changed; the calculation's own cost sheet already carries that answer
machine_key_unscoped 422 The calculation states a machine rate per machine; name the machine (details.stamped_keys lists the keys its sheet carries)
route_machines_unknown 409 A machine rate can only be moved for a machine the calculation was priced on; details.keys names the ones that could not be matched
geometry_not_reusable 409 The part data the calculation was priced from is no longer on hand; run the calculation again to make it adjustable
geometry_expired 410 The part data was kept for a limited time and has since been removed (details.expired_at); run the calculation again
inputs_changed_since_run 409 The part's files or its applied corrections changed after the calculation ran (details.roles, details.corrections, details.latest_at), so a what-if would no longer match its figures; create a new calculation for the part revision and run it to include the change
environment_changed_since_run 409 The costing environment changed after the calculation ran (details.latest_at, the same time the calculation's environment_changed_since_run states), so a what-if would mix that change into its answer; create a new calculation for the part revision and run it — that one can be adjusted. A saved scenario records the same reason
geometry_store_disabled 501 What-if pricing is not switched on in this deployment yet; an administrator can turn it on
sheet_unavailable 409 The change produced no priceable result; try a different value
not_priceable 409 The calculation could not be priced under the environment's current settings (check its bought-in operation prices), then run it again
compute_budget_exceeded 503 Working the change out took longer than one request may spend; ask for fewer changes at once or a smaller quantity
too_many_scenarios 409 The calculation already holds the most scenarios one may keep (details.limit); delete one first
nothing_to_change 422 A scenario update named neither a new name nor new adjustments

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