# Cliento API reference: Colour bar

Base URL: `https://api.cliento.com`

The Colour bar APIs are for hair salons. They let an integration manage a salon's stock of hair
colour and keep it correct as the colour is used.

Hair colour is not sold to the customer. It is used a little at a time: a tube is opened, and each
mix takes some grams from it. The colour bar keeps track of both the packs in stock and how much
is left in the open one.

There are three APIs:
- **Colour bar ranges.** A range is a supplier's series, such as one brand's permanent colours.
- **Colour bar items.** An item is one shade in a range. Each item has its own stock.
- **Colour bar mixes.** A mix is what a staff member mixed for one customer. Registering a mix
  takes the grams used from each item's stock.

Deliveries are recorded through the Stock movements API.

## Colour bar ranges

**Industry: hair and beauty.** The Colour bar ranges API lets an integration read a salon's ranges and create, change and delete
them.

A range is a supplier's series, such as one brand's permanent colours. It has an id of the form
`cbr_...` and belongs to a location. The items of a range share three settings, which are set on
the range: `kind`, `packSize` and `listPrice`.

Ranges are under `/v1/locations/{locationId}/colour-bar-ranges`. A range is also read by its id,
without its location, at `/v1/colour-bar-ranges/{colourBarRangeId}`. Reading needs the scope
`products:read`. Writing needs `stock:write`.

- `kind` is a lower-case word such as `colour`, `bleach` or `developer`. The list is open: send
  the word that fits.
- A range without a `packSize` is one whose items are used a whole pack at a time, such as a
  treatment. Its items have no open pack, except an item with a `packSize` of its own.
- `listPrice` is the supplier's list price of one pack, VAT excluded. It is what the location
  values its use of the range's items at, and is not what the location paid.
- An item can have a `packSize` and a `listPrice` of its own. Changing the range's `packSize` or
  `listPrice` changes what applies to every item without a value of its own.
- A range that still has items cannot be deleted. Delete its items first.

### List colour bar ranges

`GET /v1/locations/{locationId}/colour-bar-ranges`

Scope: `products:read`

Stability: preview

The location's colour bar ranges by name, one page at a time.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `limit` | query | integer (int32) | No | Page size, 1 to 200. Default 50. |
| `after` | query | string | No | The `nextCursor` of the previous page. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | ColourBarRangeList |
| default | Any failure. See the Errors section. | Error |

### Create a colour bar range

`POST /v1/locations/{locationId}/colour-bar-ranges`

Scope: `stock:write`

Stability: preview

Adds a range to the location's colour bar. It holds no items until they are created. Send an `Idempotency-Key` so that a repeat of the call does not add the range twice.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Request body**: ColourBarRangeCreate

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 201 | Created | ColourBarRange |
| default | Any failure. See the Errors section. | Error |

### Get a colour bar range

`GET /v1/locations/{locationId}/colour-bar-ranges/{colourBarRangeId}`

Scope: `products:read`

Stability: preview

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `colourBarRangeId` | path | string | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | ColourBarRange |
| default | Any failure. See the Errors section. | Error |

### Get a colour bar range by id

`GET /v1/colour-bar-ranges/{colourBarRangeId}`

Scope: `products:read`

Stability: preview

The same colour bar range as under its location, found from its id. For a caller that holds an id and not the location it belongs to. The answer carries `locationId`.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `colourBarRangeId` | path | string | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | ColourBarRange |
| default | Any failure. See the Errors section. | Error |

### Update a colour bar range

`PATCH /v1/locations/{locationId}/colour-bar-ranges/{colourBarRangeId}`

Scope: `stock:write`

Stability: preview

Changes only the fields in the request. Either all of them are changed or none. See `ColourBarRangeUpdate` for how to clear a field.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `colourBarRangeId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Request body**: ColourBarRangeUpdate

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | ColourBarRange |
| default | Any failure. See the Errors section. | Error |

### Delete a colour bar range

`DELETE /v1/locations/{locationId}/colour-bar-ranges/{colourBarRangeId}`

Scope: `stock:write`

Stability: preview

Deleting a range that still has items is refused with `not_allowed`. Delete its items first.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `colourBarRangeId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 204 | No Content |  |
| default | Any failure. See the Errors section. | Error |

