---
title: Tools
description: The Tools API manages your organisation's cutting-tool library: create, list, fetch, update, and delete tools, set per-material cutting data on each, and…
---

# Tools

The Tools API manages your organisation's cutting-tool library: create, list, fetch, update, and delete tools, set per-material cutting data on each, and browse the tool-type catalogue.

> **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 Tools

`GET /api/v1/tools`

Your organization's tool library, name-ordered, with each tool's
per-material cutting data inlined.

> **Paginated.** Pass `cursor` (from the previous response) to fetch the next page; `limit` caps 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. |
| `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**

<CodeTabs>

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

```python title="Python"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/tools",
    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/tools", {
  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. |


## Create Tool

`POST /api/v1/tools`

Add a tool to your organization's library, optionally with its
per-material cutting data.

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `attach_to_environment_ids` | string[] | no | Costing environments the new tool joins (D7 — membership is always explicit). Omit for the documented default: every current environment of your organization. Send [] to attach nowhere (the tool exists in the crib but prices nothing until attached). |
| `coating` | string | no | Coating designation (e.g. TiAlN), if any. |
| `cutting_material` | string | no | Cutting-material class (HSS, carbide, coated carbide, ...). |
| `diameter_mm` | number | no | Cutting diameter, in millimetres (null for turning inserts). |
| `item_no` | string | no | Vendor item / order number. |
| `kind` | `end_mill` \| `face_mill` \| `drill` \| `spot_drill` \| `tap` \| `reamer` \| `boring_bar` \| `thread_mill` \| `insert_holder_turn` \| `grooving_tool` \| `grinding_wheel` \| `chamfer_mill` \| `saw_blade` \| `edm_wire` \| `press_brake_punch` \| `press_brake_die` \| `tube_bend_die` \| `plasma_consumable_set` \| `laser_nozzle` \| `waterjet_nozzle` \| `punch_die_set` \| `weld_consumable` \| `broach` | yes | Tool kind (VDI 2852 class), e.g. end_mill or drill. |
| `name` | string | yes | Human-readable name of the tool. |
| `notes` | string | no | Free-form shop notes. |
| `parameters` | CuttingToolParameterWrite[] | no | Optional cutting-data rows to create with the tool — one row per parameter, keyed (material, operation, size, quality). |
| `price_eur` | number | no | Replacement price, in EUR. |
| `spec` | object | no | Kind-specific properties — e.g. a press-brake die's V opening and load rating, a saw blade's kerf and pitch, a plasma consumable set's amperage. Validated per kind; empty for rotating cutters, whose geometry uses the fields above. |
| `teeth` | integer | no | Number of cutting edges / flutes / blade teeth. |
| `tool_life_min` | number | no | Expected tool life in cutting minutes (per edge set). |
| `vendor` | string | no | Tool manufacturer / vendor. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/tools \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "string",
    "kind": "end_mill"
  }'
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/tools",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "name": "string",
        "kind": "end_mill"
    },
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/tools", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "name": "string",
    "kind": "end_mill"
  }),
})
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 |
| --- | --- | --- |
| `baseline` | ToolBaselineMembership |  |
| `catalogue_item_id` | string |  |
| `coating` | string | Coating designation (e.g. TiAlN), if any. |
| `created_at` | string |  |
| `cutting_data_count` | integer | Number of cutting-data rows this tool prices with: the values stated on the tool itself plus its catalogue article's data sheet. 0 means the tool carries none of its own and pricing resolves from the platform's cutting-data tables. |
| `cutting_material` | string | Cutting-material class (HSS, carbide, coated carbide, ...). |
| `diameter_mm` | number | Cutting diameter, in millimetres (null for turning inserts). |
| `differs_from_article` | boolean |  |
| `hidden` | boolean |  |
| `id` | string |  |
| `item_no` | string | Vendor item / order number. |
| `kind` | `end_mill` \| `face_mill` \| `drill` \| `spot_drill` \| `tap` \| `reamer` \| `boring_bar` \| `thread_mill` \| `insert_holder_turn` \| `grooving_tool` \| `grinding_wheel` \| `chamfer_mill` \| `saw_blade` \| `edm_wire` \| `press_brake_punch` \| `press_brake_die` \| `tube_bend_die` \| `plasma_consumable_set` \| `laser_nozzle` \| `waterjet_nozzle` \| `punch_die_set` \| `weld_consumable` \| `broach` | Tool kind (VDI 2852 class), e.g. end_mill or drill. |
| `name` | string | Human-readable name of the tool. |
| `notes` | string | Free-form shop notes. |
| `parameters` | CuttingToolParameterPublic[] |  |
| `price_eur` | number | Replacement price, in EUR. |
| `price_is_inherited` | boolean |  |
| `priced_into_quotes` | boolean | Whether a price and a life stated on this tool can reach a quote. False for kinds no pricing path consumes yet (welding and EDM consumables, brake punches, tube-bend dies): the tool is still storable, curatable and attachable to an environment, but its economics change no cost today. Laser and waterjet nozzles, plasma sets and punch sets are priced per pierce, minute or stroke. |
| `spec` | object | Kind-specific properties — e.g. a press-brake die's V opening and load rating, a saw blade's kerf and pitch, a plasma consumable set's amperage. Validated per kind; empty for rotating cutters, whose geometry uses the fields above. |
| `teeth` | integer | Number of cutting edges / flutes / blade teeth. |
| `tool_life_min` | number | Expected tool life in cutting minutes (per edge set). |
| `updated_at` | string |  |
| `valid_from` | string |  |
| `valid_to` | string |  |
| `vendor` | string | Tool manufacturer / vendor. |

