API reference
Environments
The Environments API manages costing environments.
The Environments API manages costing environments: create, list, update, and delete them, set their identity, and attach the machines they cost against.
Auto-generated from the public OpenAPI spec — this page never drifts from the running API. Base URL
https://api.arcnm.io. Authenticate with theX-API-Keyheader (see Authentication).
Create Environment
POST /api/v1/environments
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
currency |
string | no | ISO 4217 currency code the environment's rates are denominated in. |
description |
string | no | Optional longer description of the environment. |
name |
string | yes | Human-readable name for the new environment. |
region |
string | no | Geographic region this environment prices for (e.g. DE, US). |
valid_from |
string | yes | ISO date (YYYY-MM-DD) from which the environment is effective. |
valid_to |
string | no | ISO date (YYYY-MM-DD) the environment stops being effective; null = open-ended. |
Request
curl -X POST https://api.arcnm.io/api/v1/environments \
-H "X-API-Key: $ARCNM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "string",
"valid_from": "2026-06-01"
}'
import requests
resp = requests.post(
"https://api.arcnm.io/api/v1/environments",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"name": "string",
"valid_from": "2026-06-01"
},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments", {
method: "POST",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "string",
"valid_from": "2026-06-01"
}),
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
201 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
409 |
conflict |
A conflicting change, or an Idempotency-Key reused with a different body. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 201
| Field | Type | Description |
|---|---|---|
currency |
string | ISO 4217 currency code the environment's rates are denominated in. |
description |
string | Optional longer description of the environment. |
id |
string | Unique identifier of the costing environment. |
is_baseline |
boolean | Whether this is the auto-provisioned default environment for the tenant. |
machines_cloned |
integer | Number of machines copied into the environment; only set by the clone endpoints. |
name |
string | Human-readable name of the costing environment. |
parent_environment_id |
string | Environment this one inherits from, or null when it stands alone. Anything this environment does not state itself resolves from the parent and, recursively, from the parent's parent. |
rates_cloned |
integer | Number of rate rows copied into the environment; only set by the clone endpoints. |
region |
string | Geographic region this environment prices for (e.g. DE, US). |
valid_from |
string | ISO date (YYYY-MM-DD) from which this environment is effective. |
valid_to |
string | ISO date (YYYY-MM-DD) the environment stops being effective; null = open-ended. |
Example response
{
"currency": "EUR",
"description": "string",
"id": "string",
"is_baseline": true,
"machines_cloned": 0,
"name": "string",
"parent_environment_id": "string",
"rates_cloned": 0,
"region": "EU",
"valid_from": "string",
"valid_to": "string"
}
List Environments
GET /api/v1/environments/
List the organization's costing environments, newest first.
The response is a plain array, so the page position travels in the
Link (RFC 8288) and X-Next-Cursor / X-Has-More response headers.
Paginated. Pass
cursor(from the previous response) to fetch the next page;limitcaps the page size.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
cursor |
query | string | no | Opaque position token from the previous page's next_cursor (or the Link / X-Next-Cursor response header). Omit it for the first page. Keep every other query parameter identical for the whole walk — a cursor replayed against different filters is rejected. |
limit |
query | integer | no | Maximum rows to return in one page. |
order |
query | asc | desc |
no | Sort direction over the collection's ordering key. Use asc to reconcile a batch: rows come oldest-first, so work created while you page lands after your position instead of shifting rows under it. |
created_after |
query | string | no | Only rows created at or after this instant (RFC 3339, e.g. 2026-07-20T09:00:00Z). Inclusive. |
created_before |
query | string | no | Only rows created strictly before this instant (RFC 3339). Exclusive, so an after/before pair tiles a range without overlap. |
offset |
query | integer | no | Rows to skip. Superseded by cursor, which is stable under concurrent writes; kept for existing integrations. Bounded — past the cap, page with cursor. |
Request
curl -X GET https://api.arcnm.io/api/v1/environments/ \
-H "X-API-Key: $ARCNM_API_KEY"
import requests
resp = requests.get(
"https://api.arcnm.io/api/v1/environments/",
headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/", {
method: "GET",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
},
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Delete Environment
DELETE /api/v1/environments/{env_id}
Delete an environment. ON DELETE CASCADE on the rate + machine-
membership tables means the rates + memberships disappear with it;
the underlying MachineDefinition rows survive (they're org-
scoped, not env-scoped, and may be reused by sibling envs).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
Request
curl -X DELETE https://api.arcnm.io/api/v1/environments/{env_id} \
-H "X-API-Key: $ARCNM_API_KEY"
import requests
resp = requests.delete(
"https://api.arcnm.io/api/v1/environments/{env_id}",
headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}", {
method: "DELETE",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
},
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
409 |
conflict |
A conflicting change, or an Idempotency-Key reused with a different body. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 200
| Field | Type | Description |
|---|---|---|
message |
string | Human-readable result of the operation. |
Example response
{
"message": "string"
}
Get Environment
GET /api/v1/environments/{env_id}
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
Request
curl -X GET https://api.arcnm.io/api/v1/environments/{env_id} \
-H "X-API-Key: $ARCNM_API_KEY"
import requests
resp = requests.get(
"https://api.arcnm.io/api/v1/environments/{env_id}",
headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}", {
method: "GET",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
},
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 200
| Field | Type | Description |
|---|---|---|
attributes |
object | Free-form key/value metadata attached to the environment. |
currency |
string | ISO 4217 currency code the environment's rates are denominated in. |
description |
string | Optional longer description of the environment. |
id |
string | Unique identifier of the costing environment. |
is_baseline |
boolean | Whether this is the auto-provisioned default environment for the tenant. |
machines_cloned |
integer | Number of machines copied into the environment; only set by the clone endpoints. |
name |
string | Human-readable name of the costing environment. |
parent_environment_id |
string | Environment this one inherits from, or null when it stands alone. Anything this environment does not state itself resolves from the parent and, recursively, from the parent's parent. |
rates_cloned |
integer | Number of rate rows copied into the environment; only set by the clone endpoints. |
region |
string | Geographic region this environment prices for (e.g. DE, US). |
valid_from |
string | ISO date (YYYY-MM-DD) from which this environment is effective. |
valid_to |
string | ISO date (YYYY-MM-DD) the environment stops being effective; null = open-ended. |
Example response
{
"attributes": {},
"currency": "EUR",
"description": "string",
"id": "string",
"is_baseline": true,
"machines_cloned": 0,
"name": "string",
"parent_environment_id": "string",
"rates_cloned": 0,
"region": "EU",
"valid_from": "string",
"valid_to": "string"
}
Clone Environment
POST /api/v1/environments/{env_id}/clone
Duplicate one of the tenant's OWN environments (env + rates + fleet) into a fresh, non-baseline, uncalibrated copy — "start from this environment, then tune or calibrate the duplicate". Only the tenant's own envs can be cloned (any other id, including a platform preset or another tenant's env, → 404).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | Optional name for the duplicate; defaults to ' (copy)'. |
Request
curl -X POST https://api.arcnm.io/api/v1/environments/{env_id}/clone \
-H "X-API-Key: $ARCNM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "string"
}'
import requests
resp = requests.post(
"https://api.arcnm.io/api/v1/environments/{env_id}/clone",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"name": "string"
},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/clone", {
method: "POST",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "string"
}),
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
201 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
409 |
conflict |
A conflicting change, or an Idempotency-Key reused with a different body. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 201
| Field | Type | Description |
|---|---|---|
currency |
string | ISO 4217 currency code the environment's rates are denominated in. |
description |
string | Optional longer description of the environment. |
id |
string | Unique identifier of the costing environment. |
is_baseline |
boolean | Whether this is the auto-provisioned default environment for the tenant. |
machines_cloned |
integer | Number of machines copied into the environment; only set by the clone endpoints. |
name |
string | Human-readable name of the costing environment. |
parent_environment_id |
string | Environment this one inherits from, or null when it stands alone. Anything this environment does not state itself resolves from the parent and, recursively, from the parent's parent. |
rates_cloned |
integer | Number of rate rows copied into the environment; only set by the clone endpoints. |
region |
string | Geographic region this environment prices for (e.g. DE, US). |
valid_from |
string | ISO date (YYYY-MM-DD) from which this environment is effective. |
valid_to |
string | ISO date (YYYY-MM-DD) the environment stops being effective; null = open-ended. |
Example response
{
"currency": "EUR",
"description": "string",
"id": "string",
"is_baseline": true,
"machines_cloned": 0,
"name": "string",
"parent_environment_id": "string",
"rates_cloned": 0,
"region": "EU",
"valid_from": "string",
"valid_to": "string"
}
Read the environment's effective rates.
GET /api/v1/environments/{env_id}/effective-rates
The rates this environment's prices are computed with — per driver (machine hour, labour hour, overheads) and per machine, each with its source: your override, a calibrated adjustment, or the platform default. Rate detail is shown in the Arcanum app; integrations receive the status envelope.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
at |
query | string | no | As-of date for effective-dated rates; defaults to today (UTC). |
Request
curl -X GET https://api.arcnm.io/api/v1/environments/{env_id}/effective-rates \
-H "X-API-Key: $ARCNM_API_KEY"
import requests
resp = requests.get(
"https://api.arcnm.io/api/v1/environments/{env_id}/effective-rates",
headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/effective-rates", {
method: "GET",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
},
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 200
| Field | Type | Description |
|---|---|---|
as_of |
string | |
calibrated |
boolean | Whether this environment has been calibrated to your actuals. |
currency |
string | |
env_id |
string | |
mode |
per_driver | overall |
|
note |
string |
Example response
{
"as_of": "2026-06-01",
"calibrated": true,
"currency": "EUR",
"env_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"mode": "per_driver",
"note": "string"
}
Set an effective rate for one driver.
PUT /api/v1/environments/{env_id}/effective-rates
Set the rate a driver charges, in its own unit. Your number becomes the effective rate; future calibrations adjust around it. Takes effect on the next calculation.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
driver |
machine | labour | overhead_var | overhead_fix |
yes | Which rate to set. |
value |
number | yes | The effective rate to charge, in the driver's unit. |
Request
curl -X PUT https://api.arcnm.io/api/v1/environments/{env_id}/effective-rates \
-H "X-API-Key: $ARCNM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"driver": "machine",
"value": 0
}'
import requests
resp = requests.put(
"https://api.arcnm.io/api/v1/environments/{env_id}/effective-rates",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"driver": "machine",
"value": 0
},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/effective-rates", {
method: "PUT",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
"driver": "machine",
"value": 0
}),
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
409 |
conflict |
A conflicting change, or an Idempotency-Key reused with a different body. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 200
| Field | Type | Description |
|---|---|---|
as_of |
string | |
calibrated |
boolean | Whether this environment has been calibrated to your actuals. |
currency |
string | |
env_id |
string | |
mode |
per_driver | overall |
|
note |
string |
Example response
{
"as_of": "2026-06-01",
"calibrated": true,
"currency": "EUR",
"env_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"mode": "per_driver",
"note": "string"
}
Update Environment Identity
PUT /api/v1/environments/{env_id}/identity
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
currency |
string | no | New ISO 4217 currency code; omit to leave unchanged. |
description |
string | no | New description for the environment; omit to leave unchanged. |
name |
string | no | New name for the environment; omit to leave unchanged. |
parent_environment_id |
string | no | Environment this one inherits from. Anything this environment does not state itself — a machine rate, a labour rate, a subcontract price, a physics override — resolves from the parent, and from ITS parent above that, so a region can be priced once and a quarter or a customer programme can restate only what differs. Send null to detach. Omit to leave unchanged. |
region |
string | no | New region for the environment; omit to leave unchanged. |
valid_from |
string | no | New effective-from ISO date (YYYY-MM-DD); omit to leave unchanged. |
valid_to |
string | no | New effective-to ISO date (YYYY-MM-DD); omit to leave unchanged. |
Request
curl -X PUT https://api.arcnm.io/api/v1/environments/{env_id}/identity \
-H "X-API-Key: $ARCNM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"currency": "EUR",
"description": "string",
"name": "string",
"parent_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"region": "EU",
"valid_from": "2026-06-01",
"valid_to": "2026-06-01"
}'
import requests
resp = requests.put(
"https://api.arcnm.io/api/v1/environments/{env_id}/identity",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"currency": "EUR",
"description": "string",
"name": "string",
"parent_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"region": "EU",
"valid_from": "2026-06-01",
"valid_to": "2026-06-01"
},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/identity", {
method: "PUT",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
"currency": "EUR",
"description": "string",
"name": "string",
"parent_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"region": "EU",
"valid_from": "2026-06-01",
"valid_to": "2026-06-01"
}),
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
409 |
conflict |
A conflicting change, or an Idempotency-Key reused with a different body. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 200
| Field | Type | Description |
|---|---|---|
currency |
string | ISO 4217 currency code the environment's rates are denominated in. |
description |
string | Optional longer description of the environment. |
id |
string | Unique identifier of the costing environment. |
is_baseline |
boolean | Whether this is the auto-provisioned default environment for the tenant. |
machines_cloned |
integer | Number of machines copied into the environment; only set by the clone endpoints. |
name |
string | Human-readable name of the costing environment. |
parent_environment_id |
string | Environment this one inherits from, or null when it stands alone. Anything this environment does not state itself resolves from the parent and, recursively, from the parent's parent. |
rates_cloned |
integer | Number of rate rows copied into the environment; only set by the clone endpoints. |
region |
string | Geographic region this environment prices for (e.g. DE, US). |
valid_from |
string | ISO date (YYYY-MM-DD) from which this environment is effective. |
valid_to |
string | ISO date (YYYY-MM-DD) the environment stops being effective; null = open-ended. |
Example response
{
"currency": "EUR",
"description": "string",
"id": "string",
"is_baseline": true,
"machines_cloned": 0,
"name": "string",
"parent_environment_id": "string",
"rates_cloned": 0,
"region": "EU",
"valid_from": "string",
"valid_to": "string"
}
List Env Machines
GET /api/v1/environments/{env_id}/machines
Machines wired to this env, ordered by fleet_priority (lower =
earlier candidate). The same fleet feeds every pricing run for the
environment.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
Request
curl -X GET https://api.arcnm.io/api/v1/environments/{env_id}/machines \
-H "X-API-Key: $ARCNM_API_KEY"
import requests
resp = requests.get(
"https://api.arcnm.io/api/v1/environments/{env_id}/machines",
headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/machines", {
method: "GET",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
},
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Attach Machine
POST /api/v1/environments/{env_id}/machines
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
capability_overrides |
object | no | Sparse per-environment capability overrides merged over the library defaults. |
fleet_priority |
integer | no | Orders machines within the environment; lower values are tried first. |
hourly_rate_override_eur |
number | no | Optional flat machine-hour rate; defaults to the library entry's nominal rate. |
is_enabled |
boolean | no | Whether the machine is active in the environment's fleet on attach. |
library_entry_id |
string | no | Instantiate a machine from this shared/tenant library entry; mutually exclusive with machine_id / new_machine. |
machine_id |
string | no | Identifier of an existing machine to attach; mutually exclusive with new_machine. |
name_override |
string | no | Optional name for the instantiated machine; defaults to the library entry's name. |
new_machine |
MachineDefinitionCreate | no | Inline definition of a new machine to create and attach; mutually exclusive with machine_id. |
rate_operator_eur_per_h_override |
number | no | Operator wage already contained in hourly_rate_override_eur, in EUR per hour. Only meaningful with a rate override; without one the library entry's own declaration is inherited along with its rate. 0 means the rate is machine-only. |
valid_from |
string | no | ISO date (YYYY-MM-DD) the membership starts; defaults to the machine's own valid_from. |
valid_to |
string | no | ISO date (YYYY-MM-DD) the membership ends; defaults to the machine's own valid_to. |
Request
curl -X POST https://api.arcnm.io/api/v1/environments/{env_id}/machines \
-H "X-API-Key: $ARCNM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"capability_overrides": {},
"fleet_priority": 100,
"hourly_rate_override_eur": 0,
"is_enabled": true,
"library_entry_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"machine_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name_override": "string",
"new_machine": {
"burden_rate_eur": 0,
"capabilities": {
"axes_indexable": 0,
"axes_simultaneous": 0,
"certifications": [
"string"
],
"chatter_stability_lobe": {
"rpm_to_max_axial_depth_mm": [
{}
]
},
"coolant": [
"flood"
],
"iso_286_achievable_grade": "IT01",
"klass": "milling.3axis_vmc",
"max_material_thickness_mm": 0,
"max_part_envelope_mm": [
null
],
"max_setups_per_part": 6,
"max_spindle_rpm": 0,
"max_table_load_kg": 0,
"max_tool_diameter_mm": 0,
"max_tool_length_mm": 0,
"min_material_thickness_mm": 0,
"nominal_tool_change_time_s_by_class": {},
"pallet_change_time_s": 0,
"positioning_accuracy_mm": 0.01,
"rapid_traverse_m_per_min": 24,
"repeatability_mm": 0.005,
"saw_blade_cost_eur": 0,
"saw_blade_life_mm2": 0,
"saw_blade_type": "bimetal",
"saw_kerf_mm": 0,
"schema_version": "1.0.0",
"spindle_power_kw": 0,
"subclass": "small",
"vdi_3258": {
"acquisition_cost_eur": 0,
"annual_hours_T_G": 0,
"annual_hours_T_IH": 0,
"annual_hours_T_ST": 0,
"capital_interest_rate": 0,
"depreciation_life_h": 0,
"energy_eur_per_kwh": 0,
"energy_kw": 0,
"floor_space_m2": 0,
"maintenance_eur_per_year": 0,
"operator_hourly_eur": 0,
"operator_share": 0,
"space_eur_per_m2_y": 0,
"tooling_eur_per_year": 0
},
"workholding": [
"vise.3jaw"
]
},
"hourly_rate_eur": 0,
"klass": "milling.3axis_vmc",
"library_entry_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"model_no": "string",
"name": "string",
"programming_rate_eur": 0,
"rate_operator_eur_per_h": 0,
"rate_operator_share": 0,
"setup_rate_eur": 0,
"subclass": "small",
"valid_from": "2026-06-01",
"valid_to": "2026-06-01",
"vendor": "string"
},
"rate_operator_eur_per_h_override": 0,
"valid_from": "2026-06-01",
"valid_to": "2026-06-01"
}'
import requests
resp = requests.post(
"https://api.arcnm.io/api/v1/environments/{env_id}/machines",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"capability_overrides": {},
"fleet_priority": 100,
"hourly_rate_override_eur": 0,
"is_enabled": True,
"library_entry_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"machine_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name_override": "string",
"new_machine": {
"burden_rate_eur": 0,
"capabilities": {
"axes_indexable": 0,
"axes_simultaneous": 0,
"certifications": [
"string"
],
"chatter_stability_lobe": {
"rpm_to_max_axial_depth_mm": [
{}
]
},
"coolant": [
"flood"
],
"iso_286_achievable_grade": "IT01",
"klass": "milling.3axis_vmc",
"max_material_thickness_mm": 0,
"max_part_envelope_mm": [
None
],
"max_setups_per_part": 6,
"max_spindle_rpm": 0,
"max_table_load_kg": 0,
"max_tool_diameter_mm": 0,
"max_tool_length_mm": 0,
"min_material_thickness_mm": 0,
"nominal_tool_change_time_s_by_class": {},
"pallet_change_time_s": 0,
"positioning_accuracy_mm": 0.01,
"rapid_traverse_m_per_min": 24,
"repeatability_mm": 0.005,
"saw_blade_cost_eur": 0,
"saw_blade_life_mm2": 0,
"saw_blade_type": "bimetal",
"saw_kerf_mm": 0,
"schema_version": "1.0.0",
"spindle_power_kw": 0,
"subclass": "small",
"vdi_3258": {
"acquisition_cost_eur": 0,
"annual_hours_T_G": 0,
"annual_hours_T_IH": 0,
"annual_hours_T_ST": 0,
"capital_interest_rate": 0,
"depreciation_life_h": 0,
"energy_eur_per_kwh": 0,
"energy_kw": 0,
"floor_space_m2": 0,
"maintenance_eur_per_year": 0,
"operator_hourly_eur": 0,
"operator_share": 0,
"space_eur_per_m2_y": 0,
"tooling_eur_per_year": 0
},
"workholding": [
"vise.3jaw"
]
},
"hourly_rate_eur": 0,
"klass": "milling.3axis_vmc",
"library_entry_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"model_no": "string",
"name": "string",
"programming_rate_eur": 0,
"rate_operator_eur_per_h": 0,
"rate_operator_share": 0,
"setup_rate_eur": 0,
"subclass": "small",
"valid_from": "2026-06-01",
"valid_to": "2026-06-01",
"vendor": "string"
},
"rate_operator_eur_per_h_override": 0,
"valid_from": "2026-06-01",
"valid_to": "2026-06-01"
},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/machines", {
method: "POST",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
"capability_overrides": {},
"fleet_priority": 100,
"hourly_rate_override_eur": 0,
"is_enabled": true,
"library_entry_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"machine_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name_override": "string",
"new_machine": {
"burden_rate_eur": 0,
"capabilities": {
"axes_indexable": 0,
"axes_simultaneous": 0,
"certifications": [
"string"
],
"chatter_stability_lobe": {
"rpm_to_max_axial_depth_mm": [
{}
]
},
"coolant": [
"flood"
],
"iso_286_achievable_grade": "IT01",
"klass": "milling.3axis_vmc",
"max_material_thickness_mm": 0,
"max_part_envelope_mm": [
null
],
"max_setups_per_part": 6,
"max_spindle_rpm": 0,
"max_table_load_kg": 0,
"max_tool_diameter_mm": 0,
"max_tool_length_mm": 0,
"min_material_thickness_mm": 0,
"nominal_tool_change_time_s_by_class": {},
"pallet_change_time_s": 0,
"positioning_accuracy_mm": 0.01,
"rapid_traverse_m_per_min": 24,
"repeatability_mm": 0.005,
"saw_blade_cost_eur": 0,
"saw_blade_life_mm2": 0,
"saw_blade_type": "bimetal",
"saw_kerf_mm": 0,
"schema_version": "1.0.0",
"spindle_power_kw": 0,
"subclass": "small",
"vdi_3258": {
"acquisition_cost_eur": 0,
"annual_hours_T_G": 0,
"annual_hours_T_IH": 0,
"annual_hours_T_ST": 0,
"capital_interest_rate": 0,
"depreciation_life_h": 0,
"energy_eur_per_kwh": 0,
"energy_kw": 0,
"floor_space_m2": 0,
"maintenance_eur_per_year": 0,
"operator_hourly_eur": 0,
"operator_share": 0,
"space_eur_per_m2_y": 0,
"tooling_eur_per_year": 0
},
"workholding": [
"vise.3jaw"
]
},
"hourly_rate_eur": 0,
"klass": "milling.3axis_vmc",
"library_entry_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"model_no": "string",
"name": "string",
"programming_rate_eur": 0,
"rate_operator_eur_per_h": 0,
"rate_operator_share": 0,
"setup_rate_eur": 0,
"subclass": "small",
"valid_from": "2026-06-01",
"valid_to": "2026-06-01",
"vendor": "string"
},
"rate_operator_eur_per_h_override": 0,
"valid_from": "2026-06-01",
"valid_to": "2026-06-01"
}),
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
201 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
409 |
conflict |
A conflicting change, or an Idempotency-Key reused with a different body. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 201
| Field | Type | Description |
|---|---|---|
fleet_priority |
integer | Orders machines within the environment; lower values are tried first. |
health |
string | Whether this machine can be used for pricing. 'ok' — it takes part in selection. 'unresolvable' — its capability configuration is incomplete, so it is NOT considered by any calculation, even though it is enabled. Render an unresolvable machine as broken; it used to render as a normal row and silently do nothing. |
health_error |
string | Why the machine cannot be evaluated; null when health is 'ok'. |
is_enabled |
boolean | Whether this machine is active in the environment's fleet. |
machine |
MachinePublic | The attached machine definition; null only when it cannot be re-read. |
membership_id |
string | Unique identifier of the environment-machine membership. |
valid_from |
string | ISO date (YYYY-MM-DD) from which this membership is effective. |
valid_to |
string | ISO date (YYYY-MM-DD) the membership stops being effective; null = open-ended. |
Example response
{
"fleet_priority": 0,
"health": "ok",
"health_error": "string",
"is_enabled": true,
"machine": {
"burden_rate_eur": 0,
"capabilities": {},
"hourly_rate_eur": 0,
"id": "string",
"klass": "string",
"model_no": "string",
"name": "string",
"rate_operator_eur_per_h": 0,
"rate_operator_share": 0,
"source": "custom",
"subclass": "string",
"valid_from": "string",
"valid_to": "string",
"vendor": "string"
},
"membership_id": "string",
"valid_from": "string",
"valid_to": "string"
}
Detach Machine
DELETE /api/v1/environments/{env_id}/machines/{membership_id}
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
membership_id |
path | string | yes | Identifier of the membership. |
Request
curl -X DELETE https://api.arcnm.io/api/v1/environments/{env_id}/machines/{membership_id} \
-H "X-API-Key: $ARCNM_API_KEY"
import requests
resp = requests.delete(
"https://api.arcnm.io/api/v1/environments/{env_id}/machines/{membership_id}",
headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/machines/{membership_id}", {
method: "DELETE",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
},
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
409 |
conflict |
A conflicting change, or an Idempotency-Key reused with a different body. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 200
| Field | Type | Description |
|---|---|---|
message |
string | Human-readable result of the operation. |
Example response
{
"message": "string"
}
Update Membership
PATCH /api/v1/environments/{env_id}/machines/{membership_id}
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
membership_id |
path | string | yes | Identifier of the membership. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
fleet_priority |
integer | no | New ordering within the environment (lower tried first); omit to leave unchanged. |
is_enabled |
boolean | no | Whether the machine is active in the fleet; omit to leave unchanged. |
valid_from |
string | no | New membership start ISO date (YYYY-MM-DD); omit to leave unchanged. |
valid_to |
string | no | New membership end ISO date (YYYY-MM-DD); omit to leave unchanged. |
Request
curl -X PATCH https://api.arcnm.io/api/v1/environments/{env_id}/machines/{membership_id} \
-H "X-API-Key: $ARCNM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fleet_priority": 0,
"is_enabled": true,
"valid_from": "2026-06-01",
"valid_to": "2026-06-01"
}'
import requests
resp = requests.patch(
"https://api.arcnm.io/api/v1/environments/{env_id}/machines/{membership_id}",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"fleet_priority": 0,
"is_enabled": True,
"valid_from": "2026-06-01",
"valid_to": "2026-06-01"
},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/machines/{membership_id}", {
method: "PATCH",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
"fleet_priority": 0,
"is_enabled": true,
"valid_from": "2026-06-01",
"valid_to": "2026-06-01"
}),
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
409 |
conflict |
A conflicting change, or an Idempotency-Key reused with a different body. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 200
| Field | Type | Description |
|---|---|---|
fleet_priority |
integer | Orders machines within the environment; lower values are tried first. |
health |
string | Whether this machine can be used for pricing. 'ok' — it takes part in selection. 'unresolvable' — its capability configuration is incomplete, so it is NOT considered by any calculation, even though it is enabled. Render an unresolvable machine as broken; it used to render as a normal row and silently do nothing. |
health_error |
string | Why the machine cannot be evaluated; null when health is 'ok'. |
is_enabled |
boolean | Whether this machine is active in the environment's fleet. |
machine |
MachinePublic | The attached machine definition; null only when it cannot be re-read. |
membership_id |
string | Unique identifier of the environment-machine membership. |
valid_from |
string | ISO date (YYYY-MM-DD) from which this membership is effective. |
valid_to |
string | ISO date (YYYY-MM-DD) the membership stops being effective; null = open-ended. |
Example response
{
"fleet_priority": 0,
"health": "ok",
"health_error": "string",
"is_enabled": true,
"machine": {
"burden_rate_eur": 0,
"capabilities": {},
"hourly_rate_eur": 0,
"id": "string",
"klass": "string",
"model_no": "string",
"name": "string",
"rate_operator_eur_per_h": 0,
"rate_operator_share": 0,
"source": "custom",
"subclass": "string",
"valid_from": "string",
"valid_to": "string",
"vendor": "string"
},
"membership_id": "string",
"valid_from": "string",
"valid_to": "string"
}
List Rates
GET /api/v1/environments/{env_id}/rates
List all rates for an env, uniformly shaped so the rate editor
can render every kind side by side. Optional ?kind= filter.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
kind |
query | string | no | Filter to a single rate kind (labour, overhead, material, fx, or subcontract). |
Request
curl -X GET https://api.arcnm.io/api/v1/environments/{env_id}/rates \
-H "X-API-Key: $ARCNM_API_KEY"
import requests
resp = requests.get(
"https://api.arcnm.io/api/v1/environments/{env_id}/rates",
headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/rates", {
method: "GET",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
},
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Upsert Rate
POST /api/v1/environments/{env_id}/rates
Create a rate row of the requested kind. Effective-dated: the new
row's valid_from opens a window; the previous active row's
valid_to should be capped by the caller.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
currency |
string | no | ISO 4217 currency code for the rate; null to inherit. |
fields |
object | no | Kind-specific columns (category, machine_ref, …). |
kind |
string | yes | labour |
valid_from |
string | yes | Date from which the rate is effective (ISO 8601). |
valid_to |
string | no | Date after which the rate is no longer effective; null if open-ended (ISO 8601). |
Request
curl -X POST https://api.arcnm.io/api/v1/environments/{env_id}/rates \
-H "X-API-Key: $ARCNM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "string",
"valid_from": "2026-06-01"
}'
import requests
resp = requests.post(
"https://api.arcnm.io/api/v1/environments/{env_id}/rates",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"kind": "string",
"valid_from": "2026-06-01"
},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/rates", {
method: "POST",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
"kind": "string",
"valid_from": "2026-06-01"
}),
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
201 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
409 |
conflict |
A conflicting change, or an Idempotency-Key reused with a different body. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 201
| Field | Type | Description |
|---|---|---|
created_at |
string | ISO 8601 timestamp when the rate row was created. |
currency |
string | ISO 4217 currency code for the rate; null for kinds without a currency. |
effective_now |
boolean | True when this row is the one currently pricing parts: its validity window contains today and no other row for the same key supersedes it. |
engine_honoured |
boolean | True when this row can be applied as written; false rows are listed but never priced (see unhonoured_reason). |
env_id |
string | Identifier of the environment this rate belongs to. |
fields |
object | Kind-specific rate columns (e.g. hourly_rate, value, price_per_kg, rate). |
id |
string | Unique identifier of the rate row. |
kind |
string | Rate kind: labour, overhead, material, fx, or subcontract. |
unhonoured_reason |
string | Why the row is never applied, when engine_honoured is false: 'currency_mismatch' (row currency differs from the environment's; labour/overhead/material rows only — fx rows carry their own currency pair, and a subcontract row's own currency field is not read at all), 'pricing_unit_mismatch' (the row's pricing_unit is not the one this operation is quoted in — re-enter the price in the stated unit), 'bucket_combo_unhonoured' (this bucket/kind/base combination is not priceable), or 'value_not_positive' (the row's amount is zero or negative and is skipped). Null otherwise. |
valid_from |
string | ISO date (YYYY-MM-DD) from which this rate is effective. |
valid_to |
string | ISO date (YYYY-MM-DD) the rate stops being effective; null = open-ended. |
Example response
{
"created_at": "string",
"currency": "EUR",
"effective_now": false,
"engine_honoured": true,
"env_id": "string",
"fields": {},
"id": "string",
"kind": "string",
"unhonoured_reason": "string",
"valid_from": "string",
"valid_to": "string"
}
Delete Rate
DELETE /api/v1/environments/{env_id}/rates/{kind}/{rate_id}
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
kind |
path | string | yes | Rate kind to delete (labour, overhead, material, fx, or subcontract). |
rate_id |
path | string | yes | Identifier of the rate. |
Request
curl -X DELETE https://api.arcnm.io/api/v1/environments/{env_id}/rates/{kind}/{rate_id} \
-H "X-API-Key: $ARCNM_API_KEY"
import requests
resp = requests.delete(
"https://api.arcnm.io/api/v1/environments/{env_id}/rates/{kind}/{rate_id}",
headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/rates/{kind}/{rate_id}", {
method: "DELETE",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
},
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
409 |
conflict |
A conflicting change, or an Idempotency-Key reused with a different body. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 200
| Field | Type | Description |
|---|---|---|
message |
string | Human-readable result of the operation. |
Example response
{
"message": "string"
}
Drop your price for a material in this environment.
DELETE /api/v1/environments/{env_id}/rates/material/{grade_id}
Removes the environment's own price rows for this material, so it prices at the platform figure for its category again. Takes effect immediately.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
grade_id |
path | string | yes | Identifier of the grade. |
Request
curl -X DELETE https://api.arcnm.io/api/v1/environments/{env_id}/rates/material/{grade_id} \
-H "X-API-Key: $ARCNM_API_KEY"
import requests
resp = requests.delete(
"https://api.arcnm.io/api/v1/environments/{env_id}/rates/material/{grade_id}",
headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/rates/material/{grade_id}", {
method: "DELETE",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
},
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
409 |
conflict |
A conflicting change, or an Idempotency-Key reused with a different body. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 200
| Field | Type | Description |
|---|---|---|
message |
string | Human-readable result of the operation. |
Example response
{
"message": "string"
}
Set what a material costs in this environment.
PUT /api/v1/environments/{env_id}/rates/material/{grade_id}
Your number becomes the price per kilogram this environment calculates with, from valid_from onwards. The price it replaces is closed the same day rather than deleted, so past quotes keep the figure they were costed at.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
grade_id |
path | string | yes | Identifier of the grade. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
price_per_kg |
number | yes | Price per kilogram, in the environment's currency. Takes effect on the next calculation. |
valid_from |
string | no | Day the price starts applying. Defaults to today. The price you replace is closed on the same day, so the two never overlap. |
Request
curl -X PUT https://api.arcnm.io/api/v1/environments/{env_id}/rates/material/{grade_id} \
-H "X-API-Key: $ARCNM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"price_per_kg": 0
}'
import requests
resp = requests.put(
"https://api.arcnm.io/api/v1/environments/{env_id}/rates/material/{grade_id}",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"price_per_kg": 0
},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/rates/material/{grade_id}", {
method: "PUT",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
"price_per_kg": 0
}),
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
409 |
conflict |
A conflicting change, or an Idempotency-Key reused with a different body. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 200
| Field | Type | Description |
|---|---|---|
created_at |
string | ISO 8601 timestamp when the rate row was created. |
currency |
string | ISO 4217 currency code for the rate; null for kinds without a currency. |
effective_now |
boolean | True when this row is the one currently pricing parts: its validity window contains today and no other row for the same key supersedes it. |
engine_honoured |
boolean | True when this row can be applied as written; false rows are listed but never priced (see unhonoured_reason). |
env_id |
string | Identifier of the environment this rate belongs to. |
fields |
object | Kind-specific rate columns (e.g. hourly_rate, value, price_per_kg, rate). |
id |
string | Unique identifier of the rate row. |
kind |
string | Rate kind: labour, overhead, material, fx, or subcontract. |
unhonoured_reason |
string | Why the row is never applied, when engine_honoured is false: 'currency_mismatch' (row currency differs from the environment's; labour/overhead/material rows only — fx rows carry their own currency pair, and a subcontract row's own currency field is not read at all), 'pricing_unit_mismatch' (the row's pricing_unit is not the one this operation is quoted in — re-enter the price in the stated unit), 'bucket_combo_unhonoured' (this bucket/kind/base combination is not priceable), or 'value_not_positive' (the row's amount is zero or negative and is skipped). Null otherwise. |
valid_from |
string | ISO date (YYYY-MM-DD) from which this rate is effective. |
valid_to |
string | ISO date (YYYY-MM-DD) the rate stops being effective; null = open-ended. |
Example response
{
"created_at": "string",
"currency": "EUR",
"effective_now": false,
"engine_honoured": true,
"env_id": "string",
"fields": {},
"id": "string",
"kind": "string",
"unhonoured_reason": "string",
"valid_from": "string",
"valid_to": "string"
}
Read the environment's shop-practice profile.
GET /api/v1/environments/{env_id}/shop-practice
How this environment models packaging, deburring, and part loading — the practice answers that tailor time allowances to the way your shop actually works.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
Request
curl -X GET https://api.arcnm.io/api/v1/environments/{env_id}/shop-practice \
-H "X-API-Key: $ARCNM_API_KEY"
import requests
resp = requests.get(
"https://api.arcnm.io/api/v1/environments/{env_id}/shop-practice",
headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/shop-practice", {
method: "GET",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
},
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 200
| Field | Type | Description |
|---|---|---|
questions |
object | Per-question state: effective answer, source, and options. |
Example response
{
"questions": {}
}
Update the environment's shop-practice profile.
PUT /api/v1/environments/{env_id}/shop-practice
Answer the shop-practice questions (packaging, deburring, part loading). Each answer adjusts the environment's time allowances within validated bounds and is recorded in the audit log. 'standard' restores the platform behaviour.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
answers |
object | yes | Question key → answer. Only catalogued questions and answers are accepted; 'standard' restores the platform behaviour. |
Request
curl -X PUT https://api.arcnm.io/api/v1/environments/{env_id}/shop-practice \
-H "X-API-Key: $ARCNM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"answers": {}
}'
import requests
resp = requests.put(
"https://api.arcnm.io/api/v1/environments/{env_id}/shop-practice",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"answers": {}
},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/shop-practice", {
method: "PUT",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
"answers": {}
}),
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
409 |
conflict |
A conflicting change, or an Idempotency-Key reused with a different body. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 200
| Field | Type | Description |
|---|---|---|
questions |
object | Per-question state: effective answer, source, and options. |
Example response
{
"questions": {}
}
Get Calculation Tuning
GET /api/v1/environments/{env_id}/tuning
Read the environment's calculation-tuning knobs with provenance ("platform default vs your override").
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
Request
curl -X GET https://api.arcnm.io/api/v1/environments/{env_id}/tuning \
-H "X-API-Key: $ARCNM_API_KEY"
import requests
resp = requests.get(
"https://api.arcnm.io/api/v1/environments/{env_id}/tuning",
headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/tuning", {
method: "GET",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
},
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 200
| Field | Type | Description |
|---|---|---|
learning_rate |
TuningKnob | Wright learning-curve rate applied to the lot-size curve (1.0 = off). |
programming_factor |
TuningKnob | Global multiplier on estimated programming times across all process bins. |
setup_factor |
TuningKnob | Global multiplier on estimated setup times across all process bins. |
Example response
{
"learning_rate": {
"platform_default": 0,
"source": "string",
"value": 0
},
"programming_factor": {
"platform_default": 0,
"source": "string",
"value": 0
},
"setup_factor": {
"platform_default": 0,
"source": "string",
"value": 0
}
}
Update Calculation Tuning
PUT /api/v1/environments/{env_id}/tuning
Write env-tier tuning overrides (audited).
setup_factor / programming_factor write the per-bin
setup_efficiency / programming_efficiency resolver
overrides for every routable process bin at ENV tier; 1.0 clears
the override (back to platform default). learning_rate writes
the env's Wright rate. Every change lands an audit row.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
env_id |
path | string | yes | Identifier of the env. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
learning_rate |
number | no | Wright rate in [0.5, 1.0]; 1.0 disables learning. |
programming_factor |
number | no | Programming-time multiplier; omit to keep, 1.0 to reset to default. |
setup_factor |
number | no | Setup-time multiplier; omit to keep, 1.0 to reset to default. |
Request
curl -X PUT https://api.arcnm.io/api/v1/environments/{env_id}/tuning \
-H "X-API-Key: $ARCNM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"learning_rate": 0,
"programming_factor": 0,
"setup_factor": 0
}'
import requests
resp = requests.put(
"https://api.arcnm.io/api/v1/environments/{env_id}/tuning",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"learning_rate": 0,
"programming_factor": 0,
"setup_factor": 0
},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/{env_id}/tuning", {
method: "PUT",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
"learning_rate": 0,
"programming_factor": 0,
"setup_factor": 0
}),
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
404 |
not_found |
A referenced resource doesn't exist or isn't visible to your organisation. |
409 |
conflict |
A conflicting change, or an Idempotency-Key reused with a different body. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 200
| Field | Type | Description |
|---|---|---|
learning_rate |
TuningKnob | Wright learning-curve rate applied to the lot-size curve (1.0 = off). |
programming_factor |
TuningKnob | Global multiplier on estimated programming times across all process bins. |
setup_factor |
TuningKnob | Global multiplier on estimated setup times across all process bins. |
Example response
{
"learning_rate": {
"platform_default": 0,
"source": "string",
"value": 0
},
"programming_factor": {
"platform_default": 0,
"source": "string",
"value": 0
},
"setup_factor": {
"platform_default": 0,
"source": "string",
"value": 0
}
}
Clone Preset
POST /api/v1/environments/clone-preset
Deep-copy a platform region preset (env + rates + fleet) into the tenant.
The clone is immediately runnable and priceable for every discipline. Only a genuine platform preset can be cloned — any other id (including another tenant's env) reads as 404.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | Optional name for the cloned environment; defaults to ' (copy)'. |
preset_id |
string | yes | Identifier of the platform region preset to clone. |
Request
curl -X POST https://api.arcnm.io/api/v1/environments/clone-preset \
-H "X-API-Key: $ARCNM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"preset_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}'
import requests
resp = requests.post(
"https://api.arcnm.io/api/v1/environments/clone-preset",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"preset_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/clone-preset", {
method: "POST",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
"preset_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}),
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
201 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
409 |
conflict |
A conflicting change, or an Idempotency-Key reused with a different body. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |
Response body 201
| Field | Type | Description |
|---|---|---|
currency |
string | ISO 4217 currency code the environment's rates are denominated in. |
description |
string | Optional longer description of the environment. |
id |
string | Unique identifier of the costing environment. |
is_baseline |
boolean | Whether this is the auto-provisioned default environment for the tenant. |
machines_cloned |
integer | Number of machines copied into the environment; only set by the clone endpoints. |
name |
string | Human-readable name of the costing environment. |
parent_environment_id |
string | Environment this one inherits from, or null when it stands alone. Anything this environment does not state itself resolves from the parent and, recursively, from the parent's parent. |
rates_cloned |
integer | Number of rate rows copied into the environment; only set by the clone endpoints. |
region |
string | Geographic region this environment prices for (e.g. DE, US). |
valid_from |
string | ISO date (YYYY-MM-DD) from which this environment is effective. |
valid_to |
string | ISO date (YYYY-MM-DD) the environment stops being effective; null = open-ended. |
Example response
{
"currency": "EUR",
"description": "string",
"id": "string",
"is_baseline": true,
"machines_cloned": 0,
"name": "string",
"parent_environment_id": "string",
"rates_cloned": 0,
"region": "EU",
"valid_from": "string",
"valid_to": "string"
}
List Presets
GET /api/v1/environments/presets
The 6 platform region presets a tenant can clone into a runnable env.
Read-only and platform-scoped: only org_id == PLATFORM_ORG_ID rows are
returned (bypass-read inside the service), so no tenant data is exposed.
Request
curl -X GET https://api.arcnm.io/api/v1/environments/presets \
-H "X-API-Key: $ARCNM_API_KEY"
import requests
resp = requests.get(
"https://api.arcnm.io/api/v1/environments/presets",
headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://api.arcnm.io/api/v1/environments/presets", {
method: "GET",
headers: {
"X-API-Key": process.env.ARCNM_API_KEY!,
},
})
const data = await resp.json()
Responses
| Status | Description |
|---|---|
200 |
Successful Response |
422 |
Validation Error |
Errors
Standard error responses — see the Errors catalog for the full envelope, request_id, and retry-safety table.
| Status | Code | When |
|---|---|---|
401 |
invalid_api_key |
Missing, malformed, or revoked API key. |
403 |
insufficient_scope |
The key is valid but lacks a scope this endpoint requires. |
429 |
rate_limited |
Per-key or per-org rate limit exceeded — back off with jitter and retry. |