## Colour bar items

**Industry: hair and beauty.** The Colour bar items API lets an integration read a salon's colour bar items and create, change
and delete them.

An item is one shade in a range. It can also be one strength of a developer or one tub of bleach.
It has an id of the form `cbi_...` and belongs to its range's location.

Each item has its own stock. The stock is counted in packs. The item also keeps how many grams
have been taken from the open pack.

An item is not a product. A product is sold at a point of sale and is read through the Products
API. An item is used in the salon and is never sold.

Items are under `/v1/locations/{locationId}/colour-bar-items`. An item is also read by its id,
without its location, at `/v1/colour-bar-items/{colourBarItemId}`. Reading needs the scope
`products:read`. Writing needs `stock:write`.

- **Stock is counted in packs, and the open pack counts as one.** `packs` is `4` for three
  unopened tubes and one that is open. `openUsed` is the grams that have been taken from the open
  pack, and `0` when none is open.
- `unopenedPacks`, `openLeft` and `gramsLeft` are worked out from `packs`, `openUsed` and the pack
  size, and are read-only.
- **`packs` and `openUsed` are read-only here.** A delivery adds packs and a withdrawal takes
  them: record both through the Stock movements API, on a line with the item's `colourBarItemId`
  and the quantity in whole packs. `openUsed` changes when a colour bar mix is registered, in
  Cliento or through the Colour bar mixes API.
- An item follows its range's `packSize` and `listPrice` unless it has its own. The item's own
  values are `packSize` and `listPrice`; `packSizeInUse` and `listPriceInUse` are what applies.
- `eans` holds the item's barcodes. An item made from an article in a supplier's catalogue also
  has that article's barcodes, which `catalogueEans` lists. Only the item's own barcodes are added
  and removed here.
- To find the item for a scanned barcode or a row of a delivery note, list with `ean` or
  `articleNumber`. Look up the barcode first: a supplier can change its article numbers, and a
  barcode stays with the pack.
- A range holds a shade once: a second item with the same `shade` is refused with `duplicate`.
- An item without a `name` of its own is called by its `shade`.

### List colour bar items

`GET /v1/locations/{locationId}/colour-bar-items`

Scope: `products:read`

Stability: preview

The location's colour bar items, range by range and shade by shade, one page at a time. With `ean` or `articleNumber` it is a lookup: the item a scanned barcode or a delivery note row belongs to, at most 20 items. Send one of `rangeId`, `ean` and `articleNumber`, or none.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `limit` | query | integer (int32) | No | Page size, 1 to 200. Default 50. |
| `after` | query | string | No | The `nextCursor` of the previous page. |
| `rangeId` | query | string | No | Only the items of this colour bar range. |
| `ean` | query | string | No | Only items with this barcode: their own, or that of the supplier's article they were made from. |
| `articleNumber` | query | string | No | Only items with exactly this article number. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | ColourBarItemList |
| default | Any failure. See the Errors section. | Error |

### Create a colour bar item

`POST /v1/locations/{locationId}/colour-bar-items`

Scope: `stock:write`

Stability: preview

Adds an item to a colour bar range. It starts with no stock. Send an `Idempotency-Key` so that a repeat of the call does not add the item twice. A shade the range already holds, or a barcode that another item or a product of the location has, is refused with `duplicate`.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Request body**: ColourBarItemCreate

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 201 | Created | ColourBarItem |
| default | Any failure. See the Errors section. | Error |

### Get a colour bar item

`GET /v1/locations/{locationId}/colour-bar-items/{colourBarItemId}`

Scope: `products:read`

Stability: preview

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `colourBarItemId` | path | string | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | ColourBarItem |
| default | Any failure. See the Errors section. | Error |

### Get a colour bar item by id

`GET /v1/colour-bar-items/{colourBarItemId}`

