ARCNM

API reference

Calculations

The Calculations API runs a should-cost calculation on a part and returns the priced result — quote a stored revision, upload-and-quote a file in one call…

The Calculations API runs a should-cost calculation on a part and returns the priced result — quote a stored revision, upload-and-quote a file in one call, then list, fetch, or bulk-manage runs.

Auto-generated from the public OpenAPI spec — this page never drifts from the running API. Base URL https://api.arcnm.io. Authenticate with the X-API-Key header (see Authentication).

List Calculations

GET /api/v1/calculations

List calculations for the tenant.

Paged newest-first by default with a slim projection — no analytics blob, to keep the list response cheap. Walk the whole collection by following next_cursor (or the Link header) until has_more is false; count is this page's size, never the total.

To reconcile a bulk run against your own records, page with order=asc and a created_after bound: rows come oldest-first, so calculations submitted while you are still walking land after your position instead of shifting rows under it.

Optional part_id narrows the result to a single part. Optional ids turns this into the bulk polling surface: poll one request per sweep, not one per calculation.

Paginated. Pass cursor (from the previous response) to fetch the next page; limit caps the page size.

Parameters

Name In Type Required Description
status_filter query string[] no Filter by lifecycle status (e.g. queued, running, succeeded, failed, cancelled). Repeatable; several values are OR-ed.
part_id query string no Identifier of the part.
batch_id query string no Narrow to one batch's calculations — e.g. every cell of a multi-environment comparison grid. Pair with the batch comparison endpoint, which returns the pivoted matrix.
material_grade_id query string[] no Only runs priced against one of these material grades. Repeatable; several values are OR-ed, so passing every value is the same as passing none.
sort query string no Ordering key. Rows with no value yet (an in-flight run has no unit cost) sort last in both directions. Ignored when ids is set. offer_price orders on the price each calculation headlines and the comparison ranks on (the item's offer_price; an assembly priced only in part states none and sorts last); unit_cost on the cost before the cost-sheet surcharges. created_at, lot_size and annual_volume support cursor; unit_cost, offer_price and finished_at are null until a run settles, so a cursor over them cannot reach every row and is rejected — those three return the first page only.
include_facets query boolean no Also return facets: status and material values with counts, each computed over everything the OTHER active filters allow.
ids query string no Comma-separated calculation IDs (max 500). THE polling surface for bulk clients: one weight-1 sweep returns the status of every listed calculation, instead of N weight-1 detail requests that exhaust the per-org rate budget (self-DoS). When set, limit and ordering are ignored and every matching row is returned.
parent_calculation_id query string no Only the component calculations of this assembly. By default the list contains top-level calculations only — assembly components are reachable through their parent.
include_components query boolean no With part_id: also list the calculations this part received as a component of an assembly (each at the lot the assembly implied). Ignored without part_id.
cursor query string no Opaque position token from the previous page's next_cursor (or the Link / X-Next-Cursor response header). Omit it for the first page. Keep every other query parameter identical for the whole walk — a cursor replayed against different filters is rejected.
limit query integer no Maximum rows to return in one page.
order query asc | desc no Sort direction over the collection's ordering key. Use asc to reconcile a batch: rows come oldest-first, so work created while you page lands after your position instead of shifting rows under it.
created_after query string no Only rows created at or after this instant (RFC 3339, e.g. 2026-07-20T09:00:00Z). Inclusive.
created_before query string no Only rows created strictly before this instant (RFC 3339). Exclusive, so an after/before pair tiles a range without overlap.
include_total query boolean no Also return the total number of rows matching the query, across all pages. Off by default because it costs an extra scan; has_more is the cheap way to know whether to keep paging.

Request

curl -X GET https://api.arcnm.io/api/v1/calculations \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/calculations",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
count integer Number of calculations on THIS page.
facets object Present only when include_facets is set. Maps each filter name to its selectable values with counts.
has_more boolean Whether more rows match beyond this page. A full page with has_more: false is the end of the collection.
items CalculationListItem[] The calculations on this page, newest first.
next_cursor string Position token for the next page — pass it back as cursor. Null on the last page.
total integer Total rows matching the query across all pages. Null unless include_total=true was requested.

Example response

{
  "count": 0,
  "facets": {},
  "has_more": false,
  "items": [
    {
      "assembly_role": "string",
      "assembly_state": "string",
      "attempt_count": 0,
      "batch_id": "string",
      "costing_environment_id": "string",
      "created_at": "string",
      "currency": "EUR",
      "derived_from_calculation_id": "string",
      "engine": "arcanum",
      "environment_changed_since_run": {
        "latest_at": "string"
      },
      "error": "string",
      "finished_at": "string",
      "full_stock_charge": "auto",
      "id": "string",
      "inputs_changed_since_run": {
        "corrections": 0,
        "latest_at": "string",
        "roles": []
      },
      "lot_size": 50,
      "material_grade_id": "string",
      "material_ref": "1.4301",
      "max_attempts": 0,
      "name": "string",
      "nest_item_id": "string",
      "nest_revision": 0,
      "nestable": false,
      "offer_price": 0,
      "part_id": "string",
      "part_revision_id": "string",
      "revision_code": "string",
      "started_at": "string",
      "status": "string",
      "stock_format_id": "string",
      "stock_format_mm": {
        "length_mm": 0,
        "width_mm": 0
      },
      "unit_cost": 12.84
    }
  ],
  "next_cursor": "string",
  "total": 0
}

Create a calculation (queued)

POST /api/v1/calculations

Create a calculation for a part revision in a costing environment. Does NOT enqueue — call POST /calculations/{id}/run to price it.

One endpoint covers every part. An attached 2D drawing is read automatically — there is nothing to configure.

Creates exactly one calculation: costing_environment_ids with more than one entry is rejected here — fan a comparison out via POST /calculations/batch, /quote or /upload-and-quote.

Request body (application/json)

Field Type Required Description
annual_volume integer no Expected yearly quantity, used for amortizing setup over the run (1 to 1,000,000,000).
costing_environment_id string no UUID of the costing environment (machine rates, region) to price against. Omit to use your organization's default environment (the auto-provisioned default baseline).
costing_environment_ids string[] no Price this part in several environments at once, in comparison order. Each environment becomes one calculation and counts as one against the plan quota. Mutually exclusive with costing_environment_id; duplicates are collapsed preserving first occurrence. The response then carries batch_id and environment_runs.
currency string no ISO 4217 currency code for the quote; defaults to the costing environment's currency when omitted.
derived_from_calculation_id string no The calculation this one is re-run from — pass it when pricing again after a drawing was attached or from a result's own page, so the new calculation records what it supersedes. Must name a calculation of your organization; the new calculation is its own row and counts as one.
engine string no Pricing engine selector; retained for back-compat and always normalized to the sole engine.
full_stock_charge auto | full | full_no_credit | share no How the started sheet or bar of the lot is charged: auto applies the environment's full_stock_threshold (the lot pays the whole piece once its parts fill that share of it, the unused cells credited as scrap where scrap is credited); full charges the whole piece at any lot; full_no_credit charges it whole with no scrap credit on the unused cells; share charges only the lot's share at any lot. A lot of whole pieces, or a piece holding one part, is charged the same under every word. On a re-run (derived_from_calculation_id), omitting it keeps the word of the calculation re-run.
language string no BCP 47 language for the calculation's captured context (e.g. 'de', 'en'). Part of the calculation's cache identity.
lot_size integer no Number of identical parts produced per batch (1 to 1,000,000,000).
material_grade_id string no UUID of a resolved material grade; takes precedence over material_ref when both are supplied.
material_is_provided boolean no True when the customer supplies the raw material (beigestellt) — the material cost line is set to zero; machining, setup and overheads are billed normally.
material_ref string no Free-form material reference (URN, Werkstoffnummer, AISI/SAE code, trade name, or your SKU); resolved to a material grade. Unresolvable values are rejected with 422 material_unresolved plus ranked candidates. Omit to price the material read from the part's drawing (or the environment default).
name string no Optional human-readable label; defaults to a timestamped name when omitted.
nest_item_id string no Price this calculation's material on its part's line of a job nest — the id of a line (items[].id) of a job nest that has solved it. The material is then the line's share of the sheets the job nest cut it from together with other parts, less its share of what their skeleton and reusable rests earn back; everything else is priced as usual. The line must be of your organization, of this costing environment and of this part revision, and its job nest must have solved it; otherwise the request is rejected with 422 nest_item_unresolved, details.reason saying why. It cannot be combined with stock_format_id or with customer-supplied material. An upload creates a new part revision, so the upload forms reject every line. A line of the sheets an assembly shares among its own components prices that assembly only: naming one is rejected as not_found, and a re-run of such a component prices it on its own sheet. Omit it to price the part on its own sheet. On a re-run (derived_from_calculation_id), omitting it keeps the line of the calculation re-run; send null to price the part on its own sheet.
part_revision_id string yes UUID of the part revision to price.
provided_stock_kind none | near_net_profile | near_net_casting | sawn_blank no Shape of the provided stock. 'near_net_profile' (extruded profile) additionally scopes the process plan to the features the profile does not already provide.
region string no Pricing region override; defaults to the costing environment's region when omitted.
stock_format_id string no Price this calculation on one stock format — the id of a format from GET /stock-formats. An id names the format, not only the version it came from: when the format has been changed since, the version current on the day the calculation is created is used. An id your organization cannot price with today (unknown, retired or hidden) is rejected with 422 stock_format_unresolved. When the part cannot be cut from that format — too small, not bought in the part's thickness or material, or one the machine cannot load — or is not cut from sheet at all (milled, turned or bought finished), the calculation is priced as if none had been chosen, and analytics.pipeline_notes says why (stock_format_pin_unavailable). Omit it to price on the format that uses the least material per part. On a re-run (derived_from_calculation_id), omitting it keeps the format of the calculation re-run; send null to price without one.
stock_format_mm StockFormatMm no Price this calculation on a sheet of this size — length and width in mm — instead of a stock format from GET /stock-formats: the part is laid out on that one sheet, cut to size by your supplier, and the purchased stock says format_source custom. It cannot be combined with stock_format_id or nest_item_id. When the part does not fit the sheet, or is not cut from sheet stock at all, the calculation is priced as if none had been stated and analytics.pipeline_notes says why. On a re-run (derived_from_calculation_id), omitting both it and stock_format_id keeps the size or format of the calculation re-run; send null to price without one.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "part_revision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  }'
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "part_revision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    },
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "part_revision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  }),
})
const data = await resp.json()

Responses

Status Description
201 Successful Response
422 Validation Error — or a refused reference: material_unresolved (the material names no grade in the catalogue), stock_format_unresolved (stock_format_id names no stock format your organization prices with today) or nest_item_unresolved (nest_item_id names no job-nest line that can price this calculation; details.reason says why).

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 201

Field Type Description
batch_id string Groups the runs of a multi-environment quote; compare them via GET /calculations/batches/{batch_id}/comparison. Null for a single-environment quote.
costing_environment_id string UUID of the costing environment this calculation is priced against — the one you supplied, or your organization's default when you omitted it. Always echoed so a caller that omitted the field can tell which rates the quote used.
coverage CalculationCoverage Populated only when status='blocked': the calc was created but NOT enqueued because the org has no included calculations left (or is payment-blocked). Explains how many parts can still run and how to resume this one. null whenever the calc was enqueued.
environment_runs EnvironmentRunStatus[] All runs of a multi-environment quote, one per requested environment in comparison order. The top-level fields describe the first created run; poll each run (or the batch comparison) for results. Null for a single-environment quote.
error string Why the calculation failed, when this response already carries a terminal failed status — a run refused at submit time, before any worker. Null in every other case, including a normal enqueue: a run that fails LATER reports through the calculation itself, not through the response that started it.
eta_basis string How eta_seconds was derived, so the number is auditable: org_rate — divided by YOUR organization's own measured completions per second over the recent window; platform_default — your organization has too little recent history, so the estimate comes from your plan's parallelism and the platform's measured service time. Null when no ETA was computed.
eta_seconds number Estimated wait before this calculation reaches a result, derived from its queue position divided by a measured completion rate (advisory); null when not enqueued. See eta_basis for which rate was used.
id string UUID of the calculation.
queue_position integer 1-based position among YOUR organization's own queued and running calculations at enqueue time (backpressure hint); null when not enqueued. With tenant fair scheduling active your parts run in parallel with other customers, so another tenant's backlog does not push you back.
status string Current lifecycle state: 'draft' — created by POST /calculations but not yet submitted; it has no worker and no job and is priced only when you call POST /run — then queued, running, succeeded, failed, cancelled, timed_out, or 'blocked' — created but NOT started because the org is out of included calculations (see coverage). A blocked calc has no worker and no job; resume it with POST /run after topping up.

Example response

{
  "batch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "coverage": {
    "included": 0,
    "message": "string",
    "reason": "string",
    "remaining": 0,
    "tier_slug": "string",
    "upgrade_url": "string",
    "used": 0,
    "window": "string"
  },
  "environment_runs": [
    {
      "calculation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "reason": "string",
      "status": "string"
    }
  ],
  "error": "string",
  "eta_basis": "string",
  "eta_seconds": 0,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "queue_position": 0,
  "status": "string"
}

Delete Calculation

DELETE /api/v1/calculations/{calculation_id}

Hard-delete a calculation row.

A non-terminal row is first cancelled (which releases the wallet hold) so the worker can no longer transition it; the row itself is then removed. Use Cancel if you want the audit trail to persist.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.

Request

curl -X DELETE https://api.arcnm.io/api/v1/calculations/{calculation_id} \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.delete(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}", {
  method: "DELETE",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
message string Human-readable confirmation that the calculation was deleted.

Example response

{
  "message": "string"
}

Get Calculation

GET /api/v1/calculations/{calculation_id}

Read one calculation in full: its status, the part, revision and costing environment it prices, its inputs such as lot size and material, and its costed result.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.

Request