**Example response**

```json
{
  "baseline": {
    "fits_machine_names": [
      "string"
    ],
    "fits_machines": true,
    "is_enabled": true,
    "membership_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  },
  "catalogue_item_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "coating": "string",
  "created_at": "2026-06-01T12:00:00Z",
  "cutting_data_count": 0,
  "cutting_material": "carbide",
  "diameter_mm": 0,
  "differs_from_article": false,
  "hidden": false,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "item_no": "string",
  "kind": "end_mill",
  "name": "string",
  "notes": "string",
  "parameters": [],
  "price_eur": 0,
  "price_is_inherited": false,
  "priced_into_quotes": true,
  "spec": {},
  "teeth": 0,
  "tool_life_min": 0,
  "updated_at": "2026-06-01T12:00:00Z",
  "valid_from": "2026-06-01",
  "valid_to": "2026-06-01",
  "vendor": "string"
}
```

## Delete Tool

`DELETE /api/v1/tools/{tool_id}`

Retire a tool (SCD2, 0141): the row closes its validity window so a
replay of an old calculation still sees it, and its active environment
memberships close with it. A tool created and retired the same day is
hard-deleted (a zero-length window would still read as active today).
Environment cutting-data overrides already applied from it are
independent statements of shop practice and stay in force.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tool_id` | path | string | yes | Identifier of the tool. |

**Request**

<CodeTabs>

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

```python title="Python"
import requests