Scope: `products:read`

Stability: preview

The same colour bar item as under its location, found from its id. For a caller that holds an id and not the location it belongs to. The answer carries `locationId`.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `colourBarItemId` | path | string | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | ColourBarItem |
| default | Any failure. See the Errors section. | Error |

### Update a colour bar item

`PATCH /v1/locations/{locationId}/colour-bar-items/{colourBarItemId}`

Scope: `stock:write`

Stability: preview

Changes only the fields in the request. Either all of them are changed or none. See `ColourBarItemUpdate` for how to clear a field. An item stays in its range, and its stock is not changed here.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `colourBarItemId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Request body**: ColourBarItemUpdate

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | ColourBarItem |
| default | Any failure. See the Errors section. | Error |

### Delete a colour bar item

`DELETE /v1/locations/{locationId}/colour-bar-items/{colourBarItemId}`

Scope: `stock:write`

Stability: preview

Removes the item from the colour bar with whatever stock it holds, and frees its own barcodes.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `colourBarItemId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 204 | No Content |  |
| default | Any failure. See the Errors section. | Error |

### Add a barcode to a colour bar item

`POST /v1/locations/{locationId}/colour-bar-items/{colourBarItemId}/eans`

Scope: `stock:write`

Stability: preview

Gives the item a barcode of its own, as when a delivery row was matched by its article number and its barcode is new. A barcode that another item or a product of the location has is refused with `duplicate`.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `colourBarItemId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Request body**: ColourBarBarcode

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | ColourBarItem |
| default | Any failure. See the Errors section. | Error |

### Remove a barcode from a colour bar item

`DELETE /v1/locations/{locationId}/colour-bar-items/{colourBarItemId}/eans/{ean}`

Scope: `stock:write`

Stability: preview

Removes one of the item's own barcodes. Removing one of its `catalogueEans` is refused with `not_allowed`.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `colourBarItemId` | path | string | Yes |  |
| `ean` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | ColourBarItem |
| default | Any failure. See the Errors section. | Error |

## Colour bar mixes

**Industry: hair and beauty.** The Colour bar mixes API lets an integration register what staff mix, read it, undo it and total
it per staff member. It is for an integration that weighs or doses colour itself and wants
Cliento's stock and totals to follow.

A mix is what a staff member mixed for one customer. It has an id of the form `cbm_...` and
belongs to a location. A mix has one or more bowls. A bowl has components. A component names one
colour bar item and how many grams were taken from it.

Registering a mix takes the grams used from each item's stock. The grams are added to the item's
`openUsed`. When a pack is finished, the item's `packs` goes down by one. That change is recorded
as a stock movement of the type `withdrawal`.

Mixes are under `/v1/locations/{locationId}/colour-bar-mixes`. A mix is also read by its id,
without its location, at `/v1/colour-bar-mixes/{colourBarMixId}`. Reading needs the scope
`products:read`. Registering needs `stock:write`. Undoing needs `stock:correct`.

- **Amounts are whole grams, as weighed.** For an item with no pack size the amount is whole
  packs.
- **A pack leaves the stock when it is finished.** The caller says so with `openedNewPack: true`
  on the component for which a new pack was opened. If the finished pack ran out midway,
  `fromFinishedPack` says how many grams of the amount still came out of it. What was never taken
  from the finished pack is kept on the component as `residue`.
- **A pack also counts as finished without the caller saying so**, when more has been taken from
  it than it holds, unless the location has switched that off in Cliento. That setting is not
  readable through the API: read `packsFinished` on each component of the answer to see what the
  mix took from stock.
- **A mix is never refused because an item is out of stock.** What was mixed has been mixed; the
  item's `packs` stays at `0`.
- **`listValue` is what the mix took at the supplier's list price**, VAT excluded, worked out
  when the mix is registered and kept. A later change of a list price does not change it. A
  component's `listValue` has up to four decimals; the mix's is their sum rounded to two.
- **Give the mix your own `externalReference`.** Sending the same reference again changes
  nothing and answers `200` with the mix made the first time, also when that mix has been undone.
  A reference already used for a mix by another staff member is refused with `not_allowed`. To
  find the mix for a reference, list with `externalReference`.
- **`staffId` names the staff member who mixed.** Read it from the Staff API.
- **`personId` also saves the mix on that person as a recipe**, in the same call, and the mix
  then carries its `recipeId`. It needs the scope `customers:write` as well.
- **A mix is not changed.** A mistake is undone: every pack the mix took is put back, the grams
  it added to each item's `openUsed` are taken off again, and its recipe is removed. The mix is
  then registered again under a new `externalReference`. An undone mix stays readable, with
  `undoneAt`.
- **The summary** totals the mixes of a period per staff member at `listValue`, leaving out the
  ones that were undone. A location uses these totals to charge the colour used to a staff member
  who rents a chair.
- A location that keeps medical records, such as a clinic, has no colour bar mixes: every call
  is refused with `not_allowed`.

### List colour bar mixes

`GET /v1/locations/{locationId}/colour-bar-mixes`

Scope: `products:read`

Stability: preview

The location's mixes, newest first, one page at a time, including those that were undone. With `staffId`, one staff member's. With `externalReference`, the mix registered under that reference, if there is one; it is sent without `staffId` and `after`.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `limit` | query | integer (int32) | No | Page size, 1 to 200. Default 50. |
| `after` | query | string | No | The `nextCursor` of the previous page. |
| `staffId` | query | string | No | Only the mixes of this staff member. |
| `externalReference` | query | string | No | Only the mix with this external reference. Not together with `staffId` or `after`. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | ColourBarMixList |
| default | Any failure. See the Errors section. | Error |

### Register a colour bar mix

`POST /v1/locations/{locationId}/colour-bar-mixes`

Scope: `stock:write`

Stability: preview

Registers what was mixed and changes the colour bar items it names. It is never refused because an item is out of stock: what was mixed has been mixed. Send an `externalReference`, your own id for the mix, to make the call safe to repeat: when the location already has a mix under it, that mix is returned unchanged with `200`, and a reference used for another staff member's mix is refused with `not_allowed`. With `personId` the mix is also saved on that person as a recipe, which needs the scope `customers:write` as well. Not available at a location that keeps medical records.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Request body**: ColourBarMixCreate

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | A mix with this `externalReference` already existed. | ColourBarMix |
| 201 | The mix was registered. | ColourBarMix |
| default | Any failure. See the Errors section. | Error |

### Get a colour bar mix

`GET /v1/locations/{locationId}/colour-bar-mixes/{colourBarMixId}`

Scope: `products:read`

Stability: preview

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `colourBarMixId` | path | string | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | ColourBarMix |
| default | Any failure. See the Errors section. | Error |

### Get a colour bar mix by id

`GET /v1/colour-bar-mixes/{colourBarMixId}`

Scope: `products:read`

Stability: preview

The same colour bar mix as under its location, found from its id. For a caller that holds an id and not the location it belongs to, such as one read from a stock movement. The answer carries `locationId`.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `colourBarMixId` | path | string | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | ColourBarMix |
| default | Any failure. See the Errors section. | Error |

### Total colour bar mixes per staff member

`GET /v1/locations/{locationId}/colour-bar-mixes/summary`

Scope: `products:read`

Stability: preview

What was mixed from `from` to `to`, both days included, in the location's time zone, per staff member, at list price. Mixes that were undone are left out. The period is at most 366 days. A location uses these totals to charge the colour used to a staff member who rents a chair.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `from` | query | string (date) | Yes | The first day, as `YYYY-MM-DD`. |
| `to` | query | string (date) | Yes | The last day, as `YYYY-MM-DD`. |
| `staffId` | query | string | No | Only this staff member. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | ColourBarMixSummary |
| default | Any failure. See the Errors section. | Error |

### Undo a colour bar mix

`POST /v1/locations/{locationId}/colour-bar-mixes/{colourBarMixId}/undo`

Scope: `stock:correct`

Stability: preview

Undoes the whole mix, once: every pack it took is put back, the grams it added to each item's `openUsed` are taken off again, and the recipe it was saved as is removed. The mix stays, with `undoneAt`. A mix that is already undone is refused with `not_allowed`.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `colourBarMixId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Request body**: ColourBarMixUndo

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | ColourBarMix |
| default | Any failure. See the Errors section. | Error |

