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 theX-API-Keyheader (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;limitcaps 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. |
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 \
-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
}
Link to a calculation's own sheet for cutting
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"
}