---
title: Calculations
description: The Calculations API runs a should-cost calculation on a part and returns the priced result.
---

# Calculations

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

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

## List Calculations

`GET /api/v1/parts/calculations`

List recent calculations for the tenant.

Returns the most recent ``limit`` rows ordered by ``created_at``
descending with a slim projection — no analytics blob, to keep the
list response cheap.

Optional ``part_id`` narrows the result to a single part.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no | Maximum number of results to return. |
| `status_filter` | query | string | no | Filter by lifecycle status (e.g. queued, running, succeeded, failed, cancelled). |
| `part_id` | query | string | no | Identifier of the part. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X GET https://api.arcnm.io/api/v1/parts/calculations \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/parts/calculations",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/parts/calculations", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()
```

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

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


**Response body** `200`

| Field | Type | Description |
| --- | --- | --- |
| `count` | integer | Number of calculations returned in ``items`` (this page's size). |
| `items` | CalculationListItem[] | The calculations on this page, newest first. |

**Example response**

```json
{
  "count": 0,
  "items": [
    {
      "created_at": "string",
      "currency": "EUR",
      "engine": "arcanum",
      "finished_at": "string",
      "id": "string",
      "lot_size": 50,
      "material_grade_id": "string",
      "material_ref": "1.4301",
      "name": "string",
      "started_at": "string",
      "status": "string",
      "unit_cost": 12.84
    }
  ]
}
```

## Create a calculation (queued)

`POST /api/v1/parts/calculations`

Create a new Calculation row tied to (revision × env × dataset). Does NOT enqueue — call ``POST /parts/calculations/{id}/run`` to schedule.

One engine (**ARCNM**), one endpoint. The platform chooses the right drawing-understanding depth per calculation automatically. The optional ``extraction_tier`` field lets you override that depth — ``auto`` (default), ``lite`` (faster), or ``full`` (deepest); most callers should leave it unset.

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `annual_volume` | integer | no | Expected yearly quantity, used for amortizing setup over the run (>= 1). |
| `costing_environment_id` | string | yes | UUID of the costing environment (machine rates, region) to price against. |
| `currency` | string | no | ISO 4217 currency code for the quote; defaults to the costing environment's currency when omitted. |
| `dataset_link_id` | string | no | UUID of a specific dataset link to use; omit to use the revision's active primary CAD. |
| `engine` | string | no | Pricing engine selector; retained for back-compat and always normalized to the sole engine. |
| `extraction_tier` | `auto` \| `lite` \| `full` | no | Drawing-understanding depth. ``auto`` (default) lets the platform choose the right depth per drawing; ``lite`` is faster and lighter; ``full`` is the deepest. Most callers should leave this unset. |
| `lot_size` | integer | no | Number of identical parts produced per batch (>= 1). |
| `material_grade_id` | string | no | UUID of a resolved material grade; takes precedence over material_ref when both are supplied. |
| `material_ref` | string | no | Free-form material reference (URN, Werkstoffnummer, AISI/SAE code, trade name, or your SKU); resolved to a material grade. |
| `name` | string | no | Optional human-readable label; defaults to a timestamped name when omitted. |
| `part_revision_id` | string | yes | UUID of the part revision to price. |
| `region` | string | no | Pricing region override; defaults to the costing environment's region when omitted. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/parts/calculations \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "part_revision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  }'
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/parts/calculations",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "part_revision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    },
)
resp.raise_for_status()
print(resp.json())
```

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

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `201` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) 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 |
| --- | --- | --- |
| `enqueued_task` | string | Name of the background task enqueued for this run; null if nothing was scheduled. |
| `id` | string | UUID of the calculation. |
| `job_id` | string | Identifier of the queued background job; null when no job was enqueued. |
| `status` | string | Current lifecycle state (e.g. queued, running, succeeded, failed, cancelled). |

**Example response**

```json
{
  "enqueued_task": "string",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "job_id": "string",
  "status": "string"
}
```

## Delete Calculation

`DELETE /api/v1/parts/calculations/{calculation_id}`

Hard-delete a calculation row.

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

**Parameters**

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