resp = requests.delete(
    "https://api.arcnm.io/api/v1/tools/{tool_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/tools/{tool_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 result of the operation. |

**Example response**

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

## Get Tool

`GET /api/v1/tools/{tool_id}`

Fetch one tool with its per-material cutting data.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tool_id` | path | string | yes | Identifier of the tool. |

**Request**

<CodeTabs>

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

```python title="Python"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/tools/{tool_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/tools/{tool_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 |
| --- | --- | --- |
| `baseline` | ToolBaselineMembership |  |
| `catalogue_item_id` | string |  |
| `coating` | string | Coating designation (e.g. TiAlN), if any. |
| `created_at` | string |  |
| `cutting_data_count` | integer | Number of cutting-data rows this tool prices with: the values stated on the tool itself plus its catalogue article's data sheet. 0 means the tool carries none of its own and pricing resolves from the platform's cutting-data tables. |
| `cutting_material` | string | Cutting-material class (HSS, carbide, coated carbide, ...). |
| `diameter_mm` | number | Cutting diameter, in millimetres (null for turning inserts). |
| `differs_from_article` | boolean |  |
| `hidden` | boolean |  |
| `id` | string |  |
| `item_no` | string | Vendor item / order number. |
| `kind` | `end_mill` \| `face_mill` \| `drill` \| `spot_drill` \| `tap` \| `reamer` \| `boring_bar` \| `thread_mill` \| `insert_holder_turn` \| `grooving_tool` \| `grinding_wheel` \| `chamfer_mill` \| `saw_blade` \| `edm_wire` \| `press_brake_punch` \| `press_brake_die` \| `tube_bend_die` \| `plasma_consumable_set` \| `laser_nozzle` \| `waterjet_nozzle` \| `punch_die_set` \| `weld_consumable` \| `broach` | Tool kind (VDI 2852 class), e.g. end_mill or drill. |
| `name` | string | Human-readable name of the tool. |
| `notes` | string | Free-form shop notes. |
| `parameters` | CuttingToolParameterPublic[] |  |
| `price_eur` | number | Replacement price, in EUR. |
| `price_is_inherited` | boolean |  |
| `priced_into_quotes` | boolean | Whether a price and a life stated on this tool can reach a quote. False for kinds no pricing path consumes yet (welding and EDM consumables, brake punches, tube-bend dies): the tool is still storable, curatable and attachable to an environment, but its economics change no cost today. Laser and waterjet nozzles, plasma sets and punch sets are priced per pierce, minute or stroke. |
| `spec` | object | Kind-specific properties — e.g. a press-brake die's V opening and load rating, a saw blade's kerf and pitch, a plasma consumable set's amperage. Validated per kind; empty for rotating cutters, whose geometry uses the fields above. |
| `teeth` | integer | Number of cutting edges / flutes / blade teeth. |
| `tool_life_min` | number | Expected tool life in cutting minutes (per edge set). |
| `updated_at` | string |  |
| `valid_from` | string |  |
| `valid_to` | string |  |
| `vendor` | string | Tool manufacturer / vendor. |

**Example response**

```json
{
  "baseline": {
    "fits_machine_names": [
      "string"
    ],
    "fits_machines": true,
    "is_enabled": true,
    "membership_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  },
  "catalogue_item_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "coating": "string",
  "created_at": "2026-06-01T12:00:00Z",
  "cutting_data_count": 0,
  "cutting_material": "carbide",
  "diameter_mm": 0,
  "differs_from_article": false,
  "hidden": false,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "item_no": "string",
  "kind": "end_mill",
  "name": "string",
  "notes": "string",
  "parameters": [],
  "price_eur": 0,
  "price_is_inherited": false,
  "priced_into_quotes": true,
  "spec": {},
  "teeth": 0,
  "tool_life_min": 0,
  "updated_at": "2026-06-01T12:00:00Z",
  "valid_from": "2026-06-01",
  "valid_to": "2026-06-01",
  "vendor": "string"
}
```

## Update Tool

`PATCH /api/v1/tools/{tool_id}`

Edit a tool's identity / geometry / economics fields.

Every field but the bookkeeping ones (vendor, item number, coating,
notes) can change what this tool costs you, so editing one takes
effect immediately in every costing environment the tool is attached
to.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tool_id` | path | string | yes | Identifier of the tool. |

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `coating` | string | no |  |
| `cutting_material` | string | no |  |
| `diameter_mm` | number | no |  |
| `if_unmodified_since` | string | no | Optimistic concurrency. Send the `updated_at` you last read; the write is refused with 409 if the tool changed since. Omit to write unconditionally (last write wins). |
| `item_no` | string | no |  |
| `kind` | `end_mill` \| `face_mill` \| `drill` \| `spot_drill` \| `tap` \| `reamer` \| `boring_bar` \| `thread_mill` \| `insert_holder_turn` \| `grooving_tool` \| `grinding_wheel` \| `chamfer_mill` \| `saw_blade` \| `edm_wire` \| `press_brake_punch` \| `press_brake_die` \| `tube_bend_die` \| `plasma_consumable_set` \| `laser_nozzle` \| `waterjet_nozzle` \| `punch_die_set` \| `weld_consumable` \| `broach` | no |  |
| `name` | string | no |  |
| `notes` | string | no |  |
| `price_eur` | number | no |  |
| `spec` | object | no | Kind-specific properties; validated against the (possibly changed) kind. When you change a tool's kind, send the spec for the NEW kind in the same request — the old kind's spec does not carry over. |
| `teeth` | integer | no |  |
| `tool_life_min` | number | no |  |
| `vendor` | string | no |  |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X PATCH https://api.arcnm.io/api/v1/tools/{tool_id} \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "coating": "string",
    "cutting_material": "string",
    "diameter_mm": 0,
    "if_unmodified_since": "2026-06-01T12:00:00Z",
    "item_no": "string",
    "kind": "end_mill",
    "name": "string",
    "notes": "string",
    "price_eur": 0,
    "spec": {},
    "teeth": 0,
    "tool_life_min": 0,
    "vendor": "string"
  }'
```

```python title="Python"
import requests

resp = requests.patch(
    "https://api.arcnm.io/api/v1/tools/{tool_id}",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "coating": "string",
        "cutting_material": "string",
        "diameter_mm": 0,
        "if_unmodified_since": "2026-06-01T12:00:00Z",
        "item_no": "string",
        "kind": "end_mill",
        "name": "string",
        "notes": "string",
        "price_eur": 0,
        "spec": {},
        "teeth": 0,
        "tool_life_min": 0,
        "vendor": "string"
    },
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/tools/{tool_id}", {
  method: "PATCH",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "coating": "string",
    "cutting_material": "string",
    "diameter_mm": 0,
    "if_unmodified_since": "2026-06-01T12:00:00Z",
    "item_no": "string",
    "kind": "end_mill",
    "name": "string",
    "notes": "string",
    "price_eur": 0,
    "spec": {},
    "teeth": 0,
    "tool_life_min": 0,
    "vendor": "string"
  }),
})
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 |
| --- | --- | --- |
| `baseline` | ToolBaselineMembership |  |
| `catalogue_item_id` | string |  |
| `coating` | string | Coating designation (e.g. TiAlN), if any. |
| `created_at` | string |  |
| `cutting_data_count` | integer | Number of cutting-data rows this tool prices with: the values stated on the tool itself plus its catalogue article's data sheet. 0 means the tool carries none of its own and pricing resolves from the platform's cutting-data tables. |
| `cutting_material` | string | Cutting-material class (HSS, carbide, coated carbide, ...). |
| `diameter_mm` | number | Cutting diameter, in millimetres (null for turning inserts). |
| `differs_from_article` | boolean |  |
| `hidden` | boolean |  |
| `id` | string |  |
| `item_no` | string | Vendor item / order number. |
| `kind` | `end_mill` \| `face_mill` \| `drill` \| `spot_drill` \| `tap` \| `reamer` \| `boring_bar` \| `thread_mill` \| `insert_holder_turn` \| `grooving_tool` \| `grinding_wheel` \| `chamfer_mill` \| `saw_blade` \| `edm_wire` \| `press_brake_punch` \| `press_brake_die` \| `tube_bend_die` \| `plasma_consumable_set` \| `laser_nozzle` \| `waterjet_nozzle` \| `punch_die_set` \| `weld_consumable` \| `broach` | Tool kind (VDI 2852 class), e.g. end_mill or drill. |
| `name` | string | Human-readable name of the tool. |
| `notes` | string | Free-form shop notes. |
| `parameters` | CuttingToolParameterPublic[] |  |
| `price_eur` | number | Replacement price, in EUR. |
| `price_is_inherited` | boolean |  |
| `priced_into_quotes` | boolean | Whether a price and a life stated on this tool can reach a quote. False for kinds no pricing path consumes yet (welding and EDM consumables, brake punches, tube-bend dies): the tool is still storable, curatable and attachable to an environment, but its economics change no cost today. Laser and waterjet nozzles, plasma sets and punch sets are priced per pierce, minute or stroke. |
| `spec` | object | Kind-specific properties — e.g. a press-brake die's V opening and load rating, a saw blade's kerf and pitch, a plasma consumable set's amperage. Validated per kind; empty for rotating cutters, whose geometry uses the fields above. |
| `teeth` | integer | Number of cutting edges / flutes / blade teeth. |
| `tool_life_min` | number | Expected tool life in cutting minutes (per edge set). |
| `updated_at` | string |  |
| `valid_from` | string |  |
| `valid_to` | string |  |
| `vendor` | string | Tool manufacturer / vendor. |

**Example response**

```json
{
  "baseline": {
    "fits_machine_names": [
      "string"
    ],
    "fits_machines": true,
    "is_enabled": true,
    "membership_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  },
  "catalogue_item_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "coating": "string",
  "created_at": "2026-06-01T12:00:00Z",
  "cutting_data_count": 0,
  "cutting_material": "carbide",
  "diameter_mm": 0,
  "differs_from_article": false,
  "hidden": false,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "item_no": "string",
  "kind": "end_mill",
  "name": "string",
  "notes": "string",
  "parameters": [],
  "price_eur": 0,
  "price_is_inherited": false,
  "priced_into_quotes": true,
  "spec": {},
  "teeth": 0,
  "tool_life_min": 0,
  "updated_at": "2026-06-01T12:00:00Z",
  "valid_from": "2026-06-01",
  "valid_to": "2026-06-01",
  "vendor": "string"
}
```

## Copy Tool

`POST /api/v1/tools/{tool_id}/copy`

Duplicate one of your own tools into a new tool of your own.

The copy takes the source's geometry, economics, kind-specific
properties and cutting data, under a name of its own —
"<source> (copy)". The source is untouched.

The copy is your tool, not a platform one: you can delete it, and it
is not part of the platform set, so hiding a platform article or
restoring the platform set never touches it. It also joins no costing
environment on its own — attach it where you want it to price with
``POST /environments/{env_id}/tools``. To install a platform article
instead, use ``POST /tools/catalogue-items/{item_id}/adopt``: an
adopted tool keeps its link to the article, and this one does not.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tool_id` | path | string | yes | Identifier of the tool. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/tools/{tool_id}/copy \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/tools/{tool_id}/copy",
    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/tools/{tool_id}/copy", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
  },
})
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. |
| `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 |
| --- | --- | --- |
| `baseline` | ToolBaselineMembership |  |
| `catalogue_item_id` | string |  |
| `coating` | string | Coating designation (e.g. TiAlN), if any. |
| `created_at` | string |  |
| `cutting_data_count` | integer | Number of cutting-data rows this tool prices with: the values stated on the tool itself plus its catalogue article's data sheet. 0 means the tool carries none of its own and pricing resolves from the platform's cutting-data tables. |
| `cutting_material` | string | Cutting-material class (HSS, carbide, coated carbide, ...). |
| `diameter_mm` | number | Cutting diameter, in millimetres (null for turning inserts). |
| `differs_from_article` | boolean |  |
| `hidden` | boolean |  |
| `id` | string |  |
| `item_no` | string | Vendor item / order number. |
| `kind` | `end_mill` \| `face_mill` \| `drill` \| `spot_drill` \| `tap` \| `reamer` \| `boring_bar` \| `thread_mill` \| `insert_holder_turn` \| `grooving_tool` \| `grinding_wheel` \| `chamfer_mill` \| `saw_blade` \| `edm_wire` \| `press_brake_punch` \| `press_brake_die` \| `tube_bend_die` \| `plasma_consumable_set` \| `laser_nozzle` \| `waterjet_nozzle` \| `punch_die_set` \| `weld_consumable` \| `broach` | Tool kind (VDI 2852 class), e.g. end_mill or drill. |
| `name` | string | Human-readable name of the tool. |
| `notes` | string | Free-form shop notes. |
| `parameters` | CuttingToolParameterPublic[] |  |
| `price_eur` | number | Replacement price, in EUR. |
| `price_is_inherited` | boolean |  |
| `priced_into_quotes` | boolean | Whether a price and a life stated on this tool can reach a quote. False for kinds no pricing path consumes yet (welding and EDM consumables, brake punches, tube-bend dies): the tool is still storable, curatable and attachable to an environment, but its economics change no cost today. Laser and waterjet nozzles, plasma sets and punch sets are priced per pierce, minute or stroke. |
| `spec` | object | Kind-specific properties — e.g. a press-brake die's V opening and load rating, a saw blade's kerf and pitch, a plasma consumable set's amperage. Validated per kind; empty for rotating cutters, whose geometry uses the fields above. |
| `teeth` | integer | Number of cutting edges / flutes / blade teeth. |
| `tool_life_min` | number | Expected tool life in cutting minutes (per edge set). |
| `updated_at` | string |  |
| `valid_from` | string |  |
| `valid_to` | string |  |
| `vendor` | string | Tool manufacturer / vendor. |

**Example response**

```json
{
  "baseline": {
    "fits_machine_names": [
      "string"
    ],
    "fits_machines": true,
    "is_enabled": true,
    "membership_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  },
  "catalogue_item_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "coating": "string",
  "created_at": "2026-06-01T12:00:00Z",
  "cutting_data_count": 0,
  "cutting_material": "carbide",
  "diameter_mm": 0,
  "differs_from_article": false,
  "hidden": false,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "item_no": "string",
  "kind": "end_mill",
  "name": "string",
  "notes": "string",
  "parameters": [],
  "price_eur": 0,
  "price_is_inherited": false,
  "priced_into_quotes": true,
  "spec": {},
  "teeth": 0,
  "tool_life_min": 0,
  "updated_at": "2026-06-01T12:00:00Z",
  "valid_from": "2026-06-01",
  "valid_to": "2026-06-01",
  "vendor": "string"
}
```

## Get Effective Cutting Data

`GET /api/v1/tools/{tool_id}/effective-cutting-data`

The cutting data this tool actually prices with in an environment
(the default one when none is named), per material bin and parameter,
with the tier that supplies each value.

In the app, nothing is hidden: a tool with no rows of its own still
shows the platform's numbers, so the dialog is never an empty table you
are expected to fill in. An API key or an MCP grant is an integration,
and gets what the environment cutting-data read gives one: the entries
the shop stated themselves, without the platform column.

The table is stated per material bin, and that is the one limit worth
naming. Where this tool carries a value of its own, the grade's ISO 513
workpiece group is not consulted at all — that value answers for every
grade on the bin. Where it carries none, the platform figure shown here
is the material bin's, while a part on a grade mapped to a workpiece
group prices on that group's cutting speed, which can differ. The
environment's cutting data is where those per-group speeds are read
and set.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tool_id` | path | string | yes | Identifier of the tool. |
| `env_id` | query | string | no | Identifier of the env. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X GET https://api.arcnm.io/api/v1/tools/{tool_id}/effective-cutting-data \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/tools/{tool_id}/effective-cutting-data",
    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/tools/{tool_id}/effective-cutting-data", {
  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 |
| --- | --- | --- |
| `env_id` | string |  |
| `own_rows_priced` | boolean | Whether a row stated on THIS tool reaches pricing. False for a kind whose handler resolves its process bin without the tool tier (insert holders, boring bars, thread mills, waterjet nozzles): the rows are reference data and every value below is what the engine actually prices with. |
| `process` | string | The process family whose bins this tool prices through; null for reference-only kinds (a tap prices from the milling reference, a reamer from the drilling one). |
| `reference_only` | boolean |  |
| `rows` | EffectiveCuttingRowPublic[] |  |
| `tool_id` | string |  |

**Example response**

```json
{
  "env_id": "string",
  "own_rows_priced": false,
  "process": "string",
  "reference_only": true,
  "rows": [
    {
      "material_code": "string",
      "material_label": "string",
      "parameter": "string",
      "process": "string",
      "size_mm": 0,
      "source": "string",
      "unit": "string",
      "value": 0
    }
  ],
  "tool_id": "string"
}
```

## Upsert Tool Parameters

`PUT /api/v1/tools/{tool_id}/parameters`

Create-or-replace cutting-data rows on one of YOUR tools (D1 key:
material × operation × size × quality × parameter). Vendor rows live on
the catalogue ARTICLE and are read-only here; your instance rows shadow
them on the same key at pricing time.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tool_id` | path | string | yes | Identifier of the tool. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X PUT https://api.arcnm.io/api/v1/tools/{tool_id}/parameters \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.put(
    "https://api.arcnm.io/api/v1/tools/{tool_id}/parameters",
    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/tools/{tool_id}/parameters", {
  method: "PUT",
  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. |


## Delete Tool Parameter

`DELETE /api/v1/tools/{tool_id}/parameters/{parameter_id}`

Remove one cutting-data row from one of your tools.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tool_id` | path | string | yes | Identifier of the tool. |
| `parameter_id` | path | string | yes | Identifier of the parameter. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X DELETE https://api.arcnm.io/api/v1/tools/{tool_id}/parameters/{parameter_id} \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.delete(
    "https://api.arcnm.io/api/v1/tools/{tool_id}/parameters/{parameter_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/tools/{tool_id}/parameters/{parameter_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 result of the operation. |

**Example response**

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

## Reset Tool To Article

`POST /api/v1/tools/{tool_id}/reset`

Reset a platform-lineage tool to its article's values ("Reset to
platform values"): geometry, economics, cutting material, spec, name
and notes come back from the article; the tool's own cutting-data rows
and its environment memberships are untouched. A custom tool (no
article) has nothing to reset to → 409.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tool_id` | path | string | yes | Identifier of the tool. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/tools/{tool_id}/reset \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/tools/{tool_id}/reset",
    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/tools/{tool_id}/reset", {
  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 |
| --- | --- | --- |
| `baseline` | ToolBaselineMembership |  |
| `catalogue_item_id` | string |  |
| `coating` | string | Coating designation (e.g. TiAlN), if any. |
| `created_at` | string |  |
| `cutting_data_count` | integer | Number of cutting-data rows this tool prices with: the values stated on the tool itself plus its catalogue article's data sheet. 0 means the tool carries none of its own and pricing resolves from the platform's cutting-data tables. |
| `cutting_material` | string | Cutting-material class (HSS, carbide, coated carbide, ...). |
| `diameter_mm` | number | Cutting diameter, in millimetres (null for turning inserts). |
| `differs_from_article` | boolean |  |
| `hidden` | boolean |  |
| `id` | string |  |
| `item_no` | string | Vendor item / order number. |
| `kind` | `end_mill` \| `face_mill` \| `drill` \| `spot_drill` \| `tap` \| `reamer` \| `boring_bar` \| `thread_mill` \| `insert_holder_turn` \| `grooving_tool` \| `grinding_wheel` \| `chamfer_mill` \| `saw_blade` \| `edm_wire` \| `press_brake_punch` \| `press_brake_die` \| `tube_bend_die` \| `plasma_consumable_set` \| `laser_nozzle` \| `waterjet_nozzle` \| `punch_die_set` \| `weld_consumable` \| `broach` | Tool kind (VDI 2852 class), e.g. end_mill or drill. |
| `name` | string | Human-readable name of the tool. |
| `notes` | string | Free-form shop notes. |
| `parameters` | CuttingToolParameterPublic[] |  |
| `price_eur` | number | Replacement price, in EUR. |
| `price_is_inherited` | boolean |  |
| `priced_into_quotes` | boolean | Whether a price and a life stated on this tool can reach a quote. False for kinds no pricing path consumes yet (welding and EDM consumables, brake punches, tube-bend dies): the tool is still storable, curatable and attachable to an environment, but its economics change no cost today. Laser and waterjet nozzles, plasma sets and punch sets are priced per pierce, minute or stroke. |
| `spec` | object | Kind-specific properties — e.g. a press-brake die's V opening and load rating, a saw blade's kerf and pitch, a plasma consumable set's amperage. Validated per kind; empty for rotating cutters, whose geometry uses the fields above. |
| `teeth` | integer | Number of cutting edges / flutes / blade teeth. |
| `tool_life_min` | number | Expected tool life in cutting minutes (per edge set). |
| `updated_at` | string |  |
| `valid_from` | string |  |
| `valid_to` | string |  |
| `vendor` | string | Tool manufacturer / vendor. |

**Example response**

```json
{
  "baseline": {
    "fits_machine_names": [
      "string"
    ],
    "fits_machines": true,
    "is_enabled": true,
    "membership_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  },
  "catalogue_item_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "coating": "string",
  "created_at": "2026-06-01T12:00:00Z",
  "cutting_data_count": 0,
  "cutting_material": "carbide",
  "diameter_mm": 0,
  "differs_from_article": false,
  "hidden": false,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "item_no": "string",
  "kind": "end_mill",
  "name": "string",
  "notes": "string",
  "parameters": [],
  "price_eur": 0,
  "price_is_inherited": false,
  "priced_into_quotes": true,
  "spec": {},
  "teeth": 0,
  "tool_life_min": 0,
  "updated_at": "2026-06-01T12:00:00Z",
  "valid_from": "2026-06-01",
  "valid_to": "2026-06-01",
  "vendor": "string"
}
```

## Tool Catalogue

`GET /api/v1/tools/catalogue`

The closed vocabulary for tools and cutting data — tool kinds,
cutting materials, canonical material bins and parameter windows.

**Request**

<CodeTabs>

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

```python title="Python"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/tools/catalogue",
    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/tools/catalogue", {
  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 |
| --- | --- | --- |
| `cutting_materials` | string[] | Accepted cutting-material classes. |
| `materials` | ToolCatalogueMaterial[] | Canonical material bins cutting data can target. |
| `parameters` | ToolCatalogueParameter[] | Cutting parameters with units and accepted windows. |
| `process_parameters` | object | Per process family: which parameters its pricing bins use. |
| `spec_schemas` | object | Per tool kind: the JSON schema of its kind-specific spec — required fields, bounds and the wear basis. The spec form renders from these; no per-kind client code. |
| `tool_kind_operations` | object | Per tool kind: the operations a cutting-data row on it may be scoped to with `operation`. A row scoped to any other operation is refused (422) — no handler would read it. |
| `tool_kind_process` | object | Per tool kind: the process family whose pricing bins hold that tool's own cutting data, and which its data can therefore be applied to. Kinds absent from this map are reference-only — pricing derives their values from another tool's bin (a tap from the milling reference, a reamer from the drilling one) or has no cutting-parameter bin at all (grinding). |
| `tool_kinds` | string[] | Accepted tool kinds (VDI 2852). |

**Example response**

```json
{
  "cutting_materials": [
    "string"
  ],
  "materials": [
    {
      "axis": "bin",
      "code": "string",
      "iso_group": "string",
      "label": "string"
    }
  ],
  "parameters": [
    {
      "key": "string",
      "max": 0,
      "min": 0,
      "unit": "string"
    }
  ],
  "process_parameters": {},
  "spec_schemas": {},
  "tool_kind_operations": {},
  "tool_kind_process": {},
  "tool_kinds": [
    "string"
  ]
}
```

## List Catalogue Items

`GET /api/v1/tools/catalogue-items`

The tool catalogue — platform articles plus your own type
definitions (0141 split; the machine-library sibling).

Every article stays visible after adoption: ``adopted_tool_id`` names
your crib instance so the UI can show "in your crib" and a
diff-vs-article, instead of silently hiding adopted presets.
``suppressed`` marks articles you chose to hide (a per-org preference,
never a delete — platform rows are read-only at the database).

**Request**

<CodeTabs>

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

```python title="Python"
import requests

resp = requests.get(
    "https://api.arcnm.io/api/v1/tools/catalogue-items",
    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/tools/catalogue-items", {
  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. |


## Adopt Catalogue Item

`POST /api/v1/tools/catalogue-items/{item_id}/adopt`

Adopt an article into your crib — the machine-library instantiate
move: the instance is yours to rename, re-spec and price; the article
stays untouched and ``catalogue_item_id`` records the lineage.

Adopting an article you already have returns the tool you have — never
a second copy, and never renamed — and still attaches it to the
environments you name.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `item_id` | path | string | yes | Identifier of the item. |

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `attach_to_environment_ids` | string[] | no | Costing environments the adopted tool joins. Omit to let a newly created instance join every current environment; [] to attach nowhere. An environment you do not own is not found. If you already own this article, the environments you name here are joined and every other one is left as it is — adopting again never puts a tool back into an environment you took it out of. |
| `name` | string | no | Name for your instance, used only when this call creates it; defaults to the article's. If you already own this article, adopting returns the tool you have and leaves its name alone — a name you chose is never overwritten. Rename it with PATCH /tools/{tool_id}. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/tools/catalogue-items/{item_id}/adopt \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "attach_to_environment_ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ],
    "name": "string"
  }'
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/tools/catalogue-items/{item_id}/adopt",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "attach_to_environment_ids": [
            "3fa85f64-5717-4562-b3fc-2c963f66afa6"
        ],
        "name": "string"
    },
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/tools/catalogue-items/{item_id}/adopt", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "attach_to_environment_ids": [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    ],
    "name": "string"
  }),
})
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. |
| `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 |
| --- | --- | --- |
| `baseline` | ToolBaselineMembership |  |
| `catalogue_item_id` | string |  |
| `coating` | string | Coating designation (e.g. TiAlN), if any. |
| `created_at` | string |  |
| `cutting_data_count` | integer | Number of cutting-data rows this tool prices with: the values stated on the tool itself plus its catalogue article's data sheet. 0 means the tool carries none of its own and pricing resolves from the platform's cutting-data tables. |
| `cutting_material` | string | Cutting-material class (HSS, carbide, coated carbide, ...). |
| `diameter_mm` | number | Cutting diameter, in millimetres (null for turning inserts). |
| `differs_from_article` | boolean |  |
| `hidden` | boolean |  |
| `id` | string |  |
| `item_no` | string | Vendor item / order number. |
| `kind` | `end_mill` \| `face_mill` \| `drill` \| `spot_drill` \| `tap` \| `reamer` \| `boring_bar` \| `thread_mill` \| `insert_holder_turn` \| `grooving_tool` \| `grinding_wheel` \| `chamfer_mill` \| `saw_blade` \| `edm_wire` \| `press_brake_punch` \| `press_brake_die` \| `tube_bend_die` \| `plasma_consumable_set` \| `laser_nozzle` \| `waterjet_nozzle` \| `punch_die_set` \| `weld_consumable` \| `broach` | Tool kind (VDI 2852 class), e.g. end_mill or drill. |
| `name` | string | Human-readable name of the tool. |
| `notes` | string | Free-form shop notes. |
| `parameters` | CuttingToolParameterPublic[] |  |
| `price_eur` | number | Replacement price, in EUR. |
| `price_is_inherited` | boolean |  |
| `priced_into_quotes` | boolean | Whether a price and a life stated on this tool can reach a quote. False for kinds no pricing path consumes yet (welding and EDM consumables, brake punches, tube-bend dies): the tool is still storable, curatable and attachable to an environment, but its economics change no cost today. Laser and waterjet nozzles, plasma sets and punch sets are priced per pierce, minute or stroke. |
| `spec` | object | Kind-specific properties — e.g. a press-brake die's V opening and load rating, a saw blade's kerf and pitch, a plasma consumable set's amperage. Validated per kind; empty for rotating cutters, whose geometry uses the fields above. |
| `teeth` | integer | Number of cutting edges / flutes / blade teeth. |
| `tool_life_min` | number | Expected tool life in cutting minutes (per edge set). |
| `updated_at` | string |  |
| `valid_from` | string |  |
| `valid_to` | string |  |
| `vendor` | string | Tool manufacturer / vendor. |

**Example response**

```json
{
  "baseline": {
    "fits_machine_names": [
      "string"
    ],
    "fits_machines": true,
    "is_enabled": true,
    "membership_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  },
  "catalogue_item_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "coating": "string",
  "created_at": "2026-06-01T12:00:00Z",
  "cutting_data_count": 0,
  "cutting_material": "carbide",
  "diameter_mm": 0,
  "differs_from_article": false,
  "hidden": false,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "item_no": "string",
  "kind": "end_mill",
  "name": "string",
  "notes": "string",
  "parameters": [],
  "price_eur": 0,
  "price_is_inherited": false,
  "priced_into_quotes": true,
  "spec": {},
  "teeth": 0,
  "tool_life_min": 0,
  "updated_at": "2026-06-01T12:00:00Z",
  "valid_from": "2026-06-01",
  "valid_to": "2026-06-01",
  "vendor": "string"
}
```

## Unsuppress Catalogue Item

`DELETE /api/v1/tools/catalogue-items/{item_id}/suppression`

Show a platform tool-catalogue article again that you had hidden, so it is
offered for selection once more.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `item_id` | path | string | yes | Identifier of the item. |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X DELETE https://api.arcnm.io/api/v1/tools/catalogue-items/{item_id}/suppression \
  -H "X-API-Key: $ARCNM_API_KEY"
```

```python title="Python"
import requests

resp = requests.delete(
    "https://api.arcnm.io/api/v1/tools/catalogue-items/{item_id}/suppression",
    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/tools/catalogue-items/{item_id}/suppression", {
  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 result of the operation. |

**Example response**

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

## Suppress Catalogue Item

`POST /api/v1/tools/catalogue-items/{item_id}/suppression`

Hide a platform article from your catalogue view — a per-org
preference, not a delete (M5).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `item_id` | path | string | yes | Identifier of the item. |

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `reason` | string | no |  |

**Request**

<CodeTabs>

```bash title="cURL"
curl -X POST https://api.arcnm.io/api/v1/tools/catalogue-items/{item_id}/suppression \
  -H "X-API-Key: $ARCNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "string"
  }'
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.arcnm.io/api/v1/tools/catalogue-items/{item_id}/suppression",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "reason": "string"
    },
)
resp.raise_for_status()
print(resp.json())
```

```typescript title="TypeScript"
const resp = await fetch("https://api.arcnm.io/api/v1/tools/catalogue-items/{item_id}/suppression", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ARCNM_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "reason": "string"
  }),
})
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. |
| `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 |
| --- | --- | --- |
| `message` | string | Human-readable result of the operation. |

**Example response**

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