ARCNM

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.json is 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-calibrate and auto-calibrate-from-erp report a part whose quote at that lot size predates this release in unmatched_part_ids, like a part never quoted, and refuse when no part is left — quote the parts again, then retry.
  • POST /calibration/imports counts such a part in calculations_required and 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}/finalize fits only the observations submitted since this release, says in message how many it left out, and refuses when none is left: submit them again through POST /calibration/outcomes with predicted values from today's quotes.
  • POST /calibration/environments/{env_id}/teach refuses a part the environment has quoted only before this release, naming it.
  • POST /calibration/environments/{env_id}/runs/{run_id}/revert restores a run's calibration without the adjustment calibration no longer makes (the material quantity, now modelled), and answers 409 when the run fitted nothing else, leaving the active calibration as it was. The app's full calibration report lists that adjustment under retired_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.