**Request**

<CodeTabs>

```bash title="cURL"
curl -X DELETE https://api.arcnm.io/api/v1/parts/calculations/{calculation_id} \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.delete(
    "https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}", {
  method: "DELETE",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()
```

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

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


**Response body** `200`

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

**Example response**

```json
{
  "message": "string"
}
```

## Get Calculation

`GET /api/v1/parts/calculations/{calculation_id}`

**Parameters**

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

**Request**

<CodeTabs>

```bash title="cURL"
curl -X GET https://api.arcnm.io/api/v1/parts/calculations/{calculation_id} \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()
```

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

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


**Response body** `200`

| Field | Type | Description |
| --- | --- | --- |
| `analytics` | object | Free-form engine-output object carrying the cost breakdown and audit detail; its internal keys may evolve. |
| `annual_volume` | integer | Expected yearly quantity used to amortize setup cost. |
| `costing_environment_id` | string | UUID of the costing environment used for pricing. |
| `created_at` | string | ISO 8601 timestamp when the calculation was created. |
| `currency` | string | ISO 4217 currency code for the cost figures. |
| `engine` | string | Pricing engine used for this calculation. |
| `error` | string | Failure message; null unless the run failed. |
| `finished_at` | string | ISO 8601 timestamp when the run finished; null before it completes. |
| `id` | string | UUID of the calculation. |
| `lot_size` | integer | Number of identical parts produced per batch. |
| `material_grade_id` | string | UUID of the linked material grade; null when no grade is set. |
| `material_ref` | string | Material reference (URN) of the linked grade; null when no grade is set. |
| `name` | string | Human-readable label for the calculation. |
| `part_id` | string | UUID of the part this calculation belongs to. |
| `part_revision_id` | string | UUID of the part revision that was priced. |
| `setup_cost` | number | One-time setup cost in the quote's currency; null until pricing completes. |
| `started_at` | string | ISO 8601 timestamp when the run started; null before it begins. |
| `status` | string | Current lifecycle state (e.g. queued, running, succeeded, failed, cancelled). |
| `total_cost` | number | Total cost for the full lot in the quote's currency; null until pricing completes. |
| `total_time_s` | number | Total production time for the lot in seconds; null until pricing completes. |
| `unit_cost` | number | Cost per part in the quote's currency; null until pricing completes. |
| `unit_time_s` | number | Production time per part in seconds; null until pricing completes. |

**Example response**

```json
{
  "analytics": {},
  "annual_volume": 500,
  "costing_environment_id": "string",
  "created_at": "string",
  "currency": "EUR",
  "engine": "arcanum",
  "error": "string",
  "finished_at": "string",
  "id": "string",
  "lot_size": 50,
  "material_grade_id": "string",
  "material_ref": "1.4301",
  "name": "string",
  "part_id": "string",
  "part_revision_id": "string",
  "setup_cost": 52.5,
  "started_at": "string",
  "status": "string",
  "total_cost": 642,
  "total_time_s": 184,
  "unit_cost": 12.84,
  "unit_time_s": 184
}
```

## Cancel Calculation

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

Cancel a queued / running / polling calculation.

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