curl -X GET https://api.arcnm.io/api/v1/calculations/{calculation_id} \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
analytics PublicCalculationAnalytics Cost breakdown and value-engineering detail for a priced calculation; null until the calculation succeeds.
annual_volume integer Expected yearly quantity used to amortize setup cost.
assembly_context object For a component: its position, quantity per assembly, effective lot, cost share and a link back to the assembly. Null otherwise.
assembly_role string 'assembly' on a decomposed multi-part calculation, 'component' on one of its priced parts, null on an ordinary single part.
assembly_state string Assembly roll-up progress state; null on a single part.
confidence object Quote-trust signal: {state: green
costing_environment_id string UUID of the costing environment used for pricing.
coverage CalculationCoverage Populated only when status='blocked': why the calc is parked and how to resume it after topping up. null for every other state. Surfaced as a typed field (not just inside analytics) so API/MCP clients see it without the analytics allow-list stripping it.
created_at string ISO 8601 timestamp when the calculation was created.
currency string ISO 4217 currency code for the cost figures.
dataset_link_id string UUID of the dataset link (source geometry / drawing set) this calculation used. Echoed so a recalculate reuses the same dataset instead of the environment default.
derived_from_calculation_id string UUID of the calculation this one was re-run from (priced again after a drawing was attached, or from its results page); null on a first run. The re-run is its own calculation and this names what it supersedes.
engine string Pricing engine used for this calculation.
environment_changed_since_run EnvironmentChangedSinceRun Null when this result was priced on its costing environment as it stands. Otherwise the environment was edited after this run read it (latest_at); the edit may or may not move the price — a new calculation prices with the current setup, this result does not.
error string Failure message; null unless the run failed.
finished_at string ISO 8601 timestamp when the run finished; null before it completes.
full_stock_charge string The lot rule's override this calculation was priced under — full_stock_charge as requested, or kept from the calculation it re-runs: auto (the environment's threshold), full, full_no_credit or share. What the rule did at the ordered lot is material_detail.lot_rule on the cost sheet.
id string UUID of the calculation.
inputs_changed_since_run InputsChangedSinceRun Null when this result reflects every input on its part revision. Otherwise the roles (drawing_2d, mesh, rfq_text) whose active file was attached after the run read its inputs, and the number of corrections applied to the part since — the next run reads them, this result does not.
language string BCP 47 language this calculation's context was captured in. Echoed so a recalculate keeps it.
lot_size integer Number of identical parts produced per batch.
material_grade_id string UUID of the linked material grade; null when no grade is set.
material_is_provided boolean True when the customer supplies the raw material (beigestellt) and the material cost line is zero. Echoed so a recalculate keeps it instead of billing the material.
material_ref string Material reference (URN) of the linked grade; null when no grade is set.
name string Human-readable label for the calculation.
needs_human_review boolean True when the analysis flagged this calculation for human review (low confidence, prompt-injection suspicion, or conflicting sources); surfaced at the top level so API consumers can gate on it.
nest_item_id string The job-nest line this calculation's material was priced on — nest_item_id as requested, or kept from the calculation it re-runs; null when the part was priced on its own sheet. An assembly component priced on the sheets its assembly shares states it with the Nesting add-on only.
nest_revision integer The revision of that line's sheets this calculation reads: the one current when it was created, or its parent's on a re-run. A later revision of the job nest never changes it.
nestable boolean Whether this calculation can join a job nest: a finished sheet-metal price, with its layout stored, on material the shop buys. False otherwise.
parent_calculation_id string For a component: the assembly calculation it belongs to.
part_id string UUID of the part this calculation belongs to.
part_revision_id string UUID of the part revision that was priced.
provided_stock_kind none | near_net_profile | near_net_casting | sawn_blank Shape of the customer-supplied stock; 'none' when the material is bought. Echoed so a recalculate keeps it.
region string Region key used to price this calc; null when the environment's default region was used. Echoed so a recalculate preserves it instead of falling back to the environment default.
review_reasons string[] Plain-language reasons the calculation was flagged for review; empty when not flagged.
setup_cost number One-time setup cost of the lot in the quote's currency: the cost sheet's setup line times the lot size, on the same basis as unit_cost. Like unit_cost it is stated before the cost-sheet surcharges; null until pricing completes.
started_at string ISO 8601 timestamp when the run started; null before it begins. A calculation answered from an identical earlier one (not charged again) states when it was answered: its figures are that one's, priced from the same files, corrections and environment.
status string Current lifecycle state: queued, running, succeeded, failed, cancelled, timed_out, or 'blocked' (created but not started — the org is out of included calculations; see coverage).
stock_format_id string The stock format this calculation was asked to be priced on — stock_format_id as requested, or kept from the calculation it re-runs — as the version current when it was created; null when none was asked for. Whether that format priced the part is in analytics.pipeline_notes: stock_format_pin_unavailable when it did not.
stock_format_mm StockFormatMm The sheet size this calculation was asked to be priced on — stock_format_mm as requested, or kept from the calculation it re-runs; null when none was stated.
surface_treatments string[] Surface treatments stated when this calculation was created (only POST /calculations/batch accepts them); empty when none were. Part of its cache identity, not of its price.
total_cost number unit_cost for the full lot (unit_cost x lot_size) in the quote's currency, before the cost-sheet surcharges; null until pricing completes. The lot at the offer price is analytics.offer_price x lot_size.
total_time_s number Total production time for the lot in seconds: one-time setup plus lot size × per-part time; null until pricing completes.
unit_cost number Unit cost per part in the quote's currency, BEFORE the cost-sheet surcharges the environment states (overheads, administration and selling, margin, ...); null until pricing completes. The headline price is analytics.offer_price — equal to unit_cost where the environment states no surcharges, the figure the comparison ranks on and the list returns as offer_price and sorts on with sort=offer_price.
unit_time_s number Production time per part in seconds, including per-part shares of programming and inspection but excluding the one-time lot setup; null until pricing completes.

Example response

{
  "analytics": {
    "assembly": null,
    "assumptions": null,
    "cost_decomposition": null,
    "cost_drivers": null,
    "cost_sheet": null,
    "extraction": null,
    "lot_size_curve": null,
    "material_resolution": null,
    "offer_price": null,
    "optimization": null,
    "part_requirements": null,
    "pipeline_notes": null,
    "process_plan": null,
    "review": null,
    "secondary_route": null,
    "selection": null,
    "selection_failure": null,
    "time_breakdown": null,
    "unit_cost_interval": null
  },
  "annual_volume": 500,
  "assembly_context": {},
  "assembly_role": "string",
  "assembly_state": "string",
  "confidence": {},
  "costing_environment_id": "string",
  "coverage": {
    "included": 0,
    "message": "string",
    "reason": "string",
    "remaining": 0,
    "tier_slug": "string",
    "upgrade_url": "string",
    "used": 0,
    "window": "string"
  },
  "created_at": "string",
  "currency": "EUR",
  "dataset_link_id": "string",
  "derived_from_calculation_id": "string",
  "engine": "arcanum",
  "environment_changed_since_run": {
    "latest_at": "string"
  },
  "error": "string",
  "finished_at": "string",
  "full_stock_charge": "auto",
  "id": "string",
  "inputs_changed_since_run": {
    "corrections": 0,
    "latest_at": "string",
    "roles": [
      "string"
    ]
  },
  "language": "string",
  "lot_size": 50,
  "material_grade_id": "string",
  "material_is_provided": false,
  "material_ref": "1.4301",
  "name": "string",
  "needs_human_review": true,
  "nest_item_id": "string",
  "nest_revision": 0,
  "nestable": false,
  "parent_calculation_id": "string",
  "part_id": "string",
  "part_revision_id": "string",
  "provided_stock_kind": "none",
  "region": "EU",
  "review_reasons": [
    "string"
  ],
  "setup_cost": 52.5,
  "started_at": "string",
  "status": "string",
  "stock_format_id": "string",
  "stock_format_mm": {
    "length_mm": 0,
    "width_mm": 0
  },
  "surface_treatments": [
    "string"
  ],
  "total_cost": 642,
  "total_time_s": 184,
  "unit_cost": 12.84,
  "unit_time_s": 184
}

Assembly route — the work on the joined body, in order

GET /api/v1/calculations/{calculation_id}/assembly-route

The precedence-ordered assembly operations with cost + evidence.

Steps that exist but are unpriced are included with their disclosures — never omitted. Empty (pending: true) until the aggregation has run. 404 for a non-assembly calculation.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.

Request

curl -X GET https://api.arcnm.io/api/v1/calculations/{calculation_id}/assembly-route \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/assembly-route",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/assembly-route", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
calculation_id string
currency string
pending boolean True until the assembly roll-up has produced the route.
steps AssemblyRouteStepOut[]

Example response

{
  "calculation_id": "string",
  "currency": "EUR",
  "pending": true,
  "steps": [
    {
      "cost_per_unit": 0,
      "din_codes": [
        "string"
      ],
      "din_group": "string",
      "disclosures": [
        "string"
      ],
      "evidence": {},
      "key": "string",
      "lot_minimum_eur": 0,
      "priced": true,
      "rank": 0,
      "time_s_per_unit": 0
    }
  ]
}

Assembly bill of materials with live per-component status

GET /api/v1/calculations/{calculation_id}/bom

The assembly's component table, the polling surface while parts price in.

Rows come from the dispatch-time BOM (position, name, quantity, effective lot) overlaid with each component calculation's LIVE status and cost — so the table renders progressively as children complete, without one request per row. 404 for a calculation that is not an assembly.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.

Request

curl -X GET https://api.arcnm.io/api/v1/calculations/{calculation_id}/bom \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/bom",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/bom", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
assembly_state string Assembly progress state.
calculation_id string
capped boolean True when pricing was capped to the largest parts.
completeness AssemblyCompletenessOut Whether the roll-up is a price or a lower bound, and why. The one place a caller — human or agent — can tell an intended product limit apart from a gap without parsing prose.
currency string
drawings AssemblyDrawingsOut The drawing pack: how many components carry a drawing and which files still need a decision.
lot_size integer
n_occurrences integer Placed parts in the assembly.
n_sub_assemblies integer Sub-assembly nodes the CAD file states below the root (0 on a flat file).
n_unique integer Unique parts.
progress AssemblyBomProgress
rollup AssemblyRollupOut
rows AssemblyBomRowOut[]
shared_sheets AssemblySharedSheetsOut The sheet components nested together on shared sheets and what each cost alone and together; null before nesting has started.
status string The assembly calculation's own status.

Example response

{
  "assembly_state": "string",
  "calculation_id": "string",
  "capped": true,
  "completeness": {
    "n_pending": 0,
    "n_priced": 0,
    "n_unique": 0,
    "n_unpriced": 0,
    "priced_complete": true,
    "total_is_lower_bound": true,
    "unpriced": [
      {
        "by_design": true,
        "evidence": {},
        "label": "string",
        "position": 0,
        "reason_code": "string",
        "remedy": "string"
      }
    ]
  },
  "currency": "EUR",
  "drawings": {
    "assembly": {
      "added_at": "string",
      "basis": "string",
      "candidates": [
        {}
      ],
      "component_key": "string",
      "data_source_id": "string",
      "drawing_number": "string",
      "filename": "bracket.step",
      "material": "string",
      "note": "string",
      "page_count": 0,
      "position": 0,
      "size_bytes": 204800,
      "status": "string",
      "title": "string"
    },
    "n_files": 0,
    "n_matched": 0,
    "n_unassigned": 0,
    "n_with_drawing": 0,
    "unassigned": [
      {
        "added_at": "string",
        "basis": "string",
        "candidates": [],
        "component_key": "string",
        "data_source_id": "string",
        "drawing_number": "string",
        "filename": "bracket.step",
        "material": "string",
        "note": "string",
        "page_count": 0,
        "position": 0,
        "size_bytes": 204800,
        "status": "string",
        "title": "string"
      }
    ]
  },
  "lot_size": 50,
  "n_occurrences": 0,
  "n_sub_assemblies": 0,
  "n_unique": 0,
  "progress": {
    "done": 0,
    "total": 0
  },
  "rollup": {
    "components_cost": 0,
    "components_time_s": 184,
    "joining_cost": 0,
    "n": 0,
    "ops_cost": 0,
    "overhead": 0,
    "overhead_fixed": 0,
    "own_time_s": 184,
    "priced_complete": true,
    "setup_cost_amortised": 0,
    "total_cost": 642,
    "total_time_s": 184,
    "unit_cost": 12.84,
    "unit_time_s": 184
  },
  "rows": [
    {
      "assembly_path": [
        "string"
      ],
      "child_calculation_id": "string",
      "child_part_id": "string",
      "component_key": "string",
      "cost_share": 0,
      "designation": "string",
      "drawing": {
        "attached_at": "string",
        "data_source_id": "string",
        "filename": "bracket.step",
        "read_by_run": true
      },
      "effective_lot": 0,
      "extended_cost": 0,
      "label": "string",
      "machine_klass": "string",
      "machine_name": "string",
      "material_display_code": "string",
      "material_grade_id": "string",
      "needs_human_review": true,
      "occurrence_indices": [
        0
      ],
      "override": {},
      "position": 0,
      "price_notes": [
        "string"
      ],
      "price_source": "string",
      "product_id": "string",
      "qty_per": 0,
      "scrap_pct": 0,
      "standard_part": {
        "nominal_m": 0,
        "role": "primary",
        "standard": "string"
      },
      "status": "string",
      "treatments_covered": [
        {}
      ],
      "unit_cost": 12.84,
      "unit_time_s": 184,
      "unpriced_by_design": true,
      "unpriced_reason": "string",
      "unpriced_remedy": "string"
    }
  ],
  "shared_sheets": {
    "groups": [
      {
        "components": [],
        "cost_alone": 0,
        "cost_together": 0,
        "gauge_mm": 0,
        "material_category": "string",
        "material_display_code": "string",
        "saving": 0
      }
    ],
    "reason": "string",
    "state": "solving"
  },
  "status": "string"
}

Cancel Calculation

POST /api/v1/calculations/{calculation_id}/cancel

Cancel a queued / running / polling calculation.