## Schemas

### ColourBarBarcode

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `ean` | string | Yes | The barcode, at most 50 characters. Example: `4064666230412`. |

### ColourBarItem

Represents one thing a location mixes from at its colour bar: one shade of a colour, one strength of a developer, one tub of bleach. Belongs to a colour bar range and to the range's location. Holds its own balance in the location's stock, counted in packs, and how much has been taken from the pack that is open. It is not a product: it is never sold at a point of sale.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `articleNumber` | string | No | The supplier's article number, as read from a delivery note. Example: `99350118254`. |
| `catalogueEans` | array of string | Yes | Read-only. Those of `eans` that the item has from the article in a supplier's catalogue it was made from. They are not the item's own and cannot be removed from it. Can be empty. Example: `[]`. |
| `createdAt` | string (date-time) | Yes | Example: `2026-10-12T08:15:30Z`. |
| `eans` | array of string | Yes | The item's barcodes: its own, and those in `catalogueEans`. Can be empty. Example: `["4064666230412"]`. |
| `gramsLeft` | integer (int64) | No | Read-only. Grams left in all the item's packs together. Left out when the item has no pack size. Example: `203`. |
| `id` | string | Yes | Colour bar item id. Example: `cbi_3Vn8Kq5TzL1xWc7PdM2RbJ`. |
| `listPrice` | string | No | The list price of one pack, VAT excluded, where the item differs from its range. Left out when the item follows the range's `listPrice`. Example: `137.00`. |
| `listPriceInUse` | string | No | Read-only. The list price that applies: the item's own, otherwise its range's. Example: `137.00`. |
| `locationId` | string | Yes | Example: `loc_4fK9xQ2mLr8TzB1vNc7HdW`. |
| `name` | string | Yes | What the item is called. An item without a name of its own is called by its `shade`, and follows it when the shade is changed. Example: `7/10`. |
| `openLeft` | integer (int32) | No | Read-only. Grams left in the open pack. Left out when no pack is open or the item has no pack size. Example: `23`. |
| `openUsed` | integer (int32) | Yes | Read-only. Grams taken from the open pack. `0` when no pack is open. Example: `37`. |
| `packSize` | integer (int32) | No | Grams in one pack, where the item differs from its range. Left out when the item follows the range's `packSize`. Example: `60`. |
| `packSizeInUse` | integer (int32) | No | Read-only. The pack size that applies: the item's own, otherwise its range's. Left out for an item used a whole pack at a time. Example: `60`. |
| `packs` | integer (int32) | Yes | Read-only. The packs in stock, the open one included. Never negative. Example: `4`. |
| `rangeId` | string | Yes | The range the item belongs to. Set when the item is created. Example: `cbr_7Hc2Lm9QxT4vBn6KdR1WsP`. |
| `shade` | string | No | The shade of a colour, as the supplier writes it. Example: `7/10`. |
| `strengthPercent` | string | No | The strength of a developer in per cent, as a decimal string. Example: `6`. |
| `unopenedPacks` | integer (int32) | Yes | Read-only. The packs that are not opened. Example: `3`. |
| `updatedAt` | string (date-time) | Yes | When the item last changed, which includes its stock and its own barcodes. Example: `2026-10-12T08:15:30Z`. |