**Parameters**

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

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}/cancel \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}/cancel",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}/cancel", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()
```

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) 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 |
| --- | --- | --- |
| `enqueued_task` | string | Name of the background task enqueued for this run; null if nothing was scheduled. |
| `id` | string | UUID of the calculation. |
| `job_id` | string | Identifier of the queued background job; null when no job was enqueued. |
| `status` | string | Current lifecycle state (e.g. queued, running, succeeded, failed, cancelled). |

**Example response**

```json
{
  "enqueued_task": "string",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "job_id": "string",
  "status": "string"
}
```

## Upload Inputs

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

Attach a 2D drawing PDF / STL mesh / RFQ text file to the calculation.

The file is stored against the calculation's part revision under the
requested ``role``. Subsequent ``POST /run`` calls auto-discover it
by role so the pipeline can fan out additional extractors (drawing,
mesh fallback, RFQ text) alongside the primary geometry pass.

**Parameters**

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

**Request body** (`multipart/form-data`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `file` | string | yes | The file to attach (2D drawing PDF, STL mesh, or RFQ text). |
| `role` | string | yes | Role the file plays on the calculation's revision (e.g. drawing_2d, mesh_3d, rfq_text). |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}/inputs \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -F "role=primary" \
  -F "file=@file.bin"
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}/inputs",
    headers={"X-API-Key": "YOUR_API_KEY"},
    files={
        "file": open("file.bin", "rb"),
    },
    data={
        "role": "primary",
    },
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const form = new FormData()
form.append("role", "primary")
form.append("file", file) // a File or Blob

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

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) 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 |
| --- | --- | --- |
| `billing` | object | Free-form billing object describing the upload charge; its internal keys may evolve. |
| `calculation_id` | string | UUID of the calculation the file was attached to. |
| `data_source_id` | string | UUID of the stored data source created for the uploaded file. |
| `role` | string | Role the file was attached under (e.g. drawing_2d, mesh, rfq_text). |
| `sha256` | string | Hex-encoded SHA-256 digest of the uploaded bytes. |
| `size_bytes` | integer | Size of the uploaded file in bytes. |

**Example response**

```json
{
  "billing": {},
  "calculation_id": "string",
  "data_source_id": "string",
  "role": "primary",
  "sha256": "9f86d081884c7d659a2feaa0c55ad015…",
  "size_bytes": 204800
}
```

## Patch Calculation Material

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

Override the material on an existing calculation.

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

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

**Parameters**

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

**Request body** (`application/json`)

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

**Request**

<CodeTabs>

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

```python title="Python"
import requests

resp = requests.patch(
    "https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}/material",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "material_grade_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "material_ref": "1.4301"
    },
)
resp.raise_for_status()
print(resp.json())
```

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

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

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


**Response body** `200`

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

**Example response**

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

## Run Calculation

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

Enqueue the calculation onto the worker queue.

Idempotent: a row in a non-terminal state (queued/running/polling)
is not re-enqueued; a row in ``succeeded`` is returned untouched;
a row in ``failed``/``cancelled``/``timed_out`` is reset to
``queued`` and re-enqueued so the user can retry.

**Parameters**

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

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}/run \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}/run",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}/run", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()
```

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `202` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

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


**Response body** `202`

| Field | Type | Description |
| --- | --- | --- |
| `enqueued_task` | string | Name of the background task enqueued for this run; null if nothing was scheduled. |
| `id` | string | UUID of the calculation. |
| `job_id` | string | Identifier of the queued background job; null when no job was enqueued. |
| `status` | string | Current lifecycle state (e.g. queued, running, succeeded, failed, cancelled). |

**Example response**

```json
{
  "enqueued_task": "string",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "job_id": "string",
  "status": "string"
}
```

## Select Machine

`POST /api/v1/parts/calculations/{calculation_id}/select-machine`

Run geometry extraction and machine selection only, returning the rationale.

Cheaper than a full calculation (skips planning, physics, and
economics); typical latency 200–800 ms for a 10-machine fleet.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `calculation_id` | path | string | yes | Identifier of the calculation. |
| `X-Request-ID` | header | string | no | Optional client-supplied request ID; reused as an idempotency key for the metered charge. |
| `Idempotency-Key` | header | string | no | Optional key that makes this request safely retryable — see Idempotency. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}/select-machine \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}/select-machine",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/parts/calculations/{calculation_id}/select-machine", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
const data = await resp.json()
```

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

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


**Response body** `200`

| Field | Type | Description |
| --- | --- | --- |
| `calculation_id` | string | Identifier of the calculation the selection was run for. |
| `candidates` | SelectionCandidatePayload[] | All machines evaluated, with their feasibility and scoring. |
| `chosen_machine_id` | string | Identifier of the selected machine, or null if none is feasible. |
| `chosen_machine_name` | string | Name of the selected machine, or null if none is feasible. |
| `rationale_text` | string | Human-readable explanation of why the machine was or was not selected. |

**Example response**

```json
{
  "calculation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "candidates": [
    {
      "blocking_violations": [],
      "capability_score": 0,
      "feasible": true,
      "machine_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "machine_name": "string",
      "provisional_unit_cost_eur": 0
    }
  ],
  "chosen_machine_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "chosen_machine_name": "string",
  "rationale_text": "string"
}
```

## Bulk Cancel Calculations

`POST /api/v1/parts/calculations/bulk-cancel`

**Request body** (`application/json`)

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

**Request**

<CodeTabs>

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

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/parts/calculations/bulk-cancel",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "ids": [
            "3fa85f64-5717-4562-b3fc-2c963f66afa6"
        ]
    },
)
resp.raise_for_status()
print(resp.json())
```

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

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

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