Idempotent: a terminal row is returned untouched. The wallet hold is released as part of the transition so a cancelled calc never debits the org's balance.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/{calculation_id}/cancel \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/cancel",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/cancel", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
batch_id string Groups the runs of a multi-environment quote; compare them via GET /calculations/batches/{batch_id}/comparison. Null for a single-environment quote.
costing_environment_id string UUID of the costing environment this calculation is priced against — the one you supplied, or your organization's default when you omitted it. Always echoed so a caller that omitted the field can tell which rates the quote used.
coverage CalculationCoverage Populated only when status='blocked': the calc was created but NOT enqueued because the org has no included calculations left (or is payment-blocked). Explains how many parts can still run and how to resume this one. null whenever the calc was enqueued.
environment_runs EnvironmentRunStatus[] All runs of a multi-environment quote, one per requested environment in comparison order. The top-level fields describe the first created run; poll each run (or the batch comparison) for results. Null for a single-environment quote.
error string Why the calculation failed, when this response already carries a terminal failed status — a run refused at submit time, before any worker. Null in every other case, including a normal enqueue: a run that fails LATER reports through the calculation itself, not through the response that started it.
eta_basis string How eta_seconds was derived, so the number is auditable: org_rate — divided by YOUR organization's own measured completions per second over the recent window; platform_default — your organization has too little recent history, so the estimate comes from your plan's parallelism and the platform's measured service time. Null when no ETA was computed.
eta_seconds number Estimated wait before this calculation reaches a result, derived from its queue position divided by a measured completion rate (advisory); null when not enqueued. See eta_basis for which rate was used.
id string UUID of the calculation.
queue_position integer 1-based position among YOUR organization's own queued and running calculations at enqueue time (backpressure hint); null when not enqueued. With tenant fair scheduling active your parts run in parallel with other customers, so another tenant's backlog does not push you back.
status string Current lifecycle state: 'draft' — created by POST /calculations but not yet submitted; it has no worker and no job and is priced only when you call POST /run — then queued, running, succeeded, failed, cancelled, timed_out, or 'blocked' — created but NOT started because the org is out of included calculations (see coverage). A blocked calc has no worker and no job; resume it with POST /run after topping up.

Example response

{
  "batch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "coverage": {
    "included": 0,
    "message": "string",
    "reason": "string",
    "remaining": 0,
    "tier_slug": "string",
    "upgrade_url": "string",
    "used": 0,
    "window": "string"
  },
  "environment_runs": [
    {
      "calculation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "reason": "string",
      "status": "string"
    }
  ],
  "error": "string",
  "eta_basis": "string",
  "eta_seconds": 0,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "queue_position": 0,
  "status": "string"
}

Override one assembly component (material, quantity, purchased)

PATCH /api/v1/calculations/{calculation_id}/components/{component_key}

Per-component specifiability (§9).

Overridable: material, provided (beigestellt) stock, qty_per, scrap_pct, and purchased (a bought-in standard part priced from its catalogue price — the highest-value override in practice). Extracted geometry and recognised features are NOT overridable here; disagreement goes through the correction/labeling surface.

A quantity/material change re-runs exactly that component and then the roll-up; a purchased flag re-runs the roll-up only. The override is keyed by the component's stable key, so it survives re-upload of the same assembly.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.
component_key path string yes

Request body (application/json)

Field Type Required Description
clear boolean no Remove the override.
dry_run boolean no Preview the invalidation set only.
material_grade_id string no Override this component's material grade.
material_is_provided boolean no Customer-supplied (beigestellt) stock.
provided_stock_kind string no Kind of the provided stock.
purchased boolean no Bought-in standard part: price from the catalogue price below instead of manufacturing.
purchased_unit_price number no Catalogue price per piece.
qty_per number no Corrected occurrences per assembly (1 … 10 000).
scrap_pct number no Scrap fraction added to this component's lot.

Request

curl -X PATCH https://api.arcnm.io/api/v1/calculations/{calculation_id}/components/{component_key} \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "clear": false,
    "dry_run": false,
    "material_grade_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "material_is_provided": true,
    "provided_stock_kind": "string",
    "purchased": true,
    "purchased_unit_price": 0,
    "qty_per": 0,
    "scrap_pct": 0
  }'
import requests

resp = requests.patch(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/components/{component_key}",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "clear": False,
        "dry_run": False,
        "material_grade_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "material_is_provided": True,
        "provided_stock_kind": "string",
        "purchased": True,
        "purchased_unit_price": 0,
        "qty_per": 0,
        "scrap_pct": 0
    },
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/components/{component_key}", {
  method: "PATCH",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "clear": false,
    "dry_run": false,
    "material_grade_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "material_is_provided": true,
    "provided_stock_kind": "string",
    "purchased": true,
    "purchased_unit_price": 0,
    "qty_per": 0,
    "scrap_pct": 0
  }),
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
applied boolean False on a dry run.
calculation_id string
component string The component key the override targets.
override object The stored override after this call.
reaggregates boolean Whether the assembly total recomputes.
rerun_child_calculation_ids string[] Component calculations that re-run at the new inputs.

Example response

{
  "applied": true,
  "calculation_id": "string",
  "component": "string",
  "override": {},
  "reaggregates": true,
  "rerun_child_calculation_ids": [
    "string"
  ]
}

Itemised cost sheet for one calculation at one lot size

GET /api/v1/calculations/{calculation_id}/cost-sheet

The cost breakdown as the tree it was built as: one line per cost category, the tiers beneath it, and the time and rate each one is made of. Pass quantity for any lot size a calculation accepts: a quantity on the calculation's lot-size curve is answered from the figures recorded while pricing, any other from the same closed form the curve was built from — see basis. Omit it for the lot the calculation was priced at. Calculations priced before the sheet existed are answered from the figures they stored — see rebuilt.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.
quantity query integer no Lot size to read the sheet at, from 1 up to the largest lot a calculation accepts. A quantity on the lot-size curve (analytics.lot_size_curve.points) is answered from what was recorded while pricing; any other is restated from the calculation's own closed form. A calculation that records no closed form (priced before it existed, or a bought-in part) answers only the quantities on its curve. Defaults to the lot the calculation was priced for.

Request

curl -X GET https://api.arcnm.io/api/v1/calculations/{calculation_id}/cost-sheet \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/cost-sheet",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/cost-sheet", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
basis priced | curve_point | restated Where the quantity's figures come from: 'priced' — the lot the calculation was run for (exact); 'curve_point' — a quantity on its lot-size curve, recorded while pricing; 'restated' — any other quantity, priced from the same closed form the curve was built from (the same arithmetic, so a restated quantity and a curve point can never disagree). Absent on calculations that record no curve.
cost_sheet PublicCostSheet The cost lines, the tiers beneath them, and the time and rate each one is built from, for the lot size asked for.
point PublicLotSizePoint The lot-size curve point for the quantity asked for — its unit cost, offer price and times — when quantity was given.
rebuilt boolean True when the sheet was reconstructed from the rates, times and cost lines this calculation stored rather than recorded while it was priced. A reconstructed sheet carries the cost lines, the surcharge ladder and the tiers that are local to this lot size, but not the per-machine, per-clamping or per-operation detail — that is recorded only while pricing and cannot be derived afterwards. Every amount is the amount charged either way.

Example response

{
  "basis": "priced",
  "cost_sheet": {
    "currency": "EUR",
    "lines": [
      {
        "amount": 0,
        "bucket": "string",
        "children": [],
        "door": {},
        "evidence": {},
        "factors": [],
        "formula_id": "string",
        "label_id": "string",
        "label_params": {},
        "line_kind": "string",
        "op_refs": [],
        "rate": {},
        "terms": [],
        "time_basis": "string",
        "time_s": 184
      }
    ],
    "lot_size": 50,
    "notes": [
      {
        "bucket": "string",
        "code": "string",
        "residual": 0
      }
    ],
    "quantity_basis": 0,
    "reconciled": true,
    "residual": 0,
    "total": 0,
    "version": 0
  },
  "point": {
    "bench_time_s": 184,
    "cost_sheet": {
      "currency": "EUR",
      "lines": [
        {}
      ],
      "lot_size": 50,
      "notes": [
        {}
      ],
      "quantity_basis": 0,
      "reconciled": true,
      "residual": 0,
      "total": 0,
      "version": 0
    },
    "direct_unit_cost": 0,
    "effective_unit_cost": 0,
    "effective_unit_cost_by_scenario": {},
    "machine_id_at_n": "string",
    "machine_ids_at_n": [
      "string"
    ],
    "machine_name_at_n": "string",
    "machine_time_s": 184,
    "offer_price": 0,
    "quantity": 0,
    "replan_at_lot": 0,
    "replan_recommended": true,
    "run_time_s": 184,
    "setup_time_s": 184,
    "unit_cost": 12.84,
    "unit_time_s": 184
  },
  "rebuilt": true
}

Add the drawings for an assembly's parts, all at once

POST /api/v1/calculations/{calculation_id}/drawings

Attach the 2D drawings of an assembly's components in one upload.

Each file is matched to a component by the names both already carry: the drawing number the title block states or the filename against the component's product id from the CAD file, and the title block's part name against the component's designation. A file naming the assembly itself becomes the assembly drawing. A matched file is attached to the component and the component is priced again with it; the assembly total re-settles when the components finish. A file that matches nothing, or more than one component, is kept and listed as unassigned beside the bill of materials for a one-click decision — it is never guessed.

Uploaded before the assembly has been split into its parts, the files wait and are matched at that moment, so every component prices with its drawing on its first run. Uploads are not billed.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.

Request body (multipart/form-data)

Field Type Required Description
files string[] yes The drawings, one file each (PDF, PNG or JPEG) — the assembly's own drawing may be among them. Repeat the field once per file.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/{calculation_id}/drawings \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -F "files=string"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/drawings",
    headers={"X-API-Key": "YOUR_API_KEY"},
    data={
        "files": "string",
    },
)
resp.raise_for_status()
print(resp.json())
const form = new FormData()
form.append("files", "string")

const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/drawings", {
  method: "POST",
  headers: { "X-API-Key": process.env.ARCNM_API_KEY! },
  body: form,
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
applied boolean True when the files were matched against the bill of materials now. False when the assembly has not been split into its parts yet: the files wait and are matched at that moment, so every component prices with its drawing on its first run.
calculation_id string
drawings AssemblyDrawingsOut The whole pack after this call, when a BOM exists.
files AssemblyDrawingEntryOut[] The files this call touched.
n_assembly integer
n_matched integer
n_unassigned integer
rerun_child_calculation_ids string[] Components re-priced because their drawing changed; the assembly total re-settles when they finish.

Example response

{
  "applied": true,
  "calculation_id": "string",
  "drawings": {
    "assembly": {
      "added_at": "string",
      "basis": "string",
      "candidates": [
        {}
      ],
      "component_key": "string",
      "data_source_id": "string",
      "drawing_number": "string",
      "filename": "bracket.step",
      "material": "string",
      "note": "string",
      "page_count": 0,
      "position": 0,
      "size_bytes": 204800,
      "status": "string",
      "title": "string"
    },
    "n_files": 0,
    "n_matched": 0,
    "n_unassigned": 0,
    "n_with_drawing": 0,
    "unassigned": [
      {
        "added_at": "string",
        "basis": "string",
        "candidates": [],
        "component_key": "string",
        "data_source_id": "string",
        "drawing_number": "string",
        "filename": "bracket.step",
        "material": "string",
        "note": "string",
        "page_count": 0,
        "position": 0,
        "size_bytes": 204800,
        "status": "string",
        "title": "string"
      }
    ]
  },
  "files": [
    {
      "added_at": "string",
      "basis": "string",
      "candidates": [
        {}
      ],
      "component_key": "string",
      "data_source_id": "string",
      "drawing_number": "string",
      "filename": "bracket.step",
      "material": "string",
      "note": "string",
      "page_count": 0,
      "position": 0,
      "size_bytes": 204800,
      "status": "string",
      "title": "string"
    }
  ],
  "n_assembly": 0,
  "n_matched": 0,
  "n_unassigned": 0,
  "rerun_child_calculation_ids": [
    "string"
  ]
}

Decide where one drawing of the pack belongs

POST /api/v1/calculations/{calculation_id}/drawings/{data_source_id}/assign

Attach one pack file to a component (component_key from the bill of materials), make it the assembly drawing, or discard it. A component whose drawing changes is priced again; the assembly total re-settles when it finishes.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.
data_source_id path string yes Identifier of the data source.

Request body (application/json)

Field Type Required Description
component_key string no Required for component.
target component | assembly | discard yes component attaches the file to component_key; assembly makes it the assembly's own drawing; discard detaches it and drops it from the pack.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/{calculation_id}/drawings/{data_source_id}/assign \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "target": "component"
  }'
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/drawings/{data_source_id}/assign",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "target": "component"
    },
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/drawings/{data_source_id}/assign", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "target": "component"
  }),
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
applied boolean True when the files were matched against the bill of materials now. False when the assembly has not been split into its parts yet: the files wait and are matched at that moment, so every component prices with its drawing on its first run.
calculation_id string
drawings AssemblyDrawingsOut The whole pack after this call, when a BOM exists.
files AssemblyDrawingEntryOut[] The files this call touched.
n_assembly integer
n_matched integer
n_unassigned integer
rerun_child_calculation_ids string[] Components re-priced because their drawing changed; the assembly total re-settles when they finish.

Example response

{
  "applied": true,
  "calculation_id": "string",
  "drawings": {
    "assembly": {
      "added_at": "string",
      "basis": "string",
      "candidates": [
        {}
      ],
      "component_key": "string",
      "data_source_id": "string",
      "drawing_number": "string",
      "filename": "bracket.step",
      "material": "string",
      "note": "string",
      "page_count": 0,
      "position": 0,
      "size_bytes": 204800,
      "status": "string",
      "title": "string"
    },
    "n_files": 0,
    "n_matched": 0,
    "n_unassigned": 0,
    "n_with_drawing": 0,
    "unassigned": [
      {
        "added_at": "string",
        "basis": "string",
        "candidates": [],
        "component_key": "string",
        "data_source_id": "string",
        "drawing_number": "string",
        "filename": "bracket.step",
        "material": "string",
        "note": "string",
        "page_count": 0,
        "position": 0,
        "size_bytes": 204800,
        "status": "string",
        "title": "string"
      }
    ]
  },
  "files": [
    {
      "added_at": "string",
      "basis": "string",
      "candidates": [
        {}
      ],
      "component_key": "string",
      "data_source_id": "string",
      "drawing_number": "string",
      "filename": "bracket.step",
      "material": "string",
      "note": "string",
      "page_count": 0,
      "position": 0,
      "size_bytes": 204800,
      "status": "string",
      "title": "string"
    }
  ],
  "n_assembly": 0,
  "n_matched": 0,
  "n_unassigned": 0,
  "rerun_child_calculation_ids": [
    "string"
  ]
}

Upload Inputs

POST /api/v1/calculations/{calculation_id}/inputs

Attach a 2D drawing, a mesh, or an RFQ text file to the calculation.

Accepted per role: drawing_2d → PDF, PNG, JPEG; mesh → STL, OBJ; rfq_text → plain text, Markdown, PDF. A mesh carries no B-rep, so it yields envelope, mass and surface area only — attach STEP at creation time for a fully-featured quote.