### ColourBarItemCreate

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `articleNumber` | string | No | Example: `99350118254`. |
| `eans` | array of string | No | The item's own barcodes. A barcode that another item or a product of the location has is refused. Example: `["4064666230412"]`. |
| `listPrice` | string | No | The list price of one pack, VAT excluded, when the item differs from its range. Example: `149.00`. |
| `name` | string | No | What the item is called. Required when there is no `shade`. Example: `Blondor Multi Blonde`. |
| `packSize` | integer (int32) | No | Grams in one pack, when the item differs from its range. Example: `60`. |
| `rangeId` | string | Yes | The range the item belongs to. Example: `cbr_7Hc2Lm9QxT4vBn6KdR1WsP`. |
| `shade` | string | No | The shade of a colour, at most 50 characters. A range holds a shade once. Example: `7/10`. |
| `strengthPercent` | string | No | The strength of a developer in per cent, greater than 0, at most 100. Example: `6`. |

### ColourBarItemList

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of ColourBarItem | Yes |  |
| `nextCursor` | string | No | Pass as `after` to fetch the next page. Absent on the last page. Example: `cbi_3Vn8Kq5TzL1xWc7PdM2RbJ`. |

### ColourBarItemUpdate

A JSON merge patch: send only what changes. A field you leave out keeps its value; a field set to `null` is cleared, but an item must keep a `name` or a `shade`. A `name` equal to the `shade` is not a name of its own: the item is called by its shade and follows it. Clearing `packSize` or `listPrice` makes the item follow its range again.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `articleNumber` | string | No | Example: `99350118254`. |
| `listPrice` | string | No | Example: `149.00`. |
| `name` | string | No | Example: `Blondor Multi Blonde`. |
| `packSize` | integer (int32) | No | Example: `60`. |
| `shade` | string | No | Example: `7/10`. |
| `strengthPercent` | string | No | Example: `6`. |

