> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.coperniq.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.coperniq.io/_mcp/server.

## Catalog item units, measurements, and categories

### New: Units, measurements, and quantity-calculation fields on catalog items

Catalog items now expose the same unit and measurement-driven quantity calculation available in the Coperniq UI.

`GET`/`POST`/`PATCH /catalog-items` responses and request bodies gain:

| Field                                                  | Type             | Description                                                                      |
| ------------------------------------------------------ | ---------------- | -------------------------------------------------------------------------------- |
| `unit` (response) / `unitId` (request)                 | object / integer | The unit this item is sold or measured in.                                       |
| `measurements` (response) / `measurementIds` (request) | array            | Measurements (or a single sub-measurement) linked to drive this item's quantity. |
| `coverage`                                             | number           | Measurement units covered per item unit (e.g. a bundle covering \~33.33 SF).     |
| `wasteFactorMode` / `wasteFactorValue`                 | string / number  | Waste factor applied when computing quantity. Set together, or not at all.       |

```json
POST /catalog-items
{
  "name": "Architectural Shingle",
  "type": "PRODUCT",
  "category": "ROOF_COVERING",
  "tradeGroup": "ENVELOPE",
  "cost": 45,
  "price": 90,
  "unitId": 1924,
  "measurementIds": [2727],
  "coverage": 33.33,
  "wasteFactorMode": "STATIC",
  "wasteFactorValue": 10
}
```

#### Notes

* `measurementIds` is **replace-all** on `PATCH`, not a merge: omit it to leave existing links unchanged, or pass `[]` to clear all links. There's no way to append a single measurement without resending the full set.
* A linked measurement can be a plain measurement (one or more, summed — all must share a unit or be unitless) or a single sub-calculation ("sub-measurement"), never both. Linking a sub-calculation alongside anything else returns `400`.

### New: List and get catalog units

| Method | Path                               | Description                                         |
| ------ | ---------------------------------- | --------------------------------------------------- |
| `GET`  | `/catalog-units`                   | List the units of measure available to your company |
| `GET`  | `/catalog-units/{catalog_unit_id}` | Get a specific unit by ID                           |

Use these IDs when setting `unitId` on a catalog item. Every company starts with a seeded default set (Each, Square Feet, Linear Feet, etc.); `isCustom` distinguishes those from any additional units a company has added.

### New: List and get measurements

| Method | Path                             | Description                                                      |
| ------ | -------------------------------- | ---------------------------------------------------------------- |
| `GET`  | `/measurements`                  | List measurements and sub-calculations available to your company |
| `GET`  | `/measurements/{measurement_id}` | Get a specific measurement by ID                                 |

Filter with `?type=MEASUREMENT` or `?type=SUB_CALCULATION`. Use these IDs when setting `measurementIds` on a catalog item. A sub-calculation's read-only `formula` shows what it computes from, but formulas can only be authored in the Coperniq UI.

### New: List catalog categories

`GET /catalog-categories` returns the valid `category` codes for catalog items, grouped by `PRODUCT`/`SERVICE`, plus the available `tradeGroup` values — everything needed to populate `category` and `tradeGroup` on `POST`/`PATCH /catalog-items`.

### Changed: `tradeGroup` is derived from `category` when omitted

`POST /catalog-items` previously stored `tradeGroup: "OTHER"` whenever the field was omitted, even for a product category that clearly belongs to another group. It is now derived from the category — `ROOF_COVERING` resolves to `ENVELOPE`, `PV_MODULE` to `ENERGY`, and so on. Services and category `OTHER` still resolve to `OTHER`. Pass `tradeGroup` explicitly to keep full control.

### Changed: `PATCH /catalog-items/{catalog_item_id}` validates the resulting item, not just the patch

A partial update is now merged over the stored item and the result checked before the write, so a patch can no longer leave an item in a state `POST` would have rejected — for example setting `category` to a code outside the item's stored `tradeGroup`, or adding an `sku` to an item that is already a `SERVICE`. These return `400` with the offending `field`. Inconsistencies that already existed on the stored item are left alone, so unrelated edits to older items keep working.

Converting a `PRODUCT` that has an `sku` into a `SERVICE` is still allowed and does not require clearing the `sku` first — services have no `sku`, so it is dropped as part of the conversion. Sending `sku: null` during that conversion is accepted and has the same effect; for an item already stored as a `SERVICE`, a legacy `sku` cannot be cleared or changed by this API and the request returns `400`.