The file is stored against the calculation's part revision under the requested role. Subsequent POST /run calls auto-discover it by role and fold it into the analysis (drawing, mesh fallback, RFQ text) alongside the primary geometry.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.

Request body (multipart/form-data)

Field Type Required Description
file string yes The file to attach: a 2D drawing (PDF, PNG or JPEG), an STL/OBJ mesh, or an RFQ text file.
role drawing_2d | mesh | rfq_text yes Role the file plays on the calculation's revision: drawing_2d, mesh, or rfq_text.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/{calculation_id}/inputs \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -F "role=drawing_2d" \
  -F "[email protected]"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/inputs",
    headers={"X-API-Key": "YOUR_API_KEY"},
    files={
        "file": open("file.bin", "rb"),
    },
    data={
        "role": "drawing_2d",
    },
)
resp.raise_for_status()
print(resp.json())
const form = new FormData()
form.append("role", "drawing_2d")
form.append("file", file) // a File or Blob

const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/inputs", {
  method: "POST",
  headers: { "X-API-Key": process.env.ARCNM_API_KEY! },
  body: form,
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
calculation_id string UUID of the calculation the file was attached to.
data_source_id string UUID of the stored data source created for the uploaded file.
inputs_changed_since_run InputsChangedSinceRun Set when this calculation has already finished: the file just attached (and any other newer input) is not in its result. Price the part again to read it; null while the run is still ahead.
role string Role the file was attached under (e.g. drawing_2d, mesh, rfq_text).
sha256 string Hex-encoded SHA-256 digest of the uploaded bytes.
size_bytes integer Size of the uploaded file in bytes.

Example response

{
  "calculation_id": "string",
  "data_source_id": "string",
  "inputs_changed_since_run": {
    "corrections": 0,
    "latest_at": "string",
    "roles": [
      "string"
    ]
  },
  "role": "primary",
  "sha256": "9f86d081884c7d659a2feaa0c55ad015…",
  "size_bytes": 204800
}

Patch Calculation Material

PATCH /api/v1/calculations/{calculation_id}/material

Override the material on an existing calculation.

The new material_grade_id is resolved exactly the same way POST /calculations resolves it on create, so the result is consistent with the create-time logic.

The calculation isn't re-priced here — follow up with POST /run if a re-quote is desired. The explicit two-step flow lets you review the override before paying for another pipeline pass.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.

Request body (application/json)

Field Type Required Description
material_grade_id string no UUID of the material grade to link. Provide this or material_ref.
material_ref string no Material reference (URN, Werkstoffnummer, or trade name) to resolve and link. Provide this or material_grade_id.

Request

curl -X PATCH https://api.arcnm.io/api/v1/calculations/{calculation_id}/material \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "material_grade_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "material_ref": "1.4301"
  }'
import requests

resp = requests.patch(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/material",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "material_grade_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "material_ref": "1.4301"
    },
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/material", {
  method: "PATCH",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "material_grade_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "material_ref": "1.4301"
  }),
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
id string UUID of the calculation that was updated.
material_grade_id string UUID of the newly assigned material grade.
material_ref string Material reference (URN) of the newly assigned grade; null when unavailable.
previous_material_grade_id string UUID of the material grade before this change; null if none was set.
resolved_via string How the grade was resolved: a directly supplied grade id, or a free-form reference lookup.

Example response

{
  "id": "string",
  "material_grade_id": "string",
  "material_ref": "1.4301",
  "previous_material_grade_id": "string",
  "resolved_via": "string"
}

Optimization directions for a calculation

GET /api/v1/calculations/{calculation_id}/optimization

Research-grounded cost-optimization directions for one priced calculation: lot sizing, setup reduction, tolerance-cost review, material utilization, external-process benchmarking, plus agent-grade signals (inspection/programming shares, volume-bundling elasticity, DFM issues) and the live supplier-quote gap where a quote exists. Every figure derives from this calculation's own engine results.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.

Request

curl -X GET https://api.arcnm.io/api/v1/calculations/{calculation_id}/optimization \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/optimization",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/optimization", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
calculation_id string Identifier of the calculation.
currency string Currency of the money figures.
environment_changed_since_run EnvironmentChangedSinceRun Null when the run these directions derive from was priced on its costing environment as it stands. Otherwise the environment was edited after the run read it (latest_at) — a new calculation prices with the current setup, these figures do not. The same value GET /calculations/{id} states.
findings OptimizationDirection[] Optimization directions; empty when the calculation predates the findings engine or nothing material fired.
inputs_changed_since_run InputsChangedSinceRun Null when the run these directions derive from reflects every input on its part revision. Otherwise the roles (drawing_2d, mesh, rfq_text) whose active file was attached after the run read its inputs, and the number of corrections applied to the part since — the next run reads them, these figures do not. The same value GET /calculations/{id} states.
status string Calculation status the findings derive from.

Example response

{
  "calculation_id": "string",
  "currency": "EUR",
  "environment_changed_since_run": {
    "latest_at": "string"
  },
  "findings": [
    {
      "audience": "string",
      "confidence": "string",
      "kind": "string",
      "lever": "string",
      "method": "string",
      "params": {},
      "saving_per_piece": 0,
      "saving_per_year": 0
    }
  ],
  "inputs_changed_since_run": {
    "corrections": 0,
    "latest_at": "string",
    "roles": [
      "string"
    ]
  },
  "status": "string"
}

Price a calculation under what-if adjustments, without billing it

POST /api/v1/calculations/{calculation_id}/preview

Answers 'what would this part cost if …' over a calculation that is already priced: name the factors to move and read back the whole cost sheet plus the change per category. It reuses this calculation's own inputs and produces nothing you keep — no new calculation, no change to this one, no change to your environment — and it consumes none of your included calculations. Adjustments are checked against each factor's band before anything is priced, and a request that cannot be answered exactly is refused by name rather than answered approximately.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.

Request body (application/json)

Field Type Required Description
adjustments object no Factors to move, by name, each to the value given. The names are the ones a cost-sheet line lists in factors and the environment's factor endpoint publishes: a factor stated per process carries its process (time.setup_efficiency.milling), a shop-wide one does not. Every value is checked against that factor's own band before anything is priced. Leave empty to price the same factors at a different lot size.
lot_size integer no Quantity to price at. Omit to keep the quantity the calculation was priced for.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/{calculation_id}/preview \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "adjustments": {},
    "lot_size": 50
  }'
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/preview",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "adjustments": {},
        "lot_size": 50
    },
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/preview", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "adjustments": {},
    "lot_size": 50
  }),
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
cost_sheet PublicCostSheet The itemised cost sheet as priced under the adjustments.
delta object Change per top-level cost category against the priced calculation, in the sheet's currency: new minus current. A category on only one side carries its whole amount, so a line that appears or disappears is visible rather than skipped.
total_delta number Change in the cost per part against the priced calculation, in the sheet's currency: new minus current. On the same basis as the calculation's own unit_cost — the manufacturing cost, before the surcharges and mark-up your environment states. The offer price is the sum of every top-level line of cost_sheet, and each of those surcharge lines has its own entry in delta.

Example response

{
  "cost_sheet": {
    "currency": "EUR",
    "lines": [
      {
        "amount": 0,
        "bucket": "string",
        "children": [],
        "door": {},
        "evidence": {},
        "factors": [],
        "formula_id": "string",
        "label_id": "string",
        "label_params": {},
        "line_kind": "string",
        "op_refs": [],
        "rate": {},
        "terms": [],
        "time_basis": "string",
        "time_s": 184
      }
    ],
    "lot_size": 50,
    "notes": [
      {
        "bucket": "string",
        "code": "string",
        "residual": 0
      }
    ],
    "quantity_basis": 0,
    "reconciled": true,
    "residual": 0,
    "total": 0,
    "version": 0
  },
  "delta": {},
  "total_delta": 0
}

Run Calculation

POST /api/v1/calculations/{calculation_id}/run

Enqueue the calculation onto the worker queue — subject to coverage.

Idempotent: a row in a non-terminal state (queued/running/polling) is not re-enqueued; a row in succeeded is returned untouched — a finished result is a record of one run, so re-pricing the part after its inputs changed (inputs_changed_since_run) means creating a new calculation on the same revision and running that; a row in failed/cancelled/timed_out is reset to queued and re-enqueued so the user can retry.

Coverage: before enqueueing, the org's included-calculation quota is checked. A hard-capped org with no included extraction left FAILS FAST: the calc is parked as blocked (never a queued orphan) and the request raises 402 quota_exceeded with the upgrade path (F-2) — except when the dedup cache can serve an identical prior run, which stays free even over quota. A payment-blocked (dunning) org or an over-spend-cap API key still parks with a coverage payload on a 2xx. A blocked row resumes through this same path once unblocked, without spending a retry attempt.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/{calculation_id}/run \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/run",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/run", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
202 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
402 insufficient_funds The pre-authorised wallet hold for the run exceeds your balance.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 202

Field Type Description
batch_id string Groups the runs of a multi-environment quote; compare them via GET /calculations/batches/{batch_id}/comparison. Null for a single-environment quote.
costing_environment_id string UUID of the costing environment this calculation is priced against — the one you supplied, or your organization's default when you omitted it. Always echoed so a caller that omitted the field can tell which rates the quote used.
coverage CalculationCoverage Populated only when status='blocked': the calc was created but NOT enqueued because the org has no included calculations left (or is payment-blocked). Explains how many parts can still run and how to resume this one. null whenever the calc was enqueued.
environment_runs EnvironmentRunStatus[] All runs of a multi-environment quote, one per requested environment in comparison order. The top-level fields describe the first created run; poll each run (or the batch comparison) for results. Null for a single-environment quote.
error string Why the calculation failed, when this response already carries a terminal failed status — a run refused at submit time, before any worker. Null in every other case, including a normal enqueue: a run that fails LATER reports through the calculation itself, not through the response that started it.
eta_basis string How eta_seconds was derived, so the number is auditable: org_rate — divided by YOUR organization's own measured completions per second over the recent window; platform_default — your organization has too little recent history, so the estimate comes from your plan's parallelism and the platform's measured service time. Null when no ETA was computed.
eta_seconds number Estimated wait before this calculation reaches a result, derived from its queue position divided by a measured completion rate (advisory); null when not enqueued. See eta_basis for which rate was used.
id string UUID of the calculation.
queue_position integer 1-based position among YOUR organization's own queued and running calculations at enqueue time (backpressure hint); null when not enqueued. With tenant fair scheduling active your parts run in parallel with other customers, so another tenant's backlog does not push you back.
status string Current lifecycle state: 'draft' — created by POST /calculations but not yet submitted; it has no worker and no job and is priced only when you call POST /run — then queued, running, succeeded, failed, cancelled, timed_out, or 'blocked' — created but NOT started because the org is out of included calculations (see coverage). A blocked calc has no worker and no job; resume it with POST /run after topping up.

Example response

{
  "batch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "coverage": {
    "included": 0,
    "message": "string",
    "reason": "string",
    "remaining": 0,
    "tier_slug": "string",
    "upgrade_url": "string",
    "used": 0,
    "window": "string"
  },
  "environment_runs": [
    {
      "calculation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "reason": "string",
      "status": "string"
    }
  ],
  "error": "string",
  "eta_basis": "string",
  "eta_seconds": 0,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "queue_position": 0,
  "status": "string"
}

List the kept what-ifs for one calculation

GET /api/v1/calculations/{calculation_id}/scenarios

The named adjustment sets saved against this calculation. Each one stores the factors it moves and never a price: re-run the preview to see what it costs today, because a rate or a factor changed since it was saved changes the answer without changing the scenario.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.
lineage query boolean no Also return the scenarios kept on the calculations this one was re-run from (its derived_from_calculation_id chain), each row naming the calculation it belongs to — so a what-if kept before a re-run is still in reach on the re-run's page.

Request

curl -X GET https://api.arcnm.io/api/v1/calculations/{calculation_id}/scenarios \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/scenarios",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/scenarios", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
count integer Number of scenarios returned.
items CostScenarioPublic[] The scenarios, newest first.

Example response

{
  "count": 0,
  "items": [
    {
      "adjustments": {},
      "calculation_id": "string",
      "created_at": "string",
      "id": "string",
      "name": "string",
      "result": {
        "computed_at": "string",
        "delta": {},
        "message": "string",
        "refused": "string",
        "total_delta": 0,
        "unit_cost": 12.84
      },
      "updated_at": "string"
    }
  ]
}

Keep a named what-if against one calculation

POST /api/v1/calculations/{calculation_id}/scenarios

Saves a set of factor adjustments under a name so the same question can be asked again later. It stores the inputs, not a price: every name is checked against the factor list and every value against its band, and nothing is priced until you preview it.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.

Request body (application/json)

Field Type Required Description
adjustments object no The factors this what-if moves and the value each is moved to — the same names and the same bands the preview endpoint takes.
name string yes Label to find this what-if by later.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/{calculation_id}/scenarios \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "string"
  }'
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/scenarios",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "name": "string"
    },
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/scenarios", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "name": "string"
  }),
})
const data = await resp.json()

Responses

Status Description
201 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 201

Field Type Description
adjustments object The factors it moves and the value each is moved to.
calculation_id string Calculation this scenario varies.
created_at string ISO 8601 timestamp when the scenario was saved.
id string Identifier of this scenario.
name string Label given to this what-if.
result CostScenarioResult What it cost when last worked out — a dated cache of one preview, absent on a scenario saved before it was computed.
updated_at string ISO 8601 timestamp when the scenario was last changed.

Example response

{
  "adjustments": {},
  "calculation_id": "string",
  "created_at": "string",
  "id": "string",
  "name": "string",
  "result": {
    "computed_at": "string",
    "delta": {},
    "message": "string",
    "refused": "string",
    "total_delta": 0,
    "unit_cost": 12.84
  },
  "updated_at": "string"
}

Delete a kept what-if

DELETE /api/v1/calculations/{calculation_id}/scenarios/{scenario_id}

Removes one saved adjustment set. The calculation it varied is untouched.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.
scenario_id path string yes Identifier of the scenario.

Request

curl -X DELETE https://api.arcnm.io/api/v1/calculations/{calculation_id}/scenarios/{scenario_id} \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.delete(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/scenarios/{scenario_id}",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/scenarios/{scenario_id}", {
  method: "DELETE",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
message string Confirmation that the scenario was deleted.

Example response

{
  "message": "string"
}