**Response body** `200`

| Field | Type | Description |
| --- | --- | --- |
| `not_found` | string[] | IDs that did not match a calculation for this tenant. |
| `skipped` | string[] | IDs skipped because their state didn't allow the action. |
| `succeeded` | string[] | IDs of calculations the bulk action applied to successfully. |

**Example response**

```json
{
  "not_found": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "skipped": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "succeeded": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ]
}
```

## Bulk Delete Calculations

`POST /api/v1/parts/calculations/bulk-delete`

**Request body** (`application/json`)

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

**Request**

<CodeTabs>

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

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/parts/calculations/bulk-delete",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "ids": [
            "3fa85f64-5717-4562-b3fc-2c963f66afa6"
        ]
    },
)
resp.raise_for_status()
print(resp.json())
```

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

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

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


**Response body** `200`

| Field | Type | Description |
| --- | --- | --- |
| `not_found` | string[] | IDs that did not match a calculation for this tenant. |
| `skipped` | string[] | IDs skipped because their state didn't allow the action. |
| `succeeded` | string[] | IDs of calculations the bulk action applied to successfully. |

**Example response**

```json
{
  "not_found": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "skipped": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "succeeded": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ]
}
```

## Bulk Retry Calculations

`POST /api/v1/parts/calculations/bulk-retry`

Re-enqueue eligible failed/cancelled/timed_out rows.

Each row goes through the same place-hold + enqueue path as a
direct `POST /run` so wallet semantics and attempt budgets are
enforced identically. A row that's already running, succeeded,
or out of attempt budget lands in `skipped`.

**Request body** (`application/json`)

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

**Request**

<CodeTabs>

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

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/parts/calculations/bulk-retry",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "ids": [
            "3fa85f64-5717-4562-b3fc-2c963f66afa6"
        ]
    },
)
resp.raise_for_status()
print(resp.json())
```

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

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `200` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

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


**Response body** `200`

| Field | Type | Description |
| --- | --- | --- |
| `not_found` | string[] | IDs that did not match a calculation for this tenant. |
| `skipped` | string[] | IDs skipped because their state didn't allow the action. |
| `succeeded` | string[] | IDs of calculations the bulk action applied to successfully. |

**Example response**

```json
{
  "not_found": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "skipped": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ],
  "succeeded": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ]
}
```

## Quote

`POST /api/v1/parts/calculations/quote`

Convenience: create + enqueue in one round-trip.