### ColourBarMix

Represents what one staff member mixed at a location's colour bar at one time: one or more bowls, each with the colour bar items it took from and how much of each. Belongs to a location. Registering it changes the items it names: what was taken is added to an item's `openUsed`, and a pack is taken from stock when one is finished. A mix is not changed afterwards; it can be undone.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `bowls` | array of ColourBarMixBowl | Yes |  |
| `createdAt` | string (date-time) | Yes | Example: `2026-10-12T08:15:30Z`. |
| `externalReference` | string | No | Your own id for the mix. Example: `mix-2026-10-12-0042`. |
| `id` | string | Yes | Colour bar mix id. Example: `cbm_8Wq3Nd6HtY1mKx4LcP9RvB`. |
| `listValue` | string | Yes | What the mix took, at the list prices then, VAT excluded: its components' `listValue` together, rounded to two decimals, as a decimal string in the location's currency. Example: `91.33`. |
| `locationId` | string | Yes | Example: `loc_4fK9xQ2mLr8TzB1vNc7HdW`. |
| `note` | string | No | Example: `35 min`. |
| `recipeId` | string | No | The recipe the mix was saved as on a person, when it was. Example: `rcp_6Jn2Xw8QdT5bVh3ZmK7LsF`. |
| `registeredBy` | string | No | The name of who mixed: the staff member, or the API key when none was named. Example: `Anna`. |
| `staffId` | string | No | The staff member who mixed. Left out when none was named. Example: `stf_2Hs7Bq4MnX9cLk1VtR6PzD`. |
| `title` | string | No | Example: `Roots and lengths`. |
| `undoneAt` | string (date-time) | No | When the mix was undone. Left out for a mix that stands. Example: `2026-10-12T09:02:11Z`. |
| `undoneBy` | string | No | The name of who undid the mix. Example: `Anna`. |
| `updatedAt` | string (date-time) | Yes | When the mix last changed: when it was undone, otherwise `createdAt`. Example: `2026-10-12T08:15:30Z`. |

### ColourBarMixBowl

One bowl of a mix.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `components` | array of ColourBarMixComponent | Yes |  |
| `ratio` | string | No | Colour to developer, as it was sent. Kept as it is. Example: `1:1`. |
| `title` | string | No | Example: `Roots`. |

### ColourBarMixBowlCreate

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `components` | array of ColourBarMixComponentCreate | Yes |  |
| `ratio` | string | No | Colour to developer, such as `1:1` or `1:1.5`. Stored as it is. Example: `1:1`. |
| `title` | string | No | Example: `Roots`. |