Rename a kept what-if or replace the factors it moves

PATCH /api/v1/calculations/{calculation_id}/scenarios/{scenario_id}

Changes the label and/or the adjustment set of one saved what-if. A new adjustment set is checked against the factor list and every band exactly as on save, and the scenario's cached result is worked out again.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.
scenario_id path string yes Identifier of the scenario.

Request body (application/json)

Field Type Required Description
adjustments object no The factors this what-if moves, replacing the set it had — the same names and bands the preview endpoint takes.
name string no New label.

Request

curl -X PATCH https://api.arcnm.io/api/v1/calculations/{calculation_id}/scenarios/{scenario_id} \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "adjustments": {},
    "name": "string"
  }'
import requests

resp = requests.patch(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/scenarios/{scenario_id}",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "adjustments": {},
        "name": "string"
    },
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/scenarios/{scenario_id}", {
  method: "PATCH",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "adjustments": {},
    "name": "string"
  }),
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
adjustments object The factors it moves and the value each is moved to.
calculation_id string Calculation this scenario varies.
created_at string ISO 8601 timestamp when the scenario was saved.
id string Identifier of this scenario.
name string Label given to this what-if.
result CostScenarioResult What it cost when last worked out — a dated cache of one preview, absent on a scenario saved before it was computed.
updated_at string ISO 8601 timestamp when the scenario was last changed.

Example response

{
  "adjustments": {},
  "calculation_id": "string",
  "created_at": "string",
  "id": "string",
  "name": "string",
  "result": {
    "computed_at": "string",
    "delta": {},
    "message": "string",
    "refused": "string",
    "total_delta": 0,
    "unit_cost": 12.84
  },
  "updated_at": "string"
}

Export a calculation's own sheet for cutting

GET /api/v1/calculations/{calculation_id}/sheet-nest/export

The sheet a calculation's own price laid its part out on, as a cutting file — the part alone, as many copies as its price put on one sheet. Same files as a job nest's, one sheet, no zip. A calculation that stored no sheet layout, or that is not cut from sheet, has nothing to export. A file still being built after a minute is answered 503 with Retry-After; /sheet-nest/export-url never waits.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.
format query dxf | svg | json no dxf (R2010, millimetres: one block per part and one insert per copy, on the layers SHEET, MARGIN, REMNANT, CUT_OUTER, CUT_INNER and LABEL), svg (the same geometry, the view box in millimetres) or json (the nest plan: every part's contour and every sheet's placements).
exploded query boolean no DXF only: write each copy's contours as plain closed polylines in place, no blocks — for a cutting system that does not read block references.

Request

curl -X GET https://api.arcnm.io/api/v1/calculations/{calculation_id}/sheet-nest/export \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/sheet-nest/export",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/sheet-nest/export", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 The file — Content-Disposition names it — or, for format=json, the nest plan.
404 No calculation with this id in your organization (calculation_not_found), or its price stored no sheet layout (sheet_layout_not_found).
422 Validation Error
503 The file is still being built (nest_export_timeout): ask again after Retry-After seconds.

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
parts NestPlanPart[]
sheets NestPlanSheet[]
source NestPlanSource
tolerance NestPlanTolerance
units string
v integer The schema version.

Example response

{
  "parts": [
    {
      "calculation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "holes": [
        []
      ],
      "line_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "name": "string",
      "net_area_mm2": 0,
      "order_ref": "string",
      "outline": [
        []
      ],
      "placed": 0,
      "quantity": 0,
      "ref": "string"
    }
  ],
  "sheets": [
    {
      "container": {
        "key": "string",
        "kind": "format",
        "length_mm": 0,
        "width_mm": 0
      },
      "count": 0,
      "gauge_mm": 0,
      "group": 0,
      "index": 0,
      "key": "string",
      "margin_mm": 0,
      "material": "string",
      "placements": [
        {}
      ],
      "remnant": {
        "length_mm": 0,
        "width_mm": 0,
        "x_mm": 0,
        "y_mm": 0
      },
      "repeat": 0,
      "spacing_mm": 0,
      "utilisation": 0
    }
  ],
  "source": {
    "calculation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "created_at": "2026-06-01T12:00:00Z",
    "environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "finished_at": "2026-06-01T12:00:00Z",
    "kind": "run",
    "run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  },
  "tolerance": {
    "arcs": "string",
    "contour_mm": 0,
    "note": "string"
  },
  "units": "string",
  "v": 0
}

GET /api/v1/calculations/{calculation_id}/sheet-nest/export-url

The file GET /calculations/{calculation_id}/sheet-nest/export answers, as a short-lived download link — without waiting for it to be built.

200: the file is ready; url downloads it (no credentials, until expires_in_s runs out) under file_name. 202: it is being built; ask again after Retry-After seconds. A build that was refused is answered with its own code and status; fresh=true builds it again. Same parameters, scope and refusals as the export.

Parameters

Name In Type Required Description
calculation_id path string yes Identifier of the calculation.
format query dxf | svg | json no dxf (R2010, millimetres: one block per part and one insert per copy, on the layers SHEET, MARGIN, REMNANT, CUT_OUTER, CUT_INNER and LABEL), svg (the same geometry, the view box in millimetres) or json (the nest plan: every part's contour and every sheet's placements).
exploded query boolean no DXF only: write each copy's contours as plain closed polylines in place, no blocks — for a cutting system that does not read block references.
fresh query boolean no Build the file again when its last build was refused and nothing is building it now. Without it, a refusal is answered again for a few minutes.

Request

curl -X GET https://api.arcnm.io/api/v1/calculations/{calculation_id}/sheet-nest/export-url \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/calculations/{calculation_id}/sheet-nest/export-url",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/{calculation_id}/sheet-nest/export-url", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
202 The file is being built: ask again after Retry-After seconds.
404 No calculation with this id in your organization (calculation_not_found), or its price stored no sheet layout (sheet_layout_not_found).
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
content_type string
expires_in_s integer Seconds until the link expires.
file_name string
size_bytes integer The file's size in bytes.
status string
url string GET it, without credentials, before it expires; the response names the file.

Example response

{
  "content_type": "string",
  "expires_in_s": 0,
  "file_name": "string",
  "size_bytes": 204800,
  "status": "string",
  "url": "string"
}

Batch re-cost a set of part revisions

POST /api/v1/calculations/batch

Create + enqueue one calculation per revision in a single request, grouped under a CalculationBatch — the path an agent triggers when a market signal (a material price move) makes a batch re-cost worth running. Each run is metered on success against the org's subscription (v5/v6 quota + flat overage), exactly like a single /run — there is no separate per-call wallet draw. Free orgs over their included quota are rejected up front (HTTP 402).

Selection is explicit (part_revision_ids) for now; richer criteria (by material category, above-should-cost, …) layer on top of this path later.

Request body (application/json)

Field Type Required Description
annual_volume integer no
batch_name string no
costing_environment_id string no Single environment to cost against. Omit together with costing_environment_ids to use the organization's baseline environment.
costing_environment_ids string[] no Environments to cost each revision against, in comparison order. Every (revision, environment) pair becomes one calculation and counts as one against the plan quota. Mutually exclusive with costing_environment_id. Duplicates are collapsed preserving first occurrence; revisions x environments may not exceed 500.
currency string no
engine string no
engines string[] no Engines to run per revision. Today a single engine is honoured and any other value is coerced to it. The list shape is preserved for forward-compat with future engines.
language string no
lot_size integer no
material_grade_id string no
material_is_provided boolean no
material_ref string no Free-form material reference (URN, Werkstoffnummer, AISI/SAE code, trade name, or your SKU); resolved to a material grade for every calculation in the batch. Unresolvable values are rejected with 422 material_unresolved plus ranked candidates. Omit to price the material read from each part's drawing (or the environment default).
nest_item_id string no Price this calculation's material on its part's line of a job nest — the id of a line (items[].id) of a job nest that has solved it. The material is then the line's share of the sheets the job nest cut it from together with other parts, less its share of what their skeleton and reusable rests earn back; everything else is priced as usual. The line must be of your organization, of this costing environment and of this part revision, and its job nest must have solved it; otherwise the request is rejected with 422 nest_item_unresolved, details.reason saying why. It cannot be combined with stock_format_id or with customer-supplied material. An upload creates a new part revision, so the upload forms reject every line. A line of the sheets an assembly shares among its own components prices that assembly only: naming one is rejected as not_found, and a re-run of such a component prices it on its own sheet. Omit it to price the part on its own sheet.
part_revision_ids string[] yes
provided_stock_kind none | near_net_profile | near_net_casting | sawn_blank no
raw_material_strategy RawMaterialStrategy no Deprecated. The raw-material strategy does not set the priced cost — the material comes from material_ref or the part's own drawing, and its price per kilogram from the costing environment's material rate — but this block is part of a calculation's cache identity, so two otherwise-identical batch runs that differ only here are priced as distinct calculations instead of one reusing the other's result. Set material_ref and the environment's material rate instead. It stays accepted on this endpoint; retiring it would be a breaking change and would ship under a new dated API version with advance notice.
region string no
stock_format_id string no Price this calculation on one stock format — the id of a format from GET /stock-formats. An id names the format, not only the version it came from: when the format has been changed since, the version current on the day the calculation is created is used. An id your organization cannot price with today (unknown, retired or hidden) is rejected with 422 stock_format_unresolved. When the part cannot be cut from that format — too small, not bought in the part's thickness or material, or one the machine cannot load — or is not cut from sheet at all (milled, turned or bought finished), the calculation is priced as if none had been chosen, and analytics.pipeline_notes says why (stock_format_pin_unavailable). Omit it to price on the format that uses the least material per part.
surface_treatments string[] no Deprecated. Surface treatments (e.g. anodizing, zinc plating) do not set the priced cost — treatments are read from the part's drawing and priced on cost_decomposition.subcontract_cost — but this field is part of a calculation's cache identity, so two otherwise-identical batch runs that differ only here are priced as distinct calculations instead of one reusing the other's result. Prefer specifying treatments on the drawing.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/batch \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "part_revision_ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ]
  }'
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/batch",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "part_revision_ids": [
            "3fa85f64-5717-4562-b3fc-2c963f66afa6"
        ]
    },
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/batch", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "part_revision_ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ]
  }),
})
const data = await resp.json()

Responses

Status Description
202 Successful Response
422 Validation Error — or a refused reference: material_unresolved (the material names no grade in the catalogue), stock_format_unresolved (stock_format_id names no stock format your organization prices with today) or nest_item_unresolved (nest_item_id names no job-nest line that can price this calculation; details.reason says why).

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 202

Field Type Description
batch_id string Groups the created calculations.
blocked integer Cells created but parked on billing (saved, waiting on a an upgrade or a raised spend cap — see coverage); never enqueued, resumable once billing allows.
costing_environment_ids string[] The environments costed against, in comparison order.
coverage CalculationCoverage Why cells were parked, when blocked > 0 — the same payload a single-run park response carries. null when nothing parked.
enqueued integer Calculations created and enqueued.
items BatchCalculationItem[]
requested integer Requested cells: distinct revisions × distinct environments.
served_from_cache integer Cells answered instantly from an identical prior run within the billing window — never billed.
skipped integer Cells skipped for structural reasons or unquotable environments (see each item).

Example response

{
  "batch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "blocked": 0,
  "costing_environment_ids": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "coverage": {
    "included": 0,
    "message": "string",
    "reason": "string",
    "remaining": 0,
    "tier_slug": "string",
    "upgrade_url": "string",
    "used": 0,
    "window": "string"
  },
  "enqueued": 0,
  "items": [
    {
      "calculation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "part_revision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "reason": "string",
      "status": "string"
    }
  ],
  "requested": 0,
  "served_from_cache": 0,
  "skipped": 0
}

Cancel every non-terminal calculation in a batch

POST /api/v1/calculations/batches/{batch_id}/cancel

Convenience for a whole comparison grid: cancels the batch's queued/running members in one call — the per-id equivalent of /bulk-cancel without a round-trip to collect the ids first. Terminal members are reported skipped, exactly as the id-list route would.

Parameters

Name In Type Required Description
batch_id path string yes Identifier of the batch.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/batches/{batch_id}/cancel \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/batches/{batch_id}/cancel",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/batches/{batch_id}/cancel", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
blocked string[] IDs parked as 'blocked' instead of run: the org is out of included calculations (or is payment-blocked), so they were saved but NOT enqueued. Resume them with bulk-retry after topping up. Distinct from 'skipped' (wrong state) so the UI can prompt the unblock path, not an error.
coverage CalculationCoverage The billing hold the blocked ids are parked under — how many calculations remain and how to unblock them. Present only when something parked. Mirrors the same field on a batch submit, so a client renders one 'awaiting credits' path whichever endpoint parked the work.
not_found string[] IDs that did not match a calculation for this tenant.
skipped string[] IDs skipped because their state didn't allow the action.
succeeded string[] IDs of calculations the bulk action applied to successfully.
unquotable string[] IDs failed immediately instead of run: their environment has no machine that can be evaluated, so the calculation could not succeed however long it ran. Each row carries the reason. Distinct from 'blocked' (the org's wallet, resolvable by topping up) and from 'skipped' (wrong state) — this one is resolved by fixing the environment.

Example response

{
  "blocked": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "coverage": {
    "included": 0,
    "message": "string",
    "reason": "string",
    "remaining": 0,
    "tier_slug": "string",
    "upgrade_url": "string",
    "used": 0,
    "window": "string"
  },
  "not_found": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "skipped": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "succeeded": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "unquotable": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ]
}

Compare a batch's calculations across environments

GET /api/v1/calculations/batches/{batch_id}/comparison

Pivot one batch into a parts × environments matrix: per-cell costs and times, best environment per part (ties included), deltas vs the cheapest and vs the baseline environment, and per-environment aggregates (wins, median delta, basket total). All figures are computed server-side, so every consumer sees the same comparison. Poll while complete is false — cells fill in as their runs finish.

Parameters

Name In Type Required Description
batch_id path string yes Identifier of the batch.

Request