Equivalent to ``POST / + POST /{id}/run``.

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `annual_volume` | integer | no | Expected yearly quantity, used for amortizing setup over the run (>= 1). |
| `costing_environment_id` | string | yes | UUID of the costing environment (machine rates, region) to price against. |
| `currency` | string | no | ISO 4217 currency code for the quote; defaults to the costing environment's currency when omitted. |
| `dataset_link_id` | string | no | UUID of a specific dataset link to use; omit to use the revision's active primary CAD. |
| `engine` | string | no | Pricing engine selector; retained for back-compat and always normalized to the sole engine. |
| `extraction_tier` | `auto` \| `lite` \| `full` | no | Drawing-understanding depth. ``auto`` (default) lets the platform choose the right depth per drawing; ``lite`` is faster and lighter; ``full`` is the deepest. Most callers should leave this unset. |
| `lot_size` | integer | no | Number of identical parts produced per batch (>= 1). |
| `material_grade_id` | string | no | UUID of a resolved material grade; takes precedence over material_ref when both are supplied. |
| `material_ref` | string | no | Free-form material reference (URN, Werkstoffnummer, AISI/SAE code, trade name, or your SKU); resolved to a material grade. |
| `name` | string | no | Optional human-readable label; defaults to a timestamped name when omitted. |
| `part_revision_id` | string | yes | UUID of the part revision to price. |
| `region` | string | no | Pricing region override; defaults to the costing environment's region when omitted. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/parts/calculations/quote \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "part_revision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  }'
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/parts/calculations/quote",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "part_revision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    },
)
resp.raise_for_status()
print(resp.json())
```

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

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `202` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

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


**Response body** `202`

| Field | Type | Description |
| --- | --- | --- |
| `enqueued_task` | string | Name of the background task enqueued for this run; null if nothing was scheduled. |
| `id` | string | UUID of the calculation. |
| `job_id` | string | Identifier of the queued background job; null when no job was enqueued. |
| `status` | string | Current lifecycle state (e.g. queued, running, succeeded, failed, cancelled). |

**Example response**

```json
{
  "enqueued_task": "string",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "job_id": "string",
  "status": "string"
}
```

## Upload And Quote

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

One-shot upload + new-calculation flow.

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

Idempotency: ``part_number`` is the natural key — if a Part with
the same number already exists for this tenant, we attach a new
revision rather than re-creating the Part.

**Request body** (`multipart/form-data`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `annual_volume` | integer | no | Expected yearly quantity used to amortize setup cost (1 to 1,000,000,000). |
| `cad_file` | string | yes | 3D CAD file (e.g. STEP) to price; required. |
| `costing_environment_id` | string | yes | UUID of the costing environment (machine rates, region) to price against. |
| `drawing_file` | string | no | Optional 2D drawing (PDF/PNG/JPEG) for the part. |
| `engine` | string | no | Pricing engine selector; retained for back-compat and always normalized to the sole engine. |
| `extraction_tier` | string | no | Drawing-understanding depth override: auto (default), lite (faster), or full (deepest). Most callers should leave this unset. |
| `lot_size` | integer | no | Number of identical parts produced per batch (1 to 1,000,000,000). |
| `material_grade_id` | string | no | UUID of a resolved material grade; takes precedence over material_ref when both are supplied. |
| `material_ref` | string | no | Free-form material reference (URN, Werkstoffnummer, AISI/SAE code, trade name, or your SKU). |
| `part_number` | string | yes | Natural-key part number; reused to attach a new revision if the part already exists. |
| `rfq_file` | string | no | Optional RFQ text file with requirements for the part. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/parts/calculations/upload-and-quote \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -F "costing_environment_id=3fa85f64-5717-4562-b3fc-2c963f66afa6" \
  -F "part_number=BRACKET-001" \
  -F "cad_file=@part.step"
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/parts/calculations/upload-and-quote",
    headers={"X-API-Key": "YOUR_API_KEY"},
    files={
        "cad_file": open("part.step", "rb"),
    },
    data={
        "costing_environment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "part_number": "BRACKET-001",
    },
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const form = new FormData()
form.append("costing_environment_id", "3fa85f64-5717-4562-b3fc-2c963f66afa6")
form.append("part_number", "BRACKET-001")
form.append("cad_file", file) // a File or Blob

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

</CodeTabs>

**Responses**

| Status | Description |
| --- | --- |
| `202` | Successful Response |
| `422` | Validation Error |

**Errors**

Standard error responses — see the [Errors catalog](../errors.md) for the full envelope, `request_id`, and retry-safety table.

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


**Response body** `202`

| Field | Type | Description |
| --- | --- | --- |
| `enqueued_task` | string | Name of the background task enqueued for this run; null if nothing was scheduled. |
| `id` | string | UUID of the calculation. |
| `job_id` | string | Identifier of the queued background job; null when no job was enqueued. |
| `status` | string | Current lifecycle state (e.g. queued, running, succeeded, failed, cancelled). |

**Example response**

```json
{
  "enqueued_task": "string",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "job_id": "string",
  "status": "string"
}
```