### ColourBarMixComponent

What a mix took from one colour bar item, and how that left the item.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amount` | integer (int32) | Yes | How much was taken: grams, or whole packs of an item that has no pack size. Example: `30`. |
| `colourBarItemId` | string | Yes | Example: `cbi_3Vn8Kq5TzL1xWc7PdM2RbJ`. |
| `fromFinishedPack` | integer (int32) | No | The grams of `amount` that came out of the finished pack before it ran out, when a new pack was opened midway. Example: `12`. |
| `listValue` | string | No | What `amount` was worth at the list price then, VAT excluded, as a decimal string with up to four decimals. Left out when the item had no list price. Example: `68.5000`. |
| `name` | string | Yes | What the item was called when the mix was registered. Example: `7/10`. |
| `openUsed` | integer (int32) | Yes | The item's `openUsed` after this component. Example: `30`. |
| `openedNewPack` | boolean | Yes | Whether a new pack was opened for this component. Example: `false`. |
| `packs` | integer (int32) | Yes | The item's `packs` after this component. Example: `4`. |
| `packsFinished` | integer (int32) | Yes | The packs this component finished, which is what it took from stock. Example: `0`. |
| `residue` | integer (int32) | No | Set only when `openedNewPack` is `true`: the pack size less the grams that had been taken from the finished pack. Example: `3`. |
| `shade` | string | No | The item's shade when the mix was registered. Example: `7/10`. |

### ColourBarMixComponentCreate

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amount` | integer (int32) | Yes | How much was taken, as a whole number greater than 0: grams as weighed, or whole packs of an item that has no pack size. Example: `30`. |
| `colourBarItemId` | string | Yes | Example: `cbi_3Vn8Kq5TzL1xWc7PdM2RbJ`. |
| `fromFinishedPack` | integer (int32) | No | With `openedNewPack`: the grams of `amount` that still came out of the finished pack before it ran out. Leave out when the whole amount came from the new pack. Refused with `validation_failed` for an item that has no pack size. Example: `12`. |
| `openedNewPack` | boolean | No | `true` when a new pack was opened for this component: the pack that was open before is then finished and leaves the stock. Example: `false`. |

### ColourBarMixCreate

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `bowls` | array of ColourBarMixBowlCreate | Yes |  |
| `externalReference` | string | No | Your own id for the mix, at most 100 characters. A mix the location already has under it is returned unchanged. Example: `mix-2026-10-12-0042`. |
| `note` | string | No | Example: `35 min`. |
| `personId` | string | No | A person to save the mix on as a recipe, in the same call. Needs the scope `customers:write` as well. Example: `per_9Tk4Yc1WvN7pQe2XhB5MdG`. |
| `staffId` | string | No | The staff member who mixed. Read it from the Staff API. Example: `stf_2Hs7Bq4MnX9cLk1VtR6PzD`. |
| `title` | string | No | Example: `Roots and lengths`. |

### ColourBarMixList

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of ColourBarMix | Yes |  |
| `nextCursor` | string | No | Pass as `after` to fetch the next page. Absent on the last page. Example: `cbm_8Wq3Nd6HtY1mKx4LcP9RvB`. |

### ColourBarMixStaffTotal

What one staff member mixed over the period, mixes that were undone left out.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `listValue` | string | Yes | Their `listValue` together, as a decimal string. Example: `3480.25`. |
| `mixes` | integer (int32) | Yes | How many mixes. Example: `42`. |
| `name` | string | No | Example: `Anna`. |
| `staffId` | string | No | The staff member. Left out for mixes registered with no staff member. Example: `stf_2Hs7Bq4MnX9cLk1VtR6PzD`. |

### ColourBarMixSummary

What was mixed at a location from one day to another, per staff member, at list price.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string (date) | Yes | Example: `2026-10-01`. |
| `listValue` | string | Yes | The `listValue` of all `staff` together, as a decimal string. Example: `12940.75`. |
| `staff` | array of ColourBarMixStaffTotal | Yes | One total per staff member who mixed. |
| `to` | string (date) | Yes | Example: `2026-10-31`. |

