Resources
Changelog
Notable changes to the ARCNM API, SDKs and developer platform. The reference is generated from the OpenAPI spec; this page records the changes behind it.
Notable changes to the ARCNM API, SDKs, and developer platform. The API reference is auto-generated from the OpenAPI spec, so it always reflects the live API; this page records the changes behind it.
The public OpenAPI spec at
/api/v1/openapi-public.jsonis the machine-readable source of truth — any change to the API surface appears there first.
2026-10-10
Fixed — The notes behind a whole piece and a stated sheet size are
published. analytics.pipeline_notes now carries
stock_full_piece_charged when a lot pays its last started sheet or bar
whole (kind, pieces_charged, pieces_started, unused_kg, credited,
override — true when the calculation's own full_stock_charge decided
it, not the environment's full_stock_threshold),
stock_share_kept_by_override when the threshold was reached and
full_stock_charge share kept the share (kind, remainder,
full_from), and custom_format_surcharge when a sheet size the
calculation stated carries the environment's format premium on the
Subcontract line (pct, surcharge_eur). The 2026-10-09 entries below
named these facts; the API dropped the notes until now.
2026-10-09
Added — A lot pays a started sheet or bar whole once it fills most of
it. For a lot, parts fill whole stock pieces — sheets, bars — and start one
more. Until now every lot paid its share of that started piece; now, once the
lot's parts fill the environment's full_stock_threshold of it (a
fraction of the parts the piece holds; the platform's default is 0.85, so
85 %), the piece is the lot's: the lot pays it whole, and the unused cells are
credited as scrap where scrap is credited. Below the threshold the lot pays
its share as before. A lot of whole pieces, or a piece that holds one part,
prices the same as before. The threshold is a tuning knob on the costing
environment (GET/PATCH /environments/{env_id}/tuning, 0.5 to 1.0;
1.0 turns the rule off), inherited down the environment chain like every
knob.
Added — full_stock_charge on a calculation. The per-calculation
word for the rule above, on POST /calculations and on the calculation's
detail and list rows: auto (the default) applies the
environment's threshold; full charges the started piece whole 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. On a re-run
(derived_from_calculation_id) omitting it keeps the word of the
calculation re-run, as the sheet-format pin is kept. A word other than
auto is part of the calculation's identity, so two otherwise identical
runs under different words are two calculations.
Added — Price a calculation on a sheet size of your own.
POST /calculations takes stock_format_mm ({length_mm, width_mm}): 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; a sheet the part does not fit falls
back to the usual choice and analytics.pipeline_notes says why. The size
is echoed on the detail and the list, is part of the comparison's
configuration, and a re-run (derived_from_calculation_id) keeps it unless
the request states stock_format_mm or stock_format_id — null included.
A sheet cut to size carries the supplier's cut-to-size fees
(material_provision:cut_to_size) on the Subcontract line, shared by the
parts one sheet holds; the cut_to_size_provision note names the split.
Added — custom_format_surcharge_pct on GET/PUT /environments/{env_id}/tuning. The premium a sheet cut to an ordered size
carries on its metal over the same metal in a stocked format, as a fraction
(0.15 = 15 %). It is read by the app's sheet comparison when it proposes a
custom size — a proposal must beat the best stocked format after this
premium and the cut-to-size fees — and by a calculation priced on a sheet
size of its own, which books the premium on the Subcontract line beside the
cut-to-size fees (the custom_format_surcharge note names it). reset
accepts the name.
Added — A sixth shop-practice answer, rotation_step. PUT /environments/{env_id}/shop-practice takes rotation_step (standard,
step_45, step_30, step_15, free): how far a job nest may turn parts
beyond quarter turns. Single-part calculations keep quarter turns and record
no such key; a stated grain direction keeps its two turns. No price moves.
Changed — A direct upload says how to send the file, and where it
stands. POST /uploads/presign answers method PUT or POST (now an
enum): PUT takes the file's raw bytes with the headers named, POST a
multipart/form-data form with every entry of fields and the file last,
as file. The guides said to PUT the file whatever method answered;
branch on method — a file sent the other way never arrives. POST /uploads/confirm now answers status: pending while the file is checked,
confirmed once it can be attached. It answers 409 with details.reason
upload_not_received when no file has arrived (it answered success), and
upload_rejected, upload_failed or upload_cancelled for an upload it
cannot confirm (it queued it again and answered success). POST /parts/{part_id}/revisions/{revision_id}/datasets answers 409 upload_pending with Retry-After (and details.retry_after_s) while the
upload is checked — it answered 400 — and 409 with the reasons above
for an upload that will not be confirmed. Until it is confirmed, an upload
reserves its declared size_bytes of your plan's storage (the most its file
type allows when none is declared), and confirm checks the plan again on
the size that arrived (402 entitlement_exceeded). See
Uploading files.
2026-10-07
Changed — GET /environments/{env_id} answers
environment_not_found. An environment id that does not exist in your
organization, another organization's included, now answers 404 environment_not_found with details.costing_environment_id:
the answer the calculation routes already give for the same id. It used to
answer 404 not_found; the message is unchanged. If you branch on
error.code for this route, branch on environment_not_found.
Changed — An assembly priced only in part is never ranked as a
price. When a component of an assembly calculation does not price, the
assembly (assembly_state partial) states the cost of the components that
did: a lower bound, not a price. GET /calculations now returns its
offer_price as null, and sort=offer_price lists it last. On the
comparison (POST /calculations/comparison, GET /calculations/batches/{batch_id}/comparison) its cell's offer_price and
offer_total are null, while unit_cost keeps the lower bound; the cell
still never wins, anchors a delta or counts toward a basket. In
arcnm_compare_calculations and arcnm_rank_calculations it carries the new
assembly_partial flag, no offer_price, no rank and no delta, and
assembly_partial is a new incomparable_reasons value. The list and the
agent tools used to state its partial total as the offer price, so it could
sort or rank as the cheapest option. Read the assembly calculation for the
components that did not price.
Changed — An assembly states its components' changed inputs. On an
assembly calculation, inputs_changed_since_run now also covers its
components: a file attached to a component, or a correction applied to it,
after the component was priced. roles lists every role changed on the
assembly or on any component, latest_at is the newest change, and
corrections is the sum of the components' corrections. A correction filed
on the assembly's own revision no longer counts: it does not change the
assembly's price.
Added — Stop a job nest from MCP. arcnm_nest_run_cancel is the
agent's stop button for a job nest it started, as POST /nest-runs/{run_id}/cancel is for the API. By default (apply=false) it
only reads the job nest and says whether it can be cancelled; apply=true
(needs parts:write) cancels it: the materials and thicknesses already
nested keep their result, the others are cancelled, and every remnant it
holds goes back to stock. Cancelled before anything is nested, it gives its
run back to this month's allowance. A server running read-only refuses the
cancel. Calculations and uploads already had their cancel tools.
Removed (breaking) — queue_depth from the calculation capacity, and
queue positions now count only your own calculations. GET /calculations/capacity no longer returns queue_depth: it counted every
customer's queued work, not yours. queue_position — on a run's response
and in the 429 when your plan's pending bound is full — now always counts
your organization's own queued and running calculations, this one
included, as it already did with fair scheduling on; its ETA is divided
from that position. Read your own backlog from pending_count and queue
on the capacity response.
Removed (breaking) — The background-job list, and the jobs:read and
jobs:cancel scopes. GET /jobs and GET /jobs/{job_id} are gone: they
listed the platform's own background work, which is not part of the API.
Follow what you started by its own handle instead — a calculation with
GET /calculations/{calculation_id}, an upload in GET /uploads, a job
nest with GET /nest-runs/{run_id}. jobs:read and jobs:cancel are gone
from the scope list, the key and consent screens, and the OAuth
scopes_supported metadata; jobs:cancel never unlocked a route. A key or
grant that already carries them keeps working for its other scopes; a
request for them is ignored and they are never granted again. Stop
requesting them. Stopping your spend is unchanged: cancel a calculation
(POST /calculations/{calculation_id}/cancel, POST /calculations/bulk-cancel, POST /calculations/batches/{batch_id}/cancel)
or a job nest (POST /nest-runs/{run_id}/cancel) with parts:write, and an
upload with POST /uploads/{data_source_id}/cancel and uploads:write.
Removed (breaking) — job_id and enqueued_task from the calculation
status, and job_id from the upload confirm. POST /calculations, POST /calculations/quote, POST /calculations/{calculation_id}/run, POST /calculations/{calculation_id}/cancel and both upload-and-quote routes no
longer return job_id or enqueued_task: they named the background work
behind the run, which no route serves. The calculation's id is the handle;
every other field is unchanged. POST /uploads/confirm now answers
data_source_id — the upload's own id, as presign returned it — in place of
job_id. A client that read either field should read the calculation or
upload id it already holds.
Changed — A job nest buys one larger sheet where it costs less than smaller ones. Where the parts of a material and thickness, or the copies left over after their full sheets, fit one larger stocked sheet that costs less than the smaller sheets they would otherwise take, a job nest now cuts the larger sheet. It used to pick the cheaper format sheet by sheet — two sheets where one larger sheet held every copy — and then stated no saving for combining the orders. Job nests solved before keep their sheets and figures, so nesting the same orders again can state a larger saving. A part's calculation is priced as before. An assembly calculation nests its sheet components by the same rule (see 2026-10-05), so its price can now reflect the larger sheet.
Changed — The comparison ranks on the price each calculation
quotes. POST /calculations/comparison and GET /calculations/batches/{batch_id}/comparison now strike the best cell,
delta_vs_best, delta_vs_baseline, wins,
median_delta_vs_baseline_pct and basket_total on the offer price: the
figure the calculation itself headlines (analytics.offer_price), with the
surcharges its environment states on the cost sheet. Each cell carries it
as offer_price (per part) and offer_total (per lot). unit_cost and
total_cost stay on the cell, stated before those surcharges. The matrix
used to rank on unit_cost, so an environment that states its overheads
and margin as cost-sheet rows could be named the cheapest while its own
offer was higher. Where no environment states surcharges, the two figures
are equal and no result moves. The MCP tools arcnm_compare_calculations
(new offer_price_delta), arcnm_rank_calculations (ranked on
offer_price, new offer_price_delta_vs_best) and
arcnm_compare_environments (new offer_prices per part) follow the same
rule; their unit-cost figures remain as the secondary basis. In
arcnm_rank_calculations, unit_cost_delta_vs_best and
unit_cost_delta_vs_best_pct are now measured from the offer-price
winner, so they can be negative: an option that costs less to make but
more to buy, because its environment states larger surcharges. On GET /calculations, offer_price on a calculation priced before the offer
price was stored now falls back to its unit_cost instead of null — the
figure its own page headlines.
Added — Sort the calculation list by offer price. GET /calculations takes sort=offer_price, which orders on each item's
offer_price: the price the calculation headlines and the comparison ranks
on. sort=unit_cost still orders on the cost before the cost-sheet
surcharges. Like unit_cost, the offer price is null until a calculation
is priced, so those rows sort last and sort=offer_price takes no
cursor.
Added — Optimization directions say when their run predates a
change. GET /calculations/{id}/optimization now carries
environment_changed_since_run and inputs_changed_since_run: the values
GET /calculations/{id} states for the calculation the savings derive
from. arcnm_optimization_directions reads them from that response.
Changed — calibrated describes the price, as recorded when it
ran. confidence.calibrated on GET /calculations/{id} and calibrated
on every comparison cell now read the same record: whether a calibration
moved that price when the calculation ran, a per-driver calibration
included. Both are null for a calculation priced before this was
recorded: unknown, not uncalibrated. The detail used to recompute it from
the environment's calibrations at read time, so it could disagree with the
comparison and change when a calibration was published or retired later.
Re-run a calculation to price it under the current calibration.
calibration_mismatch now has one meaning for every kind of calibration:
the cell and its row's baseline disagree on calibrated. Two calibrated
cells with different multipliers are no longer flagged; a per-driver
calibration beside an uncalibrated cell now is.
Added — A cost-sheet machine row names the machine's class. The
per-machine rows of the cost sheet (label_id
calculations.cost_sheet.line.machine_group) carry
label_params.machine_klass beside label_params.machine: the same class
the machine library publishes as klass. A bought-in or sawn route
carries buy or saw instead, which are not machine-library classes. Use
it to name a catalogue machine in your reader's language and to keep a
machine your shop named itself verbatim. A calculation priced before this
change carries the name alone until it is re-run.
Changed — A what-if refuses when the environment changed after the
run. POST /calculations/{id}/preview and a saved scenario's result
answer 409 environment_changed_since_run (with
details.latest_at) when the calculation's own
environment_changed_since_run is set. The preview prices under the
environment as it is now while its baseline is the stored sheet, so even a
what-if that moved nothing reported the environment's change as its own
effect. Create and run a new calculation for the part revision; that one
can be adjusted.
Changed — Corrections to a part count as a changed input.
inputs_changed_since_run now also states corrections: how many
corrections were applied to the part revision's recognised geometry after
the run read its inputs, however they were applied (roles is empty when
only corrections changed). An identical calculation run after such a
correction is priced again rather than answered from the earlier run. A
what-if on the earlier calculation answers on the geometry that
calculation priced, or refuses with inputs_changed_since_run where that
geometry is not kept. started_at on a calculation
answered from an identical earlier one states when it was answered.
Fixed — An identical repeat is free whichever way the file was
uploaded. A file uploaded to a revision with POST /parts/{id}/revisions/{rid}/datasets/upload was stored without the content
digest the other upload routes record, so an identical repeat on it ran and
counted against your quota again, and a drawing uploaded that way did the
same for its whole revision. It is now recorded on every upload route.
Added — language on both upload-and-quote routes. POST /calculations/upload-and-quote (form field) and POST /calculations/upload-and-quote-json take language with the same default
(de) and bound as POST /calculations; the form used to drop it. A part
created by either route gets the first revision code POST /parts gives a
new part (A); a revision added to an existing part keeps its
time-stamped code.
Changed — The dataset role is a published enum. POST /parts/{id}/revisions/{rid}/datasets (role in the body) and POST …/datasets/upload (role query parameter) take cad_3d, drawing_2d,
mesh, rfq_text or other; POST /calculations/{id}/inputs takes
drawing_2d, mesh or rfq_text. Any other value is the standard
422 unprocessable_entity instead of a 400.
Changed — setup_cost is the cost sheet's setup line times the
lot, and stored calculations are restated. setup_cost on GET /calculations/{id}, on every comparison cell, and in the MCP tools
arcnm_explain_cost and arcnm_cost_breakdown is now the setup line of
the calculation's cost sheet times its lot_size: in the calculation's
currency, with its calibration applied, on the same basis as
unit_cost. It is already part of unit_cost and total_cost; don't
add it on top. It used to be stated before calibration and before the
conversion into the calculation's currency, so on a calibrated
calculation, or one priced in another currency than its environment's,
it matched neither the setup line nor currency. Calculations already
stored are restated from their own cost sheet, so past values move: if
you copied setup_cost into an ERP or a report, read it again. A
calculation priced before the cost sheet was stored, whose figure was on
the other basis, now answers null; re-run it to get the figure.
Changed — finishing_time_s is the finishing time as billed.
analytics.time_breakdown.finishing_time_s now states the finishing
seconds per unit under the machine's allowance factor: the same seconds
the cost sheet's finishing line and the lot-size curve bill. It used to
state the time before that factor, so on a part with a finishing pass it
now reads higher by the allowance. The new finishing_base_time_s
carries the time before the factor — the figure finishing_time_s stated
until now. Calculations already stored are restated when read. If you
applied the allowance yourself, stop: read finishing_time_s as billed,
or finishing_base_time_s for the base time.
Changed — A curve point's direct_unit_cost is on the unit_cost
basis. On analytics.lot_size_curve.points, direct_unit_cost is now
the direct cost per unit at that lot size in the calculation's currency,
calibrated like unit_cost, with the setup amortised over the lot:
everything before overhead, so the gap up to unit_cost is the variable
and fixed overhead. At the ordered lot it equals
cost_decomposition.direct_unit_cost. It used to be stated before
calibration, so on a calibrated calculation it sat on another basis than
the unit_cost beside it. On calculations priced before this change the
field is left out rather than published on the old basis; re-run one to
get it.
Changed — Folder summaries total in the currency their parts are
priced in. GET /folders/summary and GET /folders/{folder_id}/summary
now total each quoted part on its own calculation: its spend is its
unit_cost times its annual volume, in the calculation's currency.
currency names the currency of the totals; it used to say EUR
whatever the parts were priced in, and it is null when nothing in
scope is quoted. The new currencies lists every currency the quoted
parts in scope are priced in. When it lists more than one,
total_annual_spend_eur, potential_savings_eur and
effective_annual_cost_eur are null: amounts in different currencies
are never summed. The _eur suffixes are kept for compatibility; the
values are in currency. Read currency, or currencies, before you
display or add up a total.
Changed — The wallet balance on GET /calculations/entitlement is
nullable. wallet_balance_cents and wallet_overdraft_cents are now
typed integer | null and are null for every API key and agent grant;
the guidance sentence no longer quotes the balance either. Only a user
signed in to the app sees the figures. wallet_funded_calculations and
wallet_overdrawn are unchanged and still tell an integration whether a
calculation will run. Treat both fields as nullable and read the run
decision from remaining, over_quota, hard_capped and
wallet_overdrawn, not from the balance.
Removed — The wallet:read and audit:read scopes. Neither
granted access to anything a key or agent grant can call: the wallet and
audit routes answer only a user signed in to the app. They are gone from
the scope list, the key and consent screens, and the OAuth
scopes_supported metadata. A key or grant that already carries them
keeps working for its other scopes; a request for them is ignored and
they are never granted again. Stop requesting them.
Changed — Routes under a part revision answer the revision's 404.
GET /parts/{id}/revisions/{rid}/datasets used to answer 200 [] for a
revision that does not exist in your organization, and GET /geometry/{rid}/faces-url and GET /geometry/{rid}/drawing-url answered
200 with status no_dataset. All three now answer 404 not_found with the message Part revision not found, the
same answer as GET /parts/{id}/revisions/{rid}. So do the writes under
the revision (POST …/datasets, POST …/datasets/upload, PATCH and
DELETE …/datasets/{link_id}), including when {id} is not the
revision's part. An empty list or no_dataset now always means the
revision exists and has no such file. Query filters are unchanged: an id
in ?env_id=, ?part_id=, ?folder_id= or ?part_revision_id= that
matches nothing in your organization still lists nothing.
Security — Every key and agent grant is held to its scopes. A
missing scope answers 403 insufficient_scope whichever
account created the key or consented to the grant; there is no longer an
exception for any account. A request that worked only through that
exception now gets the 403: add the scope to the key, or grant it again
with the scope.
Security — A viewer cannot change data. A member with the
viewer role — signed in, or through an agent grant they consented to —
now gets the new error code 403 role_read_only (with
details.role) on every request that changes organization data. Reads,
and the few actions on the viewer's own state (marking their
notifications read, revoking their own grants), are unaffected. Give the
user the member role if they need to make changes.
Changed — /readyz answers its status only. The readiness probe
keeps its 200/503 status code and {"status": …} body, but no longer
returns per-dependency detail to anonymous callers. Probe on the status
code.
Changed — Every failed MCP tool call answers the API's error
envelope. An isError: true result now carries {"error": {"code", "message", "details", "doc_url"}, "request_id"} as its JSON text: for a
generated tool, the body the REST route answered, unchanged; for a curated
arcnm_* tool, the route's error.code and details.reason with a message
written for the agent. It used to be prose, with the REST body quoted inside
it for a generated tool. An error result carries no structuredContent.
Parse the text and switch on error.code; code that matched the old prose
breaks. See MCP.
Changed — Five MCP list tools answer a page. uploads_list_uploads,
environments_list_environments, tools_list_tools,
materials_list_stock_formats and machine_library_list_library now answer
{"data": [...], "next_cursor": ..., "has_more": ...} and declare it as
their outputSchema. They used to answer {"result": [...]} and gave no way
to reach the next page. Read the rows from data, and pass next_cursor
back as cursor until has_more is false. Over REST these routes still
page in the X-Next-Cursor / X-Has-More headers.
Removed — The multipart upload tools over MCP.
calculations_upload_and_quote, calculations_upload_inputs and
datasets_upload_dataset are no longer MCP tools: an MCP client sends JSON
arguments, so each answered 422 on every call. Use
calculations_upload_and_quote_json with the file as base64, or
uploads_presign, a PUT of the file, uploads_confirm, then
datasets_attach_dataset with the file's role. The REST routes are
unchanged. See MCP.
Changed — The identity tools are listed to a signed-in user only.
users_read_user_me and users_read_user_me_context are no longer listed
to an OAuth grant with scopes; API keys already did not see them. Both
routes answer 403 to every API key and every scoped grant, so the listing
now matches what each credential can call. For the same reason an API key
with no scopes is listed only the tools that need no scope.
Changed — Curated MCP prices say when they predate a change.
arcnm_explain_cost, arcnm_cost_drivers, arcnm_cost_breakdown,
arcnm_optimize_lot_size, arcnm_optimization_directions, and each side of
arcnm_compare_calculations and arcnm_rank_calculations now carry
environment_changed_since_run and inputs_changed_since_run, the two
facts GET /calculations/{id} carries. When either is set, the tool's
guidance says so and tells the agent to run a new calculation of the part
revision before quoting the price. arcnm_optimize_lot_size also carries
the lot-size curve's accuracy and exact_at_quantity, and each curve
point its offer_price, as the API states them: the curve is exact at the
quoted lot and indicative at every other one.
Changed — The lot-size scenarios value each side of the supply on its
own basis. The buyer's scenarios (buyer_stock, make_to_order and the
buyer's half of joint) now price a piece, and value the buyer's stock, at
the purchase price after the supplier's discounts — the offer price less the
customer discount and the cash discount, as a buyer's purchase costing does —
instead of the offer price, which carries the customer-discount gross-up the
buyer never pays (+11.1 % at a 10 % discount). The stock the supplier holds
or still owns (supplier_stock, the work in progress under make_to_order,
the supplier's half of joint, and consignment) is valued at production
cost including material and production overheads — the lower bound HGB §255
allows for inventory, and the IAS 2 cost — instead of the manufacturing cost without them. Where the
environment states no costing scheme nothing changes. Where it states
discounts or overheads, the scenarios' per-piece figures, holding costs,
savings and recommended lots can move; the quoted price and every curve
point's unit_cost and offer_price do not. The lot-size optimum gains
valuation_basis — purchase_price, or offer_price on a calculation made
before (recalculate to restate it) — and supplier_value_to_cost_ratio;
price_to_cost_ratio is now what the buyer pays over manufacturing cost.
arcnm_optimize_lot_size states valuation_basis as well. The folder and
workspace summaries return potential_savings_eur and
effective_annual_cost_eur as null where the scope holds a part valued at
the offer price (a calculation made before) beside one valued at the
purchase price, as they already do across currencies; the annual spend still
sums.
2026-10-05
Changed — An assembly's own job nest belongs to the assembly. The
job nest an assembly calculation runs to nest its own sheet components
together is not listed by GET /nest-runs, which lists the job nests your
organization started. Reading it — GET /nest-runs/{run_id}, its cutting
files, its revisions, its rack labels — or changing it needs the Nesting
add-on; without it, each answers addon_required (402). Job nests you
started read exactly as before.
Changed — What a job nest saves is measured against each part
alone. Every sheet quote is priced on the part's true shape, so a job
nest's saved is now what nesting the orders' parts together saved
against cutting each part alone on its own true-shape nest: alone less
nested, on every line, order, job nest and in GET /nest-runs/savings.
The rectangular-blanks basis and the split are no longer served:
rectangular, rectangular_kg, rectangular_sheets, saved_true_shape,
saved_true_shape_kg, saved_combining and saved_combining_kg are gone
from savings, from a revision's savings and from the total. Job nests
stored earlier read the same way. A single line can still save — the job
nest may cut its last copies on a smaller sheet or a remnant.
2026-09-29
Changed — Adjusting a job nest's sheets keeps your grain direction
and your mirroring rule. POST …/layout-check now names every copy your
shop practice does not allow, per sheet in against_shop_practice: turned
off the grain direction the environment states (reason
grain_direction, with the turns that keep it in allowed_rot_deg), or
mirrored where the environment allows no mirroring (reason
mirroring). Such a layout is not ok, and POST …/revisions refuses it
with nest_layout_invalid (422). The rule is the one the job nest read
when it ran; where no grain direction is stated, any turn is accepted as
before.
Added — How your shop cuts sheet, as five shop-practice answers.
PUT /environments/{env_id}/shop-practice takes grain_direction
(along_length / across_width), mirroring (not_allowed),
min_web_by_gauge (by_gauge), micro_joints (used) and
common_line_cutting (used), each opt-in and standard today's
behaviour; GET reads them back through the parent chain like every other
answer. Two of them open tables on GET/PUT /environments/{env_id}/tuning: min_web_mm_to_3mm, min_web_mm_to_6mm,
min_web_mm_to_12mm, min_web_mm_over_12mm (the minimum web per sheet
thickness bought; each starts at the web you already nest at) and
micro_joint_width_mm, micro_joints_to_300mm, micro_joints_to_1000mm,
micro_joints_over_1000mm (the tabs that hold a part in the skeleton; recorded
on the layout and in the cutting files, never priced). Where common-line
cutting is stated, material_resolution.stock.common_line_length_mm states
the cut one part shares with its neighbours, in mm per part, the sheet's
cutting files mark those cuts on a CUT_COMMON layer (SVG group and JSON plan
sheets[].common_line alike), and the JSON plan states micro_joints where
they are used. Nothing here changes a price until an environment states an
answer.
Added — Stock formats for bar, plate and tube. GET /stock-formats
takes kind (sheet, plate, bar or tube; one kind per list) and
POST /stock-formats states it, each kind with its own dimensions: a sheet
its length_mm × width_mm and gauge band; a plate its thickness_mm and
the largest plate cut to size (length_mm × width_mm); a bar its
length_mm and, optionally, the one diameter_mm it is stocked in (null for
every standard diameter); a tube its length_mm, diameter_mm (outside) and
wall_mm. A row stating another kind's dimension is refused with the field
named. Hide, retire and the "at least one format" rule work per kind, and
stock_format_id on every create body may pin a row of any kind. One
standard bar is listed under bar: 3000 mm, every diameter and material —
the length every turned part has been priced on, so nothing re-prices. A shop
that adds its own bar length, tube or plate rows sees them priced: a turned
part takes the stocked length that bills the least per part, a hollow one the
smallest stocked tube section that holds it, a plate ordered cut to size is
named by the row that covers it. The stock on a calculation carries the row
(format_id, format_key, format_label) and the alternatives with each
one's verdict, as a sheet's does; stock_w_mm is null on a bar and a tube,
and each alternative states diameter_mm and wall_mm where it has them.
Once you list your own bar rows (or hide the standard one), the list states
the lengths a bar is bought in: the bar length under /tuning
(bar_length_mm) prices a bar only where nothing on your list holds the part,
and the clamping end applies as before.
Unstated, every bar, plate and tube price is unchanged.
Added — Quote a sheet part from its DXF flat pattern. Upload a .dxf
in the CAD slot (cad_file on POST /calculations/upload-and-quote(-json),
role cad_3d on POST /parts/{part_id}/revisions/{revision_id}/datasets)
with sheet_gauge_mm and a material, and the part is priced as a flat sheet
part cut to that pattern — nested on your stocked formats, the contours
cut, the skeleton scrap. Millimetre and inch files; one closed outer
contour per file, every other loop an opening; a file the import cannot
price is refused at upload with a dxf_* code that names why (see
Uploading files). application/dxf and
image/vnd.dxf are accepted at presign and on the CAD role. Such a quote
asks for a review: analytics.review.reason_codes carries
extraction_degraded with notes: ["dxf_flat_pattern"] — a flat part at
the stated thickness, with no 3D model, so no bends, forms or 3D
manufacturability checks.
Added — Rack labels and a scan lookup for remnants.
GET /remnants/{remnant_id}/label is a printable label (SVG at true size,
100 × 50 mm) with the piece's size, thickness, material, pieces, origin,
date and a Code 128 barcode of its code — the first 12 hex digits of its id.
GET /remnants/by-code/{code} answers the piece a scanned or typed code
names, in any state; GET /nest-runs/{run_id}/remnant-labels prints one
label per rest a released job nest put into stock. lang=de|en picks the
label's words and number format — 1.200 × 800 mm or 1,200 × 800 mm, as
the app writes a size.
Changed — What a job nest saves, basis "each part alone". The
"alone" basis now buys whole sheets of the stocked format that buys the lot
for the least at the part's own nest count on that format — never the
format the single-part quote chose for its lot-free share — so it never
exceeds the rectangular-blanks basis format for format, and the true-shape
saving is never negative on a line whose formats are kept.
savings.by_line[].alone names the format (format_id, sheet_mm).
Stored savings of earlier job nests are unchanged.
Changed — A release books stock only while the environment credits
remnants. While credit_remnant is unstated, a job nest is offered no
remnants and plans none, so its release consumes and creates none — it
still freezes the sheets and counts the saving. The job nest page and the
release dialog say which state the job nest stands under.
Added — Adjust a job nest's sheets, and see what nesting saves.
POST /nest-runs/{run_id}/groups/{group_id}/layout-check takes the whole
layout of one material and thickness — every sheet with the stock format or
remnant it is cut from and its placements — and answers what the cutter
would find: every pair of parts closer than the spacing with the gap
between them, every part in the margin, every line short or over its
quantity, every container the job nest cannot cut from, each sheet's
utilisation and rest; nothing is stored. POST …/revisions stores a layout
that passes as the material and thickness's next revision and makes it
the active one — the job nest's own layout is revision 1 — priced and
shared out exactly as the job nest's result is; GET …/revisions lists
them, GET …/revisions/{n} reads one with its placements, and the export
takes ?revision=n. A calculation priced on a line records the revision it
read (nest_revision) and keeps it. Savings are public now: on every
job nest and in GET /nest-runs/savings, the sheet bought on rectangular
blanks, with each part nested alone and nested together — kg and money,
whole sheets everywhere — with saved split into true-shape nesting and
combining the orders' parts, per order too; the total counts released job
nests only and states the simulations' potential apart. The agent tools
arcnm_nest_run_status / arcnm_nest_run_result carry a job nest's saving
and arcnm_nest_savings the total. Refusals: nest_run_group_not_found,
nest_run_revision_not_found (404), nest_run_not_editable,
nest_run_released, nest_run_remnant_unavailable (409),
nest_layout_invalid (422). Nothing here changes a calculation's price.
Changed — A job nest books stock only when it is released. Solving
now reads the environment's remnants as candidates and plans which it
cuts; nothing leaves or enters stock until the shop releases the job nest
in the app, which consumes the planned remnants and puts the reusable
rests in stock in one step — a remnant no longer in stock refuses the
release by name. A released job nest is frozen. remnants_held on a job
nest is always empty from now on.
Added — Cutting files from a job nest.
GET /nest-runs/{run_id}/export writes the sheets of every solved material
and thickness of a job nest for the cutting machine: format=dxf (R2010 in
millimetres — one block per part, one insert per copy, on the layers
SHEET, MARGIN, REMNANT, CUT_OUTER, CUT_INNER and LABEL;
exploded=true writes plain closed polylines in place for a system that
reads no blocks), format=svg, or format=json — the nest plan (schema
v1): every part's contour and holes, every sheet's container, material,
thickness, allowances and placements. sheet=all answers every sheet (a
zip of one file per sheet with plan.json and manifest.csv, or the whole
plan); sheet=<group>:<index> one sheet's file. GET /calculations/{id} /sheet-nest/export does the same for the sheet a calculation's own price
laid its part out on. Contours are within 0.2 mm of the unfolded flat
pattern; arcs are polylines. Your own layouts are yours to read: by API key
and through the agent tools arcnm_nest_run_export and
arcnm_sheet_nest_export, which answer the plan inline and DXF/SVG as
download links. Refusals: nest_run_nothing_to_export (409),
nest_run_sheet_not_found (404), sheet_layout_not_found (404). Nothing
here changes a calculation's price. See the recipe
Cut a job nest.
2026-09-28
Changed — One job nest at a time, and a ceiling on its time.
POST /nest-runs refuses a start while a job nest of the organization is
queued or running (nest_run_in_flight, 409 — wait until it ends, or
cancel it), and a group_budget_s past what the organization may ask for
per material and thickness (nest_run_budget_too_long, 422; the default
ceiling is 300 s). A job nest's time is computing time: a material and
thickness ends on its budget however busy the platform is, rather than on
a wall clock that ran while it waited for a core. A job nest never buys
more sheet than cutting each of its parts alone would: where nesting the
parts together would not save, each part is laid out on its own sheets.
Nothing here changes a calculation's price. See Errors.
Added — Nest a backlog of sheet-metal parts together.
POST /nest-runs nests several orders, materials and thicknesses at once,
on the sheets and the remnants your costing environment stocks: name the
calculations (each with an optional quantity — its lot size when
omitted — order_ref, due and priority), a batch_id or a
folder_id. The job nest runs in the background; GET /nest-runs/{run_id}
reads how far it has got and what it came to — the sheets of each material
and thickness with their utilisation, each line's and each order's share of
the sheets (purchased, less the scrap_credit the skeleton sells for and
the remnant_credit the reusable rests are worth back in stock, in total
and per copy), what a batch or folder left out and why, and the remnants it
used, holds and created. allocation_rule shares each sheet's cost by the
parts' area or equally. POST /nest-runs/preview answers what a job nest
would take — its lines and copies per material and thickness and the
remnant pieces of each in stock — without starting one; GET /nest-runs
lists your job nests and POST /nest-runs/{run_id}/cancel stops one. A job
nest cuts from your remnants before it buys a sheet: a remnant it cuts from
leaves stock, and the reusable rests its sheets leave go in. It never
changes a calculation's own price. Starting and previewing a job nest each
count as a heavy request against the rate limit.
Added — Price a part at its share of a job nest's sheets. The
calculation create bodies take nest_item_id, the id of a line of a job
nest (items[].id): the part's material is then its share of the sheets
the job nest cut it from, less its share of the credits, and everything
else is priced as usual. A part never pays more material this way than
nested on its own: where the shared sheets are only partly filled, it pays
its own share of a sheet, and the rest of the sheet counts as back in
stock.
analytics.material_resolution.stock.layout_method says so with a new
value, shared_sheets. A line that cannot price the calculation is refused
with 422 nest_item_unresolved, details.reason saying
why.
Added — Remnant stock. GET /remnants lists the remnants of your
environments, POST /remnants states one your shop holds, and
POST /remnants/{remnant_id}/scrap takes pieces of one out of stock. A
remnant is never deleted, so a job nest that used or left one still names
it.
Added — Job nests over MCP. arcnm_nest_run_create plans a job
nest by default and starts it on apply=true (it needs parts:read and
parts:write, and is annotated destructive, since a job nest takes the
remnants it cuts from out of stock); arcnm_nest_run_status says how far
it has got and arcnm_nest_run_result what it came to, each line with the
id a calculation names as nest_item_id. The generated /nest-runs and
/remnants tools are not on the agent surface. See MCP.
New error codes: nest_run_not_found, nest_run_selection_not_found,
nest_run_items_unusable, nest_run_nothing_to_nest,
nest_run_selection_too_large, nest_run_line_too_large,
nest_run_too_many_copies, nest_run_not_cancellable,
nest_item_unresolved, remnant_not_found, remnant_not_in_stock and
remnant_unresolved — see Errors.
Changed — A sheet part is bought at the count a nest holds. Where
blank rectangles laid in both orientations fit more of them on a sheet
than rows of one orientation do, or where a part cut from a flat pattern
fits more copies of its own outline, the sheet is nested that way and the
part is bought at that count.
analytics.material_resolution.stock.layout_method says which, with two
new values: mixed_rectangles (blank rectangles in both orientations) and
true_shape (the part's own outline, nested). rectangle_estimate still
prices wherever no nest fits more parts. Less sheet is bought per part, so
the material cost, and the unit cost with it, falls on most of these
parts. arcnm_cost_breakdown reports the same layout_method in
material_stock.
Added — Why a part was not nested to its own outline.
analytics.pipeline_notes carries sheet_nest_downgraded when a part cut
from a flat pattern could not be nested to its own outline on the sheet it
is bought on, and a rectangle layout priced it instead. reason says why:
outline_invalid (its outline could not be read as one closed contour),
nest_budget_exceeded (the search for a layout reached its limit) or
layout_invalid (the nested layout did not keep the spacing or the edge
margin). layout_method names the layout that priced it.
Changed — A calibration is fitted again, on this release's quotes.
As on 2026-09-26, a calibration saved before this release no longer
applies: the environment quotes uncalibrated until its calibration runs
again. POST /calculations/{id}/preview refuses a calculation priced
before this release with
409 priced_under_earlier_model.
2026-09-26
Added — A calculation names the stock it bought, and where its
metal went. analytics.material_resolution.stock states the stock a part
is bought as — its kind (sheet, bar, block, tube or compound),
the stock format and who chose it (format_source: requested,
own_format or standard; bar, block and tube stock is bought in standard
sizes), the size bought beside what the part measured (measured_mm,
size_rounded_up; neither is stated where the part's thickness was
estimated from its shape rather than measured), a tube's wall_mm, a
compound's stock_pieces, parts_per_stock and how that count was
arrived at (layout_method), the format a calculation asked for when it
did not price the part and why (requested_format_label,
pin_unavailable_reason), and the bed of the machine that bounded the
formats on offer (machine_bed_l_mm, machine_bed_w_mm) — and where its
metal goes: purchased_mass_kg = part_mass_kg + scrap_mass_kg +
remnant_mass_kg, in kilograms whatever the calculation's currency, with
utilisation_net (the part's area over the sheet area it used up: its
share of the sheet less a reusable remnant that goes back to stock) and
utilisation_gross (over its whole share) on sheet.
analytics.cost_decomposition adds material_purchased_cost and
remnant_credit: material_cost is the first less the second, so the two
restate it and are not added to the unit cost again. scrap_credit is a
cost-sheet row of its own that herstellkosten subtracts, and
material_overhead is charged on the net material — material_cost less
scrap_credit — never on the stock bought. analytics.assumptions says whether each credit applied
(scrap_credit_stated, remnant_credit_stated), the price each kind of
scrap was credited at (scrap_prices) and what a reusable remnant is worth
(remnant_value_factor); each entry of scrap_prices states the mass
of that scrap (mass_kg), so the credit is the sum of mass × price,
capped at the material cost. A price is the dated scrap price for that
kind of scrap, else the environment's flat return per kilogram
(price_source env_setting). The cost sheet's material lines and the
improve_material_utilization finding carry the masses they multiply —
the scrap credit's row the mass of the scrap credited
(credited_scrap_mass_kg), which is less than the stock's scrap_mass_kg
wherever some scrap is not credited — and arcnm_cost_breakdown returns,
as material_stock for every persona, the stock (its kind, format and who
chose it, size, what the part measured, a tube's wall, parts per stock and
a compound's stock_pieces, layout_method), its mass balance with
utilisation_net, and the material line's purchase and credits with the
two credit flags; the rest of the stock and the scrap prices are on the
calculation itself. A calculation priced before this release states its
stock and the masses bought and shipped, without the scrap and remnant
masses and without a bar's length, and a compound's body count as its
stock_pieces.
Added — Credit your scrap and your remnants against the material
bought. Two shop-practice questions switch the credits on:
scrap_credit (you sell the scrap your parts leave) and remnant_credit
(usable remnants go back into stock: a rest of a sheet, or the end a bar or a
tube leaves past its part once it is at least bar_remnant_min_l_mm long),
each answered credited or
standard on PUT /environments/{env_id}/shop-practice. An environment
inherits a parent's answer, and GET reports it with
source: parent_environment and the environment that states it
(source_environment_id, source_environment_name). What your scrap dealer pays is stated per
material and kind of scrap — sheet_skeleton, chips, drop — with
PUT /environments/{env_id}/rates/scrap/{material_category}/{scrap_class}
(?grade_id= prices one grade apart from its category) and ended with
DELETE on the same path; GET /environments/{env_id}/rates/scrap lists
your environments' prices in force today, each row with the key its price
was stated for (stated_grade_id: a grade with no price of its own is
priced by its category's, which is where that price is changed or ended),
whose price it is (price_source, source_environment_name), the date it
describes (as_of) and, for a published reference price, its age
(age_days; stale past 90 days); credit_scrap_stated says whether the
environment credits scrap at all. The three routes are exposed over MCP
like the material-price pair. A price starts today or later,
never earlier, so quotes already made keep the price they were costed at,
and each refusal has its own code in the errors guide
(scrap_price_backdated, scrap_price_grade_category_mismatch,
scrap_price_not_found). Rate
rows list scrap as a kind; POST /environments/{env_id}/rates and
DELETE /environments/{env_id}/rates/{kind}/{rate_id} refuse it
(scrap_price_use_scrap_route), and DELETE /materials/grades/{id} counts
a grade's scrap prices among the references that keep it. Where a kind of
scrap has no price of yours, a parent environment's applies, then a
published reference price where one exists; one with no price at all is
credited nothing. Until an environment answers credited, no quote
changes.
Added — A credited material line shows what it is made of. Where
an environment credits its remnants, the cost sheet's material line
(GET /calculations/{calculation_id}/cost-sheet, analytics.cost_sheet)
has two children — material.purchased and material.remnant_credit,
the credit signed negative so the children add up to the line — and
states its arithmetic as formula_id purchased_minus_credits. Where it
credits its scrap, the sheet carries the scrap_credit line: one negative
row below the unit cost, taken off before the material overhead and a
material scrap allowance are charged. Each row's door is where its
figure is changed: the material price, the remnant value factor on
PUT /environments/{env_id}/tuning, and for the scrap credit the scrap
price
(PUT /environments/{env_id}/rates/scrap/{material_category}/{scrap_class},
with material_category and, where one kind of scrap was credited,
scrap_class in its params) or, where the environment's flat return per
kilogram priced it, the environment's economics. Their label_params
state the mass each row multiplies — raw_stock_mass_kg,
credited_scrap_mass_kg, remnant_mass_kg — with scrap_eur_per_kg
where one price applied and remnant_value_factor. Without a credit the
sheet is unchanged. POST /environments/{env_id}/rates/restore returns the scrap
prices to the platform's as well: none of the environment's own scrap
prices applies after it. arcnm_apply_adjustment saves no scrap price from
a cost sheet: it refuses one and names environments_scrap_price_set, the
tool that states it.
Added — Sheet nesting settings. PUT /environments/{env_id}/tuning
takes six sheet settings, and GET reports each beside its platform
default: edge_margin_mm (kept free on every side of a purchased sheet),
skeleton_web_mm (the web between two nested parts, without the cutting
gap, which is added to it), cut_to_size_increment_mm (the step a plate
ordered cut to size is bought in), and the size a leftover strip needs to
count as a reusable remnant (remnant_min_l_mm, remnant_min_w_mm,
remnant_min_area_share). remnant_value_factor says what any reusable
remnant, a rest of a sheet or the end of a bar or tube, is worth against new
stock. The remnant settings apply only where remnants are credited. reset
clears any of them by name.
Added — Bar stock settings. PUT /environments/{env_id}/tuning
takes the length you buy round bar in (bar_length_mm, 3,000 mm unless you
state another), the end of each bar its clamp holds, which no part is cut
from (bar_clamp_remnant_mm, 150 mm), and the shortest end a bar or a tube
leaves past its part that goes back into stock (bar_remnant_min_l_mm,
300 mm), and GET reports each beside its platform default. A turned part
carries its share of one bar of that length. Where remnants are credited, a
bar's end past its last part and the clamping end, or a tube's cut-off, that
reaches the minimum is credited like a sheet's rest: remnant_mass_kg and
remnant_credit state it. reset clears each by name. Until an environment
states them, no quote changes.
Added — New codes that explain a material line.
analytics.pipeline_notes carries stock_beyond_ladder when a part
measures past the largest standard size of its stock and is bought at its
own measurement (kind, measured_mm, largest_rung_mm, bought_mm),
and — only in an environment that credits sold scrap —
scrap_price_unresolved when a kind of scrap found no price and is
credited nothing (material_category, scrap_class) and
scrap_price_stale when a published reference price more than 90 days old
credited the part (material_category, scrap_class, price_source,
as_of, age_days). GET /environments/{env_id}/audit reports such stale
reference prices as the finding scrap_price_stale, whose params name
the material and the kind of scrap (material_category, scrap_class),
the day the oldest of them describes (observed_on), its age in days
(age_days) and how many prices it covers (count).
analytics.review.reason_codes gains stock_format_not_stocked when your
stock formats hold nothing in the part's thickness or material, so the
part is priced on a plate ordered cut to size (kind, gauge_mm,
measured_gauge_mm, material_category).
Added — The currency conversion a calculation's amounts carry. A
cost-sheet line states its masses and prices in the environment's rate
currency (analytics.assumptions.rate_currency) and its amount in the
calculation's currency. analytics.assumptions.currency_factor states the
factor between the two — 1.0 when they are the same — so on a calculation
in another currency a material row's mass × price × currency_factor is
its amount, and a line's time × rate the same way; on a calibrated
environment the amount also carries the calibration the line was charged
at. arcnm_cost_breakdown returns the same factor as currency_factor,
beside currency.
Changed — Sheet and bar handling is shared among the pieces your
stock actually yields. On a laser, plasma, waterjet, turret punch or
shear, one sheet exchange is now divided among the parts nested on the
sheet the calculation buys — the same format and count the material line
bills — instead of among the parts a sheet the size of the machine's bed
would hold; a plate ordered cut to size is one part per exchange. A saw
cutting a turned part's blank shares one bar clamping among the pieces cut
from the bar the material line bills. Load/unload time, and the unit cost
with it, moves on those parts — mostly up on sheet, where the bought sheet
holds fewer parts than the bed — and choosing another format with
stock_format_id now moves the handling as well as the material.
analytics.time_breakdown.derivations.load_unload.parts_per_fixture
describes the pieces that share one loading at the order quantity.
Changed — A plate ordered cut to size carries the supplier's cutting
charge. When no sheet format holds a part's blank and the plate is
ordered cut to size, the quote books the supplier's cutting service on
subcontract_cost — its minimum charge per plate and its charge per
order — as it already does for a blank no saw of yours can cut; the
cut_to_size_provision note says so, with kind: "sheet" on the plate.
Its new at_floor is true wherever the fee per piece is the service's
minimum charge rather than a price for the cut: on every plate, and on a
sawn blank whose cut would cost less. Unit costs rise on those parts.
Changed — A calibration is fitted again, on this release's quotes,
before it applies to this release's prices. This release prices material
differently, and a calibration corrects the prices it was fitted against,
so one saved before it is no longer applied: the environment quotes
uncalibrated until its calibration runs again.
GET /calibration/environments/{env_id}/next-best-evidence asks for that
run first (recalibrate_after_model_update). A run compares each actual
only with a quote this release priced, because a fit against an older
quote would correct a price the model no longer makes:
auto-calibrateandauto-calibrate-from-erpreport a part whose quote at that lot size predates this release inunmatched_part_ids, like a part never quoted, and refuse when no part is left — quote the parts again, then retry.POST /calibration/importscounts such a part incalculations_requiredand prices it before it fits. Posting a file again that you imported before this release starts a new import instead of returning the old one.POST /calibration/environments/{env_id}/finalizefits only the observations submitted since this release, says inmessagehow many it left out, and refuses when none is left: submit them again throughPOST /calibration/outcomeswith predicted values from today's quotes.POST /calibration/environments/{env_id}/teachrefuses a part the environment has quoted only before this release, naming it.POST /calibration/environments/{env_id}/runs/{run_id}/revertrestores a run's calibration without the adjustment calibration no longer makes (the material quantity, now modelled), and answers409when the run fitted nothing else, leaving the active calibration as it was. The app's full calibration report lists that adjustment underretired_drivers; an API key or an agent reads the report's status-only view, as before.
Changed — A what-if needs a calculation this release priced.
POST /calculations/{id}/preview refuses a calculation priced before this
release with 409 priced_under_earlier_model: its cost
sheet is what the earlier version charged, so a new price worked out today
would move by everything the release changed, not only by the factor you
moved. Create a new calculation for the part revision and run it; that one
can be adjusted. A scenario kept on such a calculation records the same
code in its result.refused, and arcnm_preview_cost refuses the same
way.
Fixed — Tuning shows the figure a child environment is priced
with. GET /environments/{env_id}/tuning, and the response of PUT,
showed a knob that only a parent environment states at the platform
default, while the child's calculations used the parent's figure. Every
knob now reads the way calculations read it: source is
environment_override for this environment's own value,
parent_environment for one it inherits, else platform_default, and
platform_default keeps showing the platform's own figure beside it.
Naming a knob in reset clears this environment's own value, so the
knob inherits again — the parent's figure where the parent states one.
learning_rate belongs to each environment and is not inherited.
Fixed — PUT /environments/{env_id}/shop-practice said that
standard restores the platform behaviour. It removes this environment's
own answer: where a parent environment answers the question, the parent's
answer applies, and the response returns it with source
parent_environment.
Fixed — A calibrated calculation's cost breakdown is its cost
sheet. On a calibrated environment analytics.cost_decomposition scaled
every line by one factor, the calibrated price over the uncalibrated one.
A calibration per cost driver charges each line at its own factor and
leaves material alone, so that single factor put the machine and labour
calibration on material_cost, material_purchased_cost and both credits,
and angebotspreis differed from analytics.offer_price; on an
environment calibrated by one factor on the whole price, freight_cost,
duty_cost and the totals above them carried a calibration they are never
charged. Every line is now the amount the calculation's cost sheet charges
for it, the surcharges and totals are the sheet's, and angebotspreis
equals offer_price. The outsourced steps' charges
(secondary_route[].cost_per_part, process_plan.operations[] .subcontract_per_part) follow the subcontract line they make up, and
arcnm_explain_cost states the same breakdown. Uncalibrated calculations
are unchanged.
Fixed — Every lot size is stated in the calculation's currency. On
a calculation in another currency than its environment's,
GET /calculations/{id}/cost-sheet?quantity= answered a quantity off the
lot-size curve (basis: restated) in the environment's currency under the
calculation's: the sheet, the point beside it — whose unit_cost stood
above the sheet it explains — and arcnm_cost_breakdown at that
quantity. A restated quantity is now on the basis of the priced lot and
the curve points. Calculations in their environment's currency are
unchanged.
Fixed — arcnm_cost_breakdown with a quantity other than the priced
lot returned offer_price null. It now states that lot's offer price, the
figure GET /calculations/{id}/cost-sheet?quantity= returns as
point.offer_price.
2026-09-25
Added — Stock formats: the sheets your shop buys. GET /stock-formats
lists the sheet formats calculations price with — the standard formats and
your own — and every row says in resolution what calculations do with it
(priced, suppressed, closed); ?at=YYYY-MM-DD reads the list as it
stood on an earlier date. POST /stock-formats adds a format of your own,
optionally bought only in a thickness band or one material category;
PATCH /stock-formats/{id} edits it, and a new size, band or material
starts a new version under the same key, so calculations priced on the
old one keep their numbers. A label is stored as it reads: direction
controls and zero-width characters are removed, and a label with nothing
left to read is refused (422). DELETE /stock-formats/{id} retires it. A
standard format your shop does not buy is hidden with
POST /stock-formats/{id}/suppression and shown again with DELETE on the
same path. GET /stock-formats/{id} reads any version, retired ones
included. Each refusal has its own code in the
errors guide.
Added — Choose the format a calculation is priced on.
stock_format_id on POST /calculations, /quote, /batch and
/upload-and-quote(-json) prices the part on that format instead of the
one that uses the least material. An id that names no format your
organization prices with today is refused with
422 stock_format_unresolved before anything is stored; an id from an
older version of a format you still buy prices on its current version.
When the part cannot be cut from the chosen format — or is not cut from
sheet at all, being milled, turned or bought finished — the calculation is
priced as if none had been chosen, and analytics.pipeline_notes carries
stock_format_pin_unavailable with the reason. The calculation detail and
list echo stock_format_id, and a re-run with
derived_from_calculation_id keeps it unless the request states another
format — or null. A comparison cell pinned to another format than its
row's anchor is flagged config_mismatch.
Added — When your formats decide how a sheet part is bought,
analytics.pipeline_notes says why the part was priced as a cut-to-size
plate instead: stock_format_not_stocked (no format on your list is
bought in the part's thickness or material),
stock_format_machine_holds_none (the machine cannot load any of them) or
stock_format_no_fit (none is big enough). An organization that has
neither added a format nor hidden one gets none of these.
Added — The calculation detail echoes material_is_provided,
provided_stock_kind, surface_treatments and language, as it does
region, so a re-run built from it is asked what the first run was asked.
Fixed — An agent's re-run is priced on the first run's inputs.
arcnm_apply_adjustment with recalculate=true sent only the part, the
environment and the lot, so the new calculation fell back to the create
defaults: a customer-supplied part was billed its material, and the grade
the caller chose, the quote currency, the dataset and the language were
dropped. It now states them again from the calculation detail — the grade
only when the caller chose it — as the app's own re-run does, runs the new
calculation instead of leaving it an unpriced draft, and reports
recalculation_status. A refusal after the environment was changed is
returned under refused, beside the writes.
Fixed — The multi-environment form of /quote
(costing_environment_ids, one entry included) ignored
derived_from_calculation_id and language: its calculations recorded no
parent, accepted a parent outside your organization, did not keep the
parent's stock format, and were stored in German whatever was asked. It now
follows the same rules as POST /calculations, including 404 for a parent
that is not one of your calculations.
Fixed — The materials guide and the ERP-integration and bulk RFQ triage
recipes named the 422 for a material the catalogue cannot match
material_not_resolved; the API sends
material_unresolved. The triage recipe
also listed it among the reasons a calculation fails: an unmatched
material_ref is refused when the request is sent, and no calculation is
created.
2026-09-23
Changed — A laser nozzle's own cutting data now prices the cut.
Rows on a laser_nozzle (PUT /tools/{id}/parameters: speed_m_per_min
and tau_pierce_s per material family, sheet thickness in size_mm) are
read by the laser handler the way a plasma set's chart already was: the
row nearest the sheet thickness supplies the traverse speed and the pierce
time, an environment override still wins, and the cost driver carries the
reason tool_cutting_data naming the tool instead of
material_bin_fallback. Stainless steel has a seeded N₂ cutting ladder of
its own (1–20 mm), so a stainless sheet no longer prices on the mild-steel
O₂ tier.
Changed — Cutting-data rows are validated against the tool kind.
A parameter must belong to the kind's vocabulary (V_c/f_z/f_r/a_p/
a_e on machining tools; speed_m_per_min/kerf_mm/tau_pierce_s/
pierce_max_mm on laser, plasma and waterjet consumables) and operation
must be one the kind runs (GET /tools/catalogue → tool_kind_operations).
A row that fails answers 422 with the field named in
error.details.errors[].loc, the same shape as every other validation
error.
Added — GET /tools/{id}/effective-cutting-data shows a laser nozzle's
and a plasma set's family × thickness ladder (size_mm on each row) and
states own_rows_priced: false for kinds whose handler resolves its
process bin without the tool tier (insert holders, boring bars, thread
mills, waterjet nozzles), where a stored row is reference data.
2026-09-15
Added — A material catalogue of 536 grades. Carbon, alloy,
stainless and tool steels, cast irons, aluminium, copper, titanium,
nickel and magnesium alloys, polymers and composites, reachable under
1,913 designations from 53 standard systems — so 1.0038, S235JR,
St 37-2 and Fe 360 B resolve to one grade. Lookup is format-tolerant:
1,0553, X5CrNi18 10 and AISI304 bind like their canonical spellings.
Every metal grade carries its mechanical properties with their basis
(minimum, maximum, typical); GET /materials/attribute-vocabulary states,
per property, what the costing engine reads it for.
Added — Your own materials, first class. POST /materials/grades
registers a grade private to your organization; POST /materials/grades/{id}/designations teaches the catalogue the spellings
your drawings print; PUT /materials/grades/{id}/override states your own
properties on a catalogue grade. All three reach pricing and drawing
binding alike.
Added — The learning queue. A title-block material string that
binds to nothing lands in GET /materials/unmapped-designations with a
count and ranked suggestions. Map it (/map, optionally under a
designation spelling when the text as read is not one) and it binds from
the next calculation on; dismiss what is not a material with /ignore.
Added — Workpiece material groups. Every grade maps to one of 62
ISO 513 groups (GET /materials/workpiece-groups); an environment's
cutting-data table now carries the group axis beside the material axis,
and workpiece_group_code is on every grade projection.
Changed — Editing your material master data (grades, designations, overrides, queue mappings) versions every costing environment of your organization, so the next run of an affected part is priced rather than served from the calculation cache.
2026-08-13
Added — Comparisons say when calibration is uneven. Calibration
corrects prices in the environment it was fitted for, so a calibrated
environment and an uncalibrated one are not a like-for-like comparison —
and nothing in the costs shows it. Comparison cells now carry
calibration_cost_offset and calibrated; a cell corrected differently
from its row's baseline is flagged calibration_mismatch, each
environment reports calibrated_cells, and the matrix sets
mixed_calibration. Flags, not exclusions: the deltas are real and stay
in the comparison. arcnm_compare_environments carries the same caveat
into its guidance.
Added — material_ref on POST /calculations/batch, so every
calculation entry point accepts a free-form material reference.
Added — region and currency on
/calculations/upload-and-quote(-json), matching /quote and /batch.
Added — coverage on the /calculations/bulk-retry response, so a
parked retry carries the same top-up payload a batch submit does.
Fixed — One material contract everywhere. The multi-environment
forms of /quote and /upload-and-quote resolved materials through a
laxer path than their single-environment forms, so the same
material_ref was rejected with one environment and silently ignored
with two; /calculations/batch never validated material_grade_id and
returned 500 for an unknown one. All entry points now answer identically
with 422 material_unresolved plus ranked candidates.
Fixed — Identical re-runs are free again. The dedup key was computed after the run recorded the material read from the drawing, so a re-run of the same part could never match it and was billed a second time.
Fixed — Calibrating an environment retires its cached prices. Publishing a calibration left the dedup key unchanged, so identical re-runs kept returning pre-calibration prices for the rest of the billing window.
Fixed — /calculations/bulk-retry now serves an identical prior run
from the cache like /calculations/{id}/run does, instead of re-running
and re-billing it.
Fixed — /calculations/batch now honours surface_treatments,
raw_material_strategy and language, which it documented and dropped.
Fixed — material_unresolved is documented in the
errors guide and its doc_url resolves. The
quote-a-part recipe named a code (material_not_resolved) that the API
never emits.
2026-08-11
Added — Multi-environment runs. Pass costing_environment_ids
(up to 16, in comparison order) on /calculations/quote,
/calculations/upload-and-quote(-json) or /calculations/batch to
price each revision in every listed environment with one request — the
file is uploaded once, the runs share a batch_id, and the response
lists them in environment_runs. Each (revision × environment) cell is
one calculation against your plan quota; a grid the remaining quota
can't cover never runs in part — /calculations/batch rejects it whole
(402 quota_exceeded with cells_requested/cells_available), while
/quote and /upload-and-quote(-json) save the cells parked blocked
and the 402's details also carry batch_id/parked_cells so a
post-top-up retry resumes that batch.
Added — Environment comparison.
GET /calculations/batches/{batch_id}/comparison and read-only
POST /calculations/comparison return the parts × environments matrix
server-side: per-cell costs and times, the cheapest environment per
part (ties included), deltas vs the cheapest and vs your baseline
(anchored on your baseline environment when it is in the grid, else
the grid's first environment), per-environment wins and basket totals,
staleness and mixed-currency flags.
Added — POST /calculations/batches/{batch_id}/cancel cancels a
whole batch in one call; GET /calculations gains a batch_id filter
and list items now carry costing_environment_id and batch_id.
Added — MCP: curated read-only tool arcnm_compare_environments
and prompt compare_environments.
2026-06-01
Added — Public developer portal at arcnm.io/docs: quickstart, authentication, concepts, recipes, SDKs, and a full API reference.
Added — The API reference is now auto-generated from the public OpenAPI spec on every build, so it can never drift from the running API. Each endpoint ships request/response schemas, enum values, multi-language samples (cURL / Python / TypeScript), and example responses.
Added — Machine-readable docs for AI tooling: llms.txt,
llms-full.txt, per-page Markdown, and an interactive
API explorer.
Changed — Every public endpoint now declares a typed response model, so the generated schemas are complete.