curl -X GET https://api.arcnm.io/api/v1/calculations/batches/{batch_id}/comparison \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/calculations/batches/{batch_id}/comparison",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/batches/{batch_id}/comparison", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
baseline_environment_id string The delta anchor environment.
batch_id string
complete boolean True when every cell is terminal or parked — nothing is still computing.
environments ComparisonEnvironmentSummary[]
generated_at string
ignored_duplicates string[] Calculations dropped because a newer run of the same part in the same environment was also supplied (ad-hoc comparisons only).
mixed_calibration boolean True when any cell carries calibration_mismatch: at least one column is aligned to your reported costs and another is the engine's unaligned estimate. The deltas are still real, but part of the gap may be alignment rather than the environments themselves, so a winner here is worth confirming by calibrating the other environments too. Deliberately a flag and not an incomparability: excluding uncalibrated columns would leave a tenant who has calibrated exactly one environment with no comparison at all.
mixed_currency boolean True when cells carry more than one currency; deltas, wins and basket totals are then restricted to same-currency comparisons.
not_found string[] Requested calculation ids that do not exist in your organization (ad-hoc comparisons only).
parts ComparisonPartRow[]

Example response

{
  "baseline_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "batch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "complete": false,
  "environments": [
    {
      "assembly_partial": 0,
      "basket_total": 0,
      "blocked": 0,
      "calibrated_cells": 0,
      "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "currency": "EUR",
      "failed": 0,
      "median_delta_vs_baseline_pct": 0,
      "name": "string",
      "not_run": 0,
      "pending": 0,
      "stale_cells": 0,
      "succeeded": 0,
      "wins": 0
    }
  ],
  "generated_at": "2026-06-01T12:00:00Z",
  "ignored_duplicates": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "mixed_calibration": false,
  "mixed_currency": false,
  "not_found": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "parts": [
    {
      "best_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "cells": [
        {}
      ],
      "lot_size": 50,
      "part_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "part_number": "BRACKET-001",
      "part_revision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "revision_code": "string"
    }
  ]
}

Per-component drill-down of one assembly row of a comparison

GET /api/v1/calculations/batches/{batch_id}/comparison/components

For an assembly priced across environments, break one row of the comparison matrix down to its components: each unique component of the assembly becomes a row, each environment a column, every cell that component's own calculation in that environment — cost, live status and drill-down id. Components join across environments by their stable prototype key, so 'which part drives the difference between plants' is answerable at a glance. Empty components when the revision is not a decomposed assembly.

Parameters

Name In Type Required Description
batch_id path string yes Identifier of the batch.
part_revision_id query string yes Identifier of the part revision.

Request

curl -X GET https://api.arcnm.io/api/v1/calculations/batches/{batch_id}/comparison/components \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/calculations/batches/{batch_id}/comparison/components",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/batches/{batch_id}/comparison/components", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
404 not_found A referenced resource doesn't exist or isn't visible to your organisation.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
batch_id string
components AssemblyComponentCompareRow[] Empty when the row is not a decomposed assembly.
environments string[] Column order — identical to the batch comparison's.
part_revision_id string

Example response

{
  "batch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "components": [
    {
      "cells": [
        {}
      ],
      "component_key": "string",
      "label": "string",
      "position": 0
    }
  ],
  "environments": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "part_revision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}

Bulk Cancel Calculations

POST /api/v1/calculations/bulk-cancel

Cancel several queued or running calculations at once, together with the component calculations of an assembly. Each id is reported as succeeded or skipped, for example when it had already finished.

Request body (application/json)

Field Type Required Description
ids string[] yes Calculation IDs to act on (1–200). Duplicates collapse silently.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/bulk-cancel \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ]
  }'
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/bulk-cancel",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "ids": [
            "3fa85f64-5717-4562-b3fc-2c963f66afa6"
        ]
    },
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/bulk-cancel", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ]
  }),
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
blocked string[] IDs parked as 'blocked' instead of run: the org is out of included calculations (or is payment-blocked), so they were saved but NOT enqueued. Resume them with bulk-retry after topping up. Distinct from 'skipped' (wrong state) so the UI can prompt the unblock path, not an error.
coverage CalculationCoverage The billing hold the blocked ids are parked under — how many calculations remain and how to unblock them. Present only when something parked. Mirrors the same field on a batch submit, so a client renders one 'awaiting credits' path whichever endpoint parked the work.
not_found string[] IDs that did not match a calculation for this tenant.
skipped string[] IDs skipped because their state didn't allow the action.
succeeded string[] IDs of calculations the bulk action applied to successfully.
unquotable string[] IDs failed immediately instead of run: their environment has no machine that can be evaluated, so the calculation could not succeed however long it ran. Each row carries the reason. Distinct from 'blocked' (the org's wallet, resolvable by topping up) and from 'skipped' (wrong state) — this one is resolved by fixing the environment.

Example response

{
  "blocked": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "coverage": {
    "included": 0,
    "message": "string",
    "reason": "string",
    "remaining": 0,
    "tier_slug": "string",
    "upgrade_url": "string",
    "used": 0,
    "window": "string"
  },
  "not_found": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "skipped": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "succeeded": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "unquotable": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ]
}

Bulk Delete Calculations

POST /api/v1/calculations/bulk-delete

Delete several calculations at once, together with the component calculations of an assembly. This cannot be undone. Each id is reported as succeeded or skipped.

Request body (application/json)

Field Type Required Description
ids string[] yes Calculation IDs to act on (1–200). Duplicates collapse silently.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/bulk-delete \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ]
  }'
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/bulk-delete",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "ids": [
            "3fa85f64-5717-4562-b3fc-2c963f66afa6"
        ]
    },
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/bulk-delete", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ]
  }),
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
blocked string[] IDs parked as 'blocked' instead of run: the org is out of included calculations (or is payment-blocked), so they were saved but NOT enqueued. Resume them with bulk-retry after topping up. Distinct from 'skipped' (wrong state) so the UI can prompt the unblock path, not an error.
coverage CalculationCoverage The billing hold the blocked ids are parked under — how many calculations remain and how to unblock them. Present only when something parked. Mirrors the same field on a batch submit, so a client renders one 'awaiting credits' path whichever endpoint parked the work.
not_found string[] IDs that did not match a calculation for this tenant.
skipped string[] IDs skipped because their state didn't allow the action.
succeeded string[] IDs of calculations the bulk action applied to successfully.
unquotable string[] IDs failed immediately instead of run: their environment has no machine that can be evaluated, so the calculation could not succeed however long it ran. Each row carries the reason. Distinct from 'blocked' (the org's wallet, resolvable by topping up) and from 'skipped' (wrong state) — this one is resolved by fixing the environment.

Example response

{
  "blocked": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "coverage": {
    "included": 0,
    "message": "string",
    "reason": "string",
    "remaining": 0,
    "tier_slug": "string",
    "upgrade_url": "string",
    "used": 0,
    "window": "string"
  },
  "not_found": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "skipped": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "succeeded": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "unquotable": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ]
}

Bulk Retry Calculations

POST /api/v1/calculations/bulk-retry

Re-enqueue eligible failed/cancelled/timed_out rows.

Each row goes through the same quota-gate + enqueue path as a direct POST /run so subscription semantics and attempt budgets are enforced identically. A row that's already running, succeeded, out of attempt budget, or over a Free org's included quota lands in skipped. A row whose environment can make nothing lands in unquotable, failed immediately rather than enqueued to fail slowly.

Request body (application/json)

Field Type Required Description
ids string[] yes Calculation IDs to act on (1–200). Duplicates collapse silently.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/bulk-retry \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ]
  }'
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/bulk-retry",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "ids": [
            "3fa85f64-5717-4562-b3fc-2c963f66afa6"
        ]
    },
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/bulk-retry", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ]
  }),
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
blocked string[] IDs parked as 'blocked' instead of run: the org is out of included calculations (or is payment-blocked), so they were saved but NOT enqueued. Resume them with bulk-retry after topping up. Distinct from 'skipped' (wrong state) so the UI can prompt the unblock path, not an error.
coverage CalculationCoverage The billing hold the blocked ids are parked under — how many calculations remain and how to unblock them. Present only when something parked. Mirrors the same field on a batch submit, so a client renders one 'awaiting credits' path whichever endpoint parked the work.
not_found string[] IDs that did not match a calculation for this tenant.
skipped string[] IDs skipped because their state didn't allow the action.
succeeded string[] IDs of calculations the bulk action applied to successfully.
unquotable string[] IDs failed immediately instead of run: their environment has no machine that can be evaluated, so the calculation could not succeed however long it ran. Each row carries the reason. Distinct from 'blocked' (the org's wallet, resolvable by topping up) and from 'skipped' (wrong state) — this one is resolved by fixing the environment.

Example response

{
  "blocked": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "coverage": {
    "included": 0,
    "message": "string",
    "reason": "string",
    "remaining": 0,
    "tier_slug": "string",
    "upgrade_url": "string",
    "used": 0,
    "window": "string"
  },
  "not_found": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "skipped": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "succeeded": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "unquotable": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ]
}

Get Calculation Capacity

GET /api/v1/calculations/capacity

Introspect this organization's calculation capacity and queue state.

Read-only snapshot for bulk submitters: how many calculations may run in parallel (inflight_cap), the plan's pending bound and how much of it is used, and the organization's own queue. Poll it to pace a bulk submission instead of discovering limits through 429 responses.

Request

curl -X GET https://api.arcnm.io/api/v1/calculations/capacity \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/calculations/capacity",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/capacity", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
admission_enabled boolean Whether submit-path admission control is active. When false, pending_bound is not enforced (submits always enqueue).
completion_rate_per_s number Completions per second the ETAs below were divided by — your organization's own measured rate over the recent window when eta_basis is org_rate, otherwise the plan-share estimate used in its place. Null when nothing is queued. Divide any position by this to reproduce its ETA.
eta_basis string Which rate the ETAs below were divided by: org_rate (your own measured throughput) or platform_default (a plan-share estimate for an organization with too little history). Null when nothing is queued.
fair_scheduling boolean Whether tenant-fair scheduling is active. When true, each organization's calculations are scheduled from its own queue and served in parallel with other customers, so another tenant's backlog cannot delay the start of yours.
inflight_cap integer Maximum calculations this organization may have RUNNING concurrently. Enforced only when inflight_enabled is true; calculations over the cap stay queued and start as slots free up.
inflight_enabled boolean Whether the per-organization concurrency cap is actively enforced. When false, inflight_cap is advisory only.
inflight_now integer Calculations currently running for this organization; null when the live counter is briefly unavailable.
pending_bound integer Your plan's pending-calculation bound: the maximum queued + running calculations this organization may hold at once. Submits beyond it are rejected with HTTP 429 until earlier calculations drain.
pending_count integer This organization's calculations currently queued or running (its admitted footprint).
queue CalculationQueueEntry[] Your organization's own waiting calculations, in the order they will be served, each with its 1-based position and ETA. Empty when nothing is waiting or when fair scheduling is off (with it off there is no per-organization queue to report a position in). Capped at 200 entries; queue_pending_total is the untruncated count.
queue_pending_total integer Total calculations waiting in your organization's queue, before the 200-entry cap applied to queue.
tier_slug string Subscription tier the caps below are derived from (e.g. free, pro, scale, enterprise); null when the org has no resolved tier (defaults apply).

Example response

{
  "admission_enabled": true,
  "completion_rate_per_s": 184,
  "eta_basis": "string",
  "fair_scheduling": false,
  "inflight_cap": 0,
  "inflight_enabled": true,
  "inflight_now": 0,
  "pending_bound": 0,
  "pending_count": 0,
  "queue": [
    {
      "calculation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "eta_seconds": 0,
      "position": 0
    }
  ],
  "queue_pending_total": 0,
  "tier_slug": "string"
}

Compare an ad-hoc set of calculations across environments

POST /api/v1/calculations/comparison

The same parts × environments matrix as the batch comparison, for any set of your calculation ids — e.g. re-runs of the same parts from different days or batches. A read expressed as POST only because id lists don't fit in a query string; nothing is created or modified. Cells whose configuration diverges from their row's anchor are flagged incomparable_reasons instead of being silently compared.

Request body (application/json)

Field Type Required Description
baseline_environment_id string no Delta anchor. Omit to anchor on your organization's baseline environment when the compared calculations include it, else on the first calculation's environment. Supplying an environment none of the calculations used anchors on the first calculation's environment.
calculation_ids string[] yes Calculations to compare (duplicates collapse). Ids that do not exist in your organization are reported in not_found, never guessed at.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/comparison \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "calculation_ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ]
  }'
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/comparison",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "calculation_ids": [
            "3fa85f64-5717-4562-b3fc-2c963f66afa6"
        ]
    },
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/comparison", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "calculation_ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ]
  }),
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
baseline_environment_id string The delta anchor environment.
batch_id string
complete boolean True when every cell is terminal or parked — nothing is still computing.
environments ComparisonEnvironmentSummary[]
generated_at string
ignored_duplicates string[] Calculations dropped because a newer run of the same part in the same environment was also supplied (ad-hoc comparisons only).
mixed_calibration boolean True when any cell carries calibration_mismatch: at least one column is aligned to your reported costs and another is the engine's unaligned estimate. The deltas are still real, but part of the gap may be alignment rather than the environments themselves, so a winner here is worth confirming by calibrating the other environments too. Deliberately a flag and not an incomparability: excluding uncalibrated columns would leave a tenant who has calibrated exactly one environment with no comparison at all.
mixed_currency boolean True when cells carry more than one currency; deltas, wins and basket totals are then restricted to same-currency comparisons.
not_found string[] Requested calculation ids that do not exist in your organization (ad-hoc comparisons only).
parts ComparisonPartRow[]

Example response

{
  "baseline_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "batch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "complete": false,
  "environments": [
    {
      "assembly_partial": 0,
      "basket_total": 0,
      "blocked": 0,
      "calibrated_cells": 0,
      "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "currency": "EUR",
      "failed": 0,
      "median_delta_vs_baseline_pct": 0,
      "name": "string",
      "not_run": 0,
      "pending": 0,
      "stale_cells": 0,
      "succeeded": 0,
      "wins": 0
    }
  ],
  "generated_at": "2026-06-01T12:00:00Z",
  "ignored_duplicates": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "mixed_calibration": false,
  "mixed_currency": false,
  "not_found": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "parts": [
    {
      "best_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "cells": [
        {}
      ],
      "lot_size": 50,
      "part_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "part_number": "BRACKET-001",
      "part_revision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "revision_code": "string"
    }
  ]
}

Remaining calculation quota

GET /api/v1/calculations/entitlement

This organization's remaining included-calculation quota for the current window, plus its monthly overage budget. Check it before a bulk run so you can upgrade the plan (or raise the overage cap) up front instead of discovering the wall when a calc parks as blocked.

Request