### ColourBarMixUndo

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `staffId` | string | No | The staff member who undoes the mix. Read it from the Staff API. Example: `stf_2Hs7Bq4MnX9cLk1VtR6PzD`. |

### ColourBarRange

Represents a supplier's series as a location keeps it at its colour bar, such as one brand's permanent colours. Belongs to a location. Holds what its items have in common: their kind, the size of a pack and its list price. Colour bar items belong to it.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `brand` | string | No | Example: `Wella`. |
| `createdAt` | string (date-time) | Yes | Example: `2026-10-12T08:15:30Z`. |
| `id` | string | Yes | Colour bar range id. Example: `cbr_7Hc2Lm9QxT4vBn6KdR1WsP`. |
| `kind` | string | Yes | What the range's items are used as, such as `colour` or `developer`. A lower-case word; the list is open. Example: `colour`. |
| `listPrice` | string | No | The supplier's list price of one pack, VAT excluded, as a decimal string in the location's currency. Example: `137.00`. |
| `locationId` | string | Yes | Example: `loc_4fK9xQ2mLr8TzB1vNc7HdW`. |
| `name` | string | Yes | What the range is called at the location. Example: `Koleston Perfect ME+`. |
| `packSize` | integer (int32) | No | Grams in one pack of the range's items. Left out for a range whose items are used a whole pack at a time. Example: `60`. |
| `series` | string | No | The supplier's name of the series. Example: `KP ME+`. |
| `updatedAt` | string (date-time) | Yes | When the range itself was last changed. Example: `2026-10-12T08:15:30Z`. |

### ColourBarRangeCreate

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `brand` | string | No | Example: `Wella`. |
| `kind` | string | Yes | What the range's items are used as, such as `colour` or `developer`: a lower-case word of at most 30 characters. Example: `colour`. |
| `listPrice` | string | No | The supplier's list price of one pack, VAT excluded. Example: `137.00`. |
| `name` | string | Yes | What the range is called at the location, at most 255 characters. Example: `Koleston Perfect ME+`. |
| `packSize` | integer (int32) | No | Grams in one pack. Leave out for a range whose items are used a whole pack at a time. Example: `60`. |
| `series` | string | No | The supplier's name of the series. Example: `KP ME+`. |

### ColourBarRangeList

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of ColourBarRange | Yes |  |
| `nextCursor` | string | No | Pass as `after` to fetch the next page. Absent on the last page. Example: `cbr_7Hc2Lm9QxT4vBn6KdR1WsP`. |

### ColourBarRangeUpdate

A JSON merge patch: send only what changes. A field you leave out keeps its value; a field set to `null` is cleared. `name` and `kind` cannot be cleared.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `brand` | string | No | Example: `Wella`. |
| `kind` | string | No | Example: `colour`. |
| `listPrice` | string | No | Example: `137.00`. |
| `name` | string | No | Example: `Koleston Perfect ME+`. |
| `packSize` | integer (int32) | No | Grams in one pack. Changing it changes the pack size of every item of the range that has none of its own. Example: `60`. |
| `series` | string | No | Example: `KP ME+`. |

### Error

The body of every non-2xx response.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `code` | string | Yes | Stable, machine-readable error code. Values today: `invalid_request`, `validation_failed`, `entity_not_found`, `duplicate`, `merchant_required`, `code_already_used`, `gift_card_already_used`, `pin_not_recognised`, `insufficient_stock`, `not_allowed`, `request_in_progress`, `idempotency_key_reused`, `unauthenticated`, `forbidden`, `account_blocked`, `rate_limited`, `route_not_found`, `method_not_allowed`, `internal_error`. |
| `details` | object of any value | No | Extra detail. For `validation_failed`, `fieldErrors` maps each rejected field to the reason. |
| `message` | string | Yes | Human-readable explanation. Not stable; do not parse it. |