curl -X GET https://api.arcnm.io/api/v1/calculations/entitlement \
  -H "X-API-Key: $ARCNM_API_KEY"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/calculations/entitlement",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/entitlement", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()

Responses

Status Description
200 Successful Response
422 Validation Error

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 200

Field Type Description
activation_runs_remaining integer Calculations this account may still run before a human reviews it, or null when no review is pending and only the plan quota applies. Separate from 'remaining': a new account can have plenty of included quota and still be limited to a handful of runs until it is reviewed. A bulk submitter should size a batch by the SMALLER of the two — the batch is admitted up to this number and the rest is saved, not run.
effective integer Included plus any one-window rollover — the real ceiling this window.
guidance string What the numbers mean and what to do next.
hard_capped boolean True when the plan blocks further runs at the ceiling; false when over-quota runs bill as overage instead of blocking.
has_payment_method boolean Whether a card is on file. Required before any calculation beyond the included volume on a 'wallet' plan.
included integer Calculations included in the plan per window.
over_quota boolean True when used has reached the effective ceiling.
overage_budget_remaining_cents integer Overage spend still inside the cap, in cents; null when uncapped.
overage_cap_cents integer This organization's monthly overage spend cap in cents, or null when uncapped. Bounds what over-quota runs may cost; it does not affect included calculations.
overage_cents integer Price of one calculation beyond the included volume, in cents. Null when this plan cannot bill overage.
overage_settlement string How overage is paid: 'wallet' (prepaid balance — top up to continue) or 'invoice' (contracted, billed in arrears). Empty when the plan cannot bill overage.
overage_spend_cents integer Overage already billed this calendar month, in cents.
payment_blocked boolean True when billing is suspended (dunning) and no calculation will run.
plan string Subscription tier slug the quota applies to.
remaining integer Calculations still available this window (effective − used, floored at 0).
rollover integer Unused calculations carried in from last window.
spend_cap_blocked boolean True when the organization's overage cap is reached and set to 'pause' — further over-quota runs park as 'blocked' until the cap is raised or the month rolls over.
used integer Calculations already consumed this window.
wallet_balance_cents integer Prepaid balance in cents. Only meaningful for 'wallet' settlement. Null for API keys and agent grants: the balance is billing data, shown in the dashboard only. 'wallet_funded_calculations' and 'wallet_overdrawn' say what it means for your next runs.
wallet_funded_calculations integer How many more overage calculations the current balance covers. Null when settlement is not 'wallet'.
wallet_overdraft_cents integer How far the prepaid balance is below zero, as a positive number of cents. 0 for a wallet that is not overdrawn. Null for API keys and agent grants, like 'wallet_balance_cents'.
wallet_overdraft_floor_cents integer The hard floor on the prepaid balance, as a positive number of cents: the balance can never go below its negation. Runs pause once it is reached.
wallet_overdrawn boolean True when the prepaid balance has reached its hard floor. The wallet analogue of an exhausted hard cap: further runs park as 'blocked' and resume on a top-up, and the balance cannot be driven any further negative.
window string Quota window granularity: 'month' or 'year'.
window_start string ISO 8601 start of the current quota window.

Example response

{
  "activation_runs_remaining": 0,
  "effective": 0,
  "guidance": "string",
  "hard_capped": true,
  "has_payment_method": false,
  "included": 0,
  "over_quota": true,
  "overage_budget_remaining_cents": 0,
  "overage_cap_cents": 0,
  "overage_cents": 0,
  "overage_settlement": "",
  "overage_spend_cents": 0,
  "payment_blocked": true,
  "plan": "string",
  "remaining": 0,
  "rollover": 0,
  "spend_cap_blocked": false,
  "used": 0,
  "wallet_balance_cents": 0,
  "wallet_funded_calculations": 0,
  "wallet_overdraft_cents": 0,
  "wallet_overdraft_floor_cents": 0,
  "wallet_overdrawn": false,
  "window": "string",
  "window_start": "string"
}

Quote

POST /api/v1/calculations/quote

Convenience: create + enqueue in one round-trip.

Equivalent to POST / + POST /{id}/run. With costing_environment_ids the part is priced in every requested environment at once (one calculation per environment, grouped under batch_id for comparison).

Request body (application/json)

Field Type Required Description
annual_volume integer no Expected yearly quantity, used for amortizing setup over the run (1 to 1,000,000,000).
costing_environment_id string no UUID of the costing environment (machine rates, region) to price against. Omit to use your organization's default environment (the auto-provisioned default baseline).
costing_environment_ids string[] no Price this part in several environments at once, in comparison order. Each environment becomes one calculation and counts as one against the plan quota. Mutually exclusive with costing_environment_id; duplicates are collapsed preserving first occurrence. The response then carries batch_id and environment_runs.
currency string no ISO 4217 currency code for the quote; defaults to the costing environment's currency when omitted.
dataset_link_id string no UUID of a specific dataset link to use; omit to use the revision's active primary CAD.
derived_from_calculation_id string no The calculation this one is re-run from — pass it when pricing again after a drawing was attached or from a result's own page, so the new calculation records what it supersedes. Must name a calculation of your organization; the new calculation is its own row and counts as one.
engine string no Pricing engine selector; retained for back-compat and always normalized to the sole engine.
full_stock_charge auto | full | full_no_credit | share no How the started sheet or bar of the lot is charged: auto applies the environment's full_stock_threshold (the lot pays the whole piece once its parts fill that share of it, the unused cells credited as scrap where scrap is credited); full charges the whole piece at any lot; full_no_credit charges it whole with no scrap credit on the unused cells; share charges only the lot's share at any lot. A lot of whole pieces, or a piece holding one part, is charged the same under every word. On a re-run (derived_from_calculation_id), omitting it keeps the word of the calculation re-run.
language string no BCP 47 language for the calculation's captured context (e.g. 'de', 'en'). Part of the calculation's cache identity.
lot_size integer no Number of identical parts produced per batch (1 to 1,000,000,000).
material_grade_id string no UUID of a resolved material grade; takes precedence over material_ref when both are supplied.
material_is_provided boolean no True when the customer supplies the raw material (beigestellt) — the material cost line is set to zero; machining, setup and overheads are billed normally.
material_ref string no Free-form material reference (URN, Werkstoffnummer, AISI/SAE code, trade name, or your SKU); resolved to a material grade. Unresolvable values are rejected with 422 material_unresolved plus ranked candidates. Omit to price the material read from the part's drawing (or the environment default).
name string no Optional human-readable label; defaults to a timestamped name when omitted.
nest_item_id string no Price this calculation's material on its part's line of a job nest — the id of a line (items[].id) of a job nest that has solved it. The material is then the line's share of the sheets the job nest cut it from together with other parts, less its share of what their skeleton and reusable rests earn back; everything else is priced as usual. The line must be of your organization, of this costing environment and of this part revision, and its job nest must have solved it; otherwise the request is rejected with 422 nest_item_unresolved, details.reason saying why. It cannot be combined with stock_format_id or with customer-supplied material. An upload creates a new part revision, so the upload forms reject every line. A line of the sheets an assembly shares among its own components prices that assembly only: naming one is rejected as not_found, and a re-run of such a component prices it on its own sheet. Omit it to price the part on its own sheet. On a re-run (derived_from_calculation_id), omitting it keeps the line of the calculation re-run; send null to price the part on its own sheet.
part_revision_id string yes UUID of the part revision to price.
provided_stock_kind none | near_net_profile | near_net_casting | sawn_blank no Shape of the provided stock. 'near_net_profile' (extruded profile) additionally scopes the process plan to the features the profile does not already provide.
region string no Pricing region override; defaults to the costing environment's region when omitted.
stock_format_id string no Price this calculation on one stock format — the id of a format from GET /stock-formats. An id names the format, not only the version it came from: when the format has been changed since, the version current on the day the calculation is created is used. An id your organization cannot price with today (unknown, retired or hidden) is rejected with 422 stock_format_unresolved. When the part cannot be cut from that format — too small, not bought in the part's thickness or material, or one the machine cannot load — or is not cut from sheet at all (milled, turned or bought finished), the calculation is priced as if none had been chosen, and analytics.pipeline_notes says why (stock_format_pin_unavailable). Omit it to price on the format that uses the least material per part. On a re-run (derived_from_calculation_id), omitting it keeps the format of the calculation re-run; send null to price without one.
stock_format_mm StockFormatMm no Price this calculation on a sheet of this size — length and width in mm — instead of a stock format from GET /stock-formats: the part is laid out on that one sheet, cut to size by your supplier, and the purchased stock says format_source custom. It cannot be combined with stock_format_id or nest_item_id. When the part does not fit the sheet, or is not cut from sheet stock at all, the calculation is priced as if none had been stated and analytics.pipeline_notes says why. On a re-run (derived_from_calculation_id), omitting both it and stock_format_id keeps the size or format of the calculation re-run; send null to price without one.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/quote \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "part_revision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  }'
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/quote",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "part_revision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    },
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/quote", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "part_revision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  }),
})
const data = await resp.json()

Responses

Status Description
202 Successful Response
422 Validation Error — or a refused reference: material_unresolved (the material names no grade in the catalogue), stock_format_unresolved (stock_format_id names no stock format your organization prices with today) or nest_item_unresolved (nest_item_id names no job-nest line that can price this calculation; details.reason says why).

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
402 insufficient_funds The pre-authorised wallet hold for the run exceeds your balance.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 202

Field Type Description
batch_id string Groups the runs of a multi-environment quote; compare them via GET /calculations/batches/{batch_id}/comparison. Null for a single-environment quote.
costing_environment_id string UUID of the costing environment this calculation is priced against — the one you supplied, or your organization's default when you omitted it. Always echoed so a caller that omitted the field can tell which rates the quote used.
coverage CalculationCoverage Populated only when status='blocked': the calc was created but NOT enqueued because the org has no included calculations left (or is payment-blocked). Explains how many parts can still run and how to resume this one. null whenever the calc was enqueued.
environment_runs EnvironmentRunStatus[] All runs of a multi-environment quote, one per requested environment in comparison order. The top-level fields describe the first created run; poll each run (or the batch comparison) for results. Null for a single-environment quote.
error string Why the calculation failed, when this response already carries a terminal failed status — a run refused at submit time, before any worker. Null in every other case, including a normal enqueue: a run that fails LATER reports through the calculation itself, not through the response that started it.
eta_basis string How eta_seconds was derived, so the number is auditable: org_rate — divided by YOUR organization's own measured completions per second over the recent window; platform_default — your organization has too little recent history, so the estimate comes from your plan's parallelism and the platform's measured service time. Null when no ETA was computed.
eta_seconds number Estimated wait before this calculation reaches a result, derived from its queue position divided by a measured completion rate (advisory); null when not enqueued. See eta_basis for which rate was used.
id string UUID of the calculation.
queue_position integer 1-based position among YOUR organization's own queued and running calculations at enqueue time (backpressure hint); null when not enqueued. With tenant fair scheduling active your parts run in parallel with other customers, so another tenant's backlog does not push you back.
status string Current lifecycle state: 'draft' — created by POST /calculations but not yet submitted; it has no worker and no job and is priced only when you call POST /run — then queued, running, succeeded, failed, cancelled, timed_out, or 'blocked' — created but NOT started because the org is out of included calculations (see coverage). A blocked calc has no worker and no job; resume it with POST /run after topping up.

Example response

{
  "batch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "coverage": {
    "included": 0,
    "message": "string",
    "reason": "string",
    "remaining": 0,
    "tier_slug": "string",
    "upgrade_url": "string",
    "used": 0,
    "window": "string"
  },
  "environment_runs": [
    {
      "calculation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "reason": "string",
      "status": "string"
    }
  ],
  "error": "string",
  "eta_basis": "string",
  "eta_seconds": 0,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "queue_position": 0,
  "status": "string"
}

Upload And Quote

POST /api/v1/calculations/upload-and-quote

One-shot upload + new-calculation flow.

Takes a STEP file (+ optional 2D drawing PDF + RFQ text), creates the underlying Part / PartRevision / dataset rows, and enqueues the calculation. This is the simplest end-to-end UX — the user goes from "I have a CAD file" to "I'll see a quote in 30 s" with one HTTP call.

Idempotency: part_number is the natural key — if a Part with the same number already exists for this tenant, on_conflict decides what happens, and filing in the response says what did.

Request body (multipart/form-data)

Field Type Required Description
annual_volume integer no Expected yearly quantity used to amortize setup cost (1 to 1,000,000,000).
cad_file string yes STEP file (.step/.stp) to price; required — STEP is the only 3D format accepted. Export as STEP AP203/AP214/AP242. Or the DXF flat pattern of a sheet part (.dxf): then sheet_gauge_mm and a material are required, and the part is priced as a flat sheet part cut to that pattern.
costing_environment_id string no UUID of the costing environment (machine rates, region) to price against. Omit to use your organization's default environment (the auto-provisioned default baseline).
costing_environment_ids string[] no Price the part in several environments at once — repeat the field once per environment id, in comparison order. The file is uploaded once; each environment becomes one calculation and counts as one against the plan quota. The response then carries batch_id and environment_runs. Mutually exclusive with costing_environment_id; at most 16.
currency string no ISO 4217 currency code for the quote; defaults to the costing environment's currency when omitted.
drawing_file string no Optional 2D drawing (PDF/PNG/JPEG) for the part.
engine string no Pricing engine selector; retained for back-compat and always normalized to the sole engine.
folder_id string no Optional workspace folder to file the created part into (bulk upload into a folder).
language string no BCP 47 language for the calculation's captured context (e.g. 'de', 'en'). Part of the calculation's cache identity.
lot_size integer no Number of identical parts produced per batch (1 to 1,000,000,000).
material_grade_id string no UUID of a resolved material grade; takes precedence over material_ref when both are supplied.
material_is_provided boolean no True when the customer supplies the raw material (beigestellt) — the material cost line is set to zero.
material_ref string no Free-form material reference (URN, Werkstoffnummer, AISI/SAE code, trade name, or your SKU). Unresolvable values are rejected with 422 material_unresolved plus ranked candidates. Omit to price the material read from the part's drawing (or the environment default).
nest_item_id string no Price this calculation's material on its part's line of a job nest — the id of a line (items[].id) of a job nest that has solved it. The material is then the line's share of the sheets the job nest cut it from together with other parts, less its share of what their skeleton and reusable rests earn back; everything else is priced as usual. The line must be of your organization, of this costing environment and of this part revision, and its job nest must have solved it; otherwise the request is rejected with 422 nest_item_unresolved, details.reason saying why. It cannot be combined with stock_format_id or with customer-supplied material. An upload creates a new part revision, so the upload forms reject every line. A line of the sheets an assembly shares among its own components prices that assembly only: naming one is rejected as not_found, and a re-run of such a component prices it on its own sheet. Omit it to price the part on its own sheet.
on_conflict keep_folder | move | new_part | skip no How to resolve a part_number that already exists in this workspace. keep_folder — add a revision, leave the part where it is (the historical behaviour, now stated rather than assumed); move — add a revision AND re-file the part into folder_id; new_part — create a separate part under a distinct number (977 → 977-2); skip — upload nothing, create no calculation, meter nothing. Whatever happens is reported in filing.
part_number string yes Natural-key part number; reused to attach a new revision if the part already exists.
provided_stock_kind none | near_net_profile | near_net_casting | sawn_blank no Shape of the provided stock; 'near_net_profile' scopes the plan to the features the profile does not already provide.
region string no Pricing region override; defaults to the costing environment's region when omitted.
rfq_file string no Optional RFQ text file with requirements for the part.
sheet_gauge_mm number no Sheet thickness in millimetres — required when the CAD file is a DXF flat pattern (a 2D cutting file carries no thickness); ignored for STEP.
stock_format_id string no Price this calculation on one stock format — the id of a format from GET /stock-formats. An id names the format, not only the version it came from: when the format has been changed since, the version current on the day the calculation is created is used. An id your organization cannot price with today (unknown, retired or hidden) is rejected with 422 stock_format_unresolved. When the part cannot be cut from that format — too small, not bought in the part's thickness or material, or one the machine cannot load — or is not cut from sheet at all (milled, turned or bought finished), the calculation is priced as if none had been chosen, and analytics.pipeline_notes says why (stock_format_pin_unavailable). Omit it to price on the format that uses the least material per part.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/upload-and-quote \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -F "part_number=BRACKET-001" \
  -F "[email protected]"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/upload-and-quote",
    headers={"X-API-Key": "YOUR_API_KEY"},
    files={
        "cad_file": open("part.step", "rb"),
    },
    data={
        "part_number": "BRACKET-001",
    },
)
resp.raise_for_status()
print(resp.json())
const form = new FormData()
form.append("part_number", "BRACKET-001")
form.append("cad_file", file) // a File or Blob

const resp = await fetch("https://api.arcnm.io/api/v1/calculations/upload-and-quote", {
  method: "POST",
  headers: { "X-API-Key": process.env.ARCNM_API_KEY! },
  body: form,
})
const data = await resp.json()

Responses

Status Description
202 Successful Response
422 Validation Error — or a refused reference: material_unresolved (the material names no grade in the catalogue), stock_format_unresolved (stock_format_id names no stock format your organization prices with today) or nest_item_unresolved (nest_item_id names no job-nest line that can price this calculation; details.reason says why).

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
402 insufficient_funds The pre-authorised wallet hold for the run exceeds your balance.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 202

Field Type Description
batch_id string Groups the runs of a multi-environment quote; compare them via GET /calculations/batches/{batch_id}/comparison. Null for a single-environment quote.
costing_environment_id string UUID of the costing environment this calculation is priced against — the one you supplied, or your organization's default when you omitted it. Always echoed so a caller that omitted the field can tell which rates the quote used.
coverage CalculationCoverage Populated only when status='blocked': the calc was created but NOT enqueued because the org has no included calculations left (or is payment-blocked). Explains how many parts can still run and how to resume this one. null whenever the calc was enqueued.
environment_runs EnvironmentRunStatus[] All runs of a multi-environment quote, one per requested environment in comparison order. The top-level fields describe the first created run; poll each run (or the batch comparison) for results. Null for a single-environment quote.
error string Why the calculation failed, when this response already carries a terminal failed status — a run refused at submit time, before any worker. Null in every other case, including a normal enqueue: a run that fails LATER reports through the calculation itself, not through the response that started it.
eta_basis string How eta_seconds was derived, so the number is auditable: org_rate — divided by YOUR organization's own measured completions per second over the recent window; platform_default — your organization has too little recent history, so the estimate comes from your plan's parallelism and the platform's measured service time. Null when no ETA was computed.
eta_seconds number Estimated wait before this calculation reaches a result, derived from its queue position divided by a measured completion rate (advisory); null when not enqueued. See eta_basis for which rate was used.
filing PartFilingOutcome What happened to the part and its folder. Always present on the upload routes. Check folder_applied before telling a user their files were filed where they asked.
id string UUID of the calculation. Null only when filing.action_taken is skipped — you asked for the conflict to be skipped, so no calculation was created.
queue_position integer 1-based position among YOUR organization's own queued and running calculations at enqueue time (backpressure hint); null when not enqueued. With tenant fair scheduling active your parts run in parallel with other customers, so another tenant's backlog does not push you back.
status string Current lifecycle state: 'draft' — created by POST /calculations but not yet submitted; it has no worker and no job and is priced only when you call POST /run — then queued, running, succeeded, failed, cancelled, timed_out, or 'blocked' — created but NOT started because the org is out of included calculations (see coverage). A blocked calc has no worker and no job; resume it with POST /run after topping up.

Example response

{
  "batch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "coverage": {
    "included": 0,
    "message": "string",
    "reason": "string",
    "remaining": 0,
    "tier_slug": "string",
    "upgrade_url": "string",
    "used": 0,
    "window": "string"
  },
  "environment_runs": [
    {
      "calculation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "reason": "string",
      "status": "string"
    }
  ],
  "error": "string",
  "eta_basis": "string",
  "eta_seconds": 0,
  "filing": {
    "action_taken": "string",
    "collided_with_part_number": "string",
    "existing_part": true,
    "folder_applied": true,
    "folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "part_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "part_number": "BRACKET-001",
    "previous_folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "requested_folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  },
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "queue_position": 0,
  "status": "string"
}

Upload (base64 JSON) + quote in one call

POST /api/v1/calculations/upload-and-quote-json

JSON-body twin of /upload-and-quote for clients that can only pass strings (MCP): the CAD file — and optional drawing / RFQ — ride as base64 (or data: URI) strings. Best for small single-part STEP files; base64 inflates ~33% and rides inside the call, so use the presign flow for large assemblies. Same find-or-create-by-part_number idempotency, billing, and enqueue semantics as the multipart route.

Request body (application/json)

Field Type Required Description
annual_volume integer no Expected yearly quantity used to amortize setup cost (1 to 1,000,000,000).
cad_content_type string no MIME type of the CAD file, when known.
cad_file_b64 string yes Base64 (or a data: URI) of the 3D CAD file (e.g. STEP). Required.
cad_filename string yes CAD filename including extension, e.g. bracket.step — used for format detection.
costing_environment_id string no UUID of the costing environment to price against. Omit to use your organization's default environment.
costing_environment_ids string[] no Price the part in several environments at once, in comparison order. The file is uploaded once; each environment becomes one calculation and counts as one against the plan quota. The response then carries batch_id and environment_runs. Mutually exclusive with costing_environment_id.
currency string no ISO 4217 currency code for the quote; defaults to the costing environment's currency when omitted.
drawing_content_type string no MIME type of the drawing file, when known.
drawing_file_b64 string no Optional base64 (or data: URI) of a 2D drawing (PDF/PNG/JPEG).
drawing_filename string no Drawing filename including extension.
engine string no Pricing engine selector; retained for back-compat and always normalized to the sole engine.
folder_id string no Optional workspace folder to file the created part into.
language string no BCP 47 language for the calculation's captured context (e.g. 'de', 'en'). Part of the calculation's cache identity.
lot_size integer no Number of identical parts produced per batch (1 to 1,000,000,000).
material_grade_id string no UUID of a resolved material grade; takes precedence over material_ref when both are supplied.
material_is_provided boolean no True when the customer supplies the raw material (beigestellt).
material_ref string no Free-form material reference (URN, Werkstoffnummer, AISI/SAE code, trade name, or your SKU). Unresolvable values are rejected with 422 material_unresolved plus ranked candidates. Omit to price the material read from the part's drawing (or the environment default).
nest_item_id string no Price this calculation's material on its part's line of a job nest — the id of a line (items[].id) of a job nest that has solved it. The material is then the line's share of the sheets the job nest cut it from together with other parts, less its share of what their skeleton and reusable rests earn back; everything else is priced as usual. The line must be of your organization, of this costing environment and of this part revision, and its job nest must have solved it; otherwise the request is rejected with 422 nest_item_unresolved, details.reason saying why. It cannot be combined with stock_format_id or with customer-supplied material. An upload creates a new part revision, so the upload forms reject every line. A line of the sheets an assembly shares among its own components prices that assembly only: naming one is rejected as not_found, and a re-run of such a component prices it on its own sheet. Omit it to price the part on its own sheet.
on_conflict keep_folder | move | new_part | skip no How to resolve a part_number that already exists: keep_folder (add a revision, leave the part where it is — the historical behaviour), move (add a revision and re-file into folder_id), new_part (create a separate part under a distinct number), or skip (upload nothing, create no calculation, meter nothing). The outcome is always reported in filing.
part_number string yes Natural-key part number; reused to attach a new revision if the part already exists.
provided_stock_kind none | near_net_profile | near_net_casting | sawn_blank no Shape of the provided stock; 'near_net_profile' scopes the plan.
region string no Pricing region override; defaults to the costing environment's region when omitted.
rfq_content_type string no MIME type of the RFQ file, when known.
rfq_file_b64 string no Optional base64 (or data: URI) of an RFQ text file.
rfq_filename string no RFQ filename including extension.
sheet_gauge_mm number no Sheet thickness in millimetres — required when the CAD file is a DXF flat pattern (a 2D cutting file carries no thickness); ignored for STEP.
stock_format_id string no Price this calculation on one stock format — the id of a format from GET /stock-formats. An id names the format, not only the version it came from: when the format has been changed since, the version current on the day the calculation is created is used. An id your organization cannot price with today (unknown, retired or hidden) is rejected with 422 stock_format_unresolved. When the part cannot be cut from that format — too small, not bought in the part's thickness or material, or one the machine cannot load — or is not cut from sheet at all (milled, turned or bought finished), the calculation is priced as if none had been chosen, and analytics.pipeline_notes says why (stock_format_pin_unavailable). Omit it to price on the format that uses the least material per part.

Request

curl -X POST https://api.arcnm.io/api/v1/calculations/upload-and-quote-json \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "part_number": "BRACKET-001",
    "cad_file_b64": "string",
    "cad_filename": "bracket.step"
  }'
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/calculations/upload-and-quote-json",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "part_number": "BRACKET-001",
        "cad_file_b64": "string",
        "cad_filename": "bracket.step"
    },
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/calculations/upload-and-quote-json", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "part_number": "BRACKET-001",
    "cad_file_b64": "string",
    "cad_filename": "bracket.step"
  }),
})
const data = await resp.json()

Responses

Status Description
202 Successful Response
422 Validation Error — or a refused reference: material_unresolved (the material names no grade in the catalogue), stock_format_unresolved (stock_format_id names no stock format your organization prices with today) or nest_item_unresolved (nest_item_id names no job-nest line that can price this calculation; details.reason says why).

Errors

Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.

Status Code When
401 invalid_api_key Missing, malformed, or revoked API key.
403 insufficient_scope The key is valid but lacks a scope this endpoint requires.
409 conflict A conflicting change, or an Idempotency-Key reused with a different body.
429 rate_limited Per-key or per-org rate limit exceeded — back off with jitter and retry.

Response body 202

Field Type Description
batch_id string Groups the runs of a multi-environment quote; compare them via GET /calculations/batches/{batch_id}/comparison. Null for a single-environment quote.
costing_environment_id string UUID of the costing environment this calculation is priced against — the one you supplied, or your organization's default when you omitted it. Always echoed so a caller that omitted the field can tell which rates the quote used.
coverage CalculationCoverage Populated only when status='blocked': the calc was created but NOT enqueued because the org has no included calculations left (or is payment-blocked). Explains how many parts can still run and how to resume this one. null whenever the calc was enqueued.
environment_runs EnvironmentRunStatus[] All runs of a multi-environment quote, one per requested environment in comparison order. The top-level fields describe the first created run; poll each run (or the batch comparison) for results. Null for a single-environment quote.
error string Why the calculation failed, when this response already carries a terminal failed status — a run refused at submit time, before any worker. Null in every other case, including a normal enqueue: a run that fails LATER reports through the calculation itself, not through the response that started it.
eta_basis string How eta_seconds was derived, so the number is auditable: org_rate — divided by YOUR organization's own measured completions per second over the recent window; platform_default — your organization has too little recent history, so the estimate comes from your plan's parallelism and the platform's measured service time. Null when no ETA was computed.
eta_seconds number Estimated wait before this calculation reaches a result, derived from its queue position divided by a measured completion rate (advisory); null when not enqueued. See eta_basis for which rate was used.
filing PartFilingOutcome What happened to the part and its folder. Always present on the upload routes. Check folder_applied before telling a user their files were filed where they asked.
id string UUID of the calculation. Null only when filing.action_taken is skipped — you asked for the conflict to be skipped, so no calculation was created.
queue_position integer 1-based position among YOUR organization's own queued and running calculations at enqueue time (backpressure hint); null when not enqueued. With tenant fair scheduling active your parts run in parallel with other customers, so another tenant's backlog does not push you back.
status string Current lifecycle state: 'draft' — created by POST /calculations but not yet submitted; it has no worker and no job and is priced only when you call POST /run — then queued, running, succeeded, failed, cancelled, timed_out, or 'blocked' — created but NOT started because the org is out of included calculations (see coverage). A blocked calc has no worker and no job; resume it with POST /run after topping up.

Example response

{
  "batch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "coverage": {
    "included": 0,
    "message": "string",
    "reason": "string",
    "remaining": 0,
    "tier_slug": "string",
    "upgrade_url": "string",
    "used": 0,
    "window": "string"
  },
  "environment_runs": [
    {
      "calculation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "reason": "string",
      "status": "string"
    }
  ],
  "error": "string",
  "eta_basis": "string",
  "eta_seconds": 0,
  "filing": {
    "action_taken": "string",
    "collided_with_part_number": "string",
    "existing_part": true,
    "folder_applied": true,
    "folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "part_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "part_number": "BRACKET-001",
    "previous_folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "requested_folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  },
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "queue_position": 0,
  "status": "string"
}