# Cliento API reference: Products and stock

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

The Products and stock APIs are for an integration that keeps track of a location's stock. It
can read the products and their balances, and record deliveries and withdrawals.

A location has one product list and one stock. All the location's merchants share them.

- **Products.** A product is an article the location sells or uses. A product with stock tracking
  has a balance: the number of units the location has.
- **Stock movements.** A stock movement is one change to a balance. The movements are the history
  behind every balance.

A balance changes in several ways. These are the common ones:

- A sale at a point of sale takes out what is sold, whichever merchant sells it.
- A delivery adds to it.
- Staff take out what they use.
- A stocktake counts the stock and corrects the balance to what was counted.

A hair salon's colours, developers and bleaches are not products. They are colour bar items,
described under Colour bar. Their balances are in the same stock, so a stock movement names
either a product or a colour bar item.

The Products API reads the product list and the balances. The Stock movements API reads the
history and records deliveries and withdrawals. The APIs for points of sale, sales and stocktakes
are coming soon.

## Products

The Products API lists a location's products, each with its current stock balance. Use it to show
what a location sells and has in stock, or to look a product up from a scanned barcode or a
delivery note.

A product is an article the location sells or uses. A product with stock tracking has a balance.
The Products API returns only the current balance. What changed it is read from the Stock
movements API.

A product belongs to one location and has an id of the form `prd_...`.
`GET /v1/locations/{locationId}/products` lists the products a page at a time. Reading needs the
scope `products:read`.

- **To look one product up**, add `ean` or `articleNumber` to the list.
- **Quantities are whole numbers**: ten bottles are a `stockQuantity` of 10.
- **`stockQuantity` is there only for a product the location keeps stock of** (`stockTracked`).
- **Purchase prices** (`latestPurchasePrice`, `averageCost`) are returned only to a key that also
  has the scope `stock:cost`.

### List products

`GET /v1/locations/{locationId}/products`

Scope: `products:read`

Stability: preview

The location's products, one page at a time, including those that are not shown on the point-of-sale screen as products that can be sold. To read them all, repeat the call with `after` set to the `nextCursor` of the previous page until a page has none. With `ean` or `articleNumber` it is a lookup: the product a scanned barcode or a delivery note row belongs to.

**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. |
| `ean` | query | string | No | Only products with this barcode. Normally one; Two can match, because a product also has the barcodes of the supplier's article it is linked to; see `eans`. |
| `articleNumber` | query | string | No | Only products with exactly this article number. |

**Responses**

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

### Get a product

`GET /v1/locations/{locationId}/products/{productId}`

Scope: `products:read`

Stability: preview

**Parameters**

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

**Responses**

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

### Get a product by id

`GET /v1/products/{productId}`

Scope: `products:read`

Stability: preview

The same product 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 |
| --- | --- | --- | --- | --- |
| `productId` | path | string | Yes |  |

**Responses**

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

## Stock movements

The Stock movements API lets an integration read every change to a location's stock, and record
deliveries and withdrawals of its own. Use it when you need to know what changed a balance and
why, or when you keep your own copy of the balances. The Products API returns only the current
balance.

A location has one stock. Every product with stock tracking has a balance in it. A balance is
never set directly. It changes when something happens, such as:

- A merchant sells the product at a point of sale.
- A delivery arrives.
- Staff take out what they use.
- A stocktake corrects the balance to what was counted.

A stock movement is the record of one such change to one balance. A movement is never changed or
removed. A mistake is undone by a new movement that corrects it.

Movements made by a sale or a stocktake are read here too. The Sales API and the Stocktakes API
are coming soon.

A colour bar item has a balance in the same stock, counted in packs. A change to it is a stock
movement too. A movement has a `productId` or a `colourBarItemId`, never both. When a colour bar
mix finishes a pack, the pack leaves the stock as a `withdrawal` with the mix's `colourBarMixId`.
Such a movement is not corrected on its own; the mix is undone.

A stock movement belongs to one location and has an id of the form `mov_...`.
`GET /v1/locations/{locationId}/stock-movements` lists the movements oldest first. Reading needs
the scope `stock:read`.

The list can be followed. Keep the id of the last movement you have read and pass it as `after`
to get what has happened since. That is how to keep your own copy of the balances up to date
without reading every product again. A movement appears in the list about ten seconds after it is
made.

### Recording movements

`POST /v1/locations/{locationId}/stock-movements` records a stock document: one or more stock
changes that belong together, such as the rows of a delivery note, or products taken from the
shelf for the location's own use. It needs the scope `stock:write`.

- **A `delivery` or `withdrawal` line names a product or a colour bar item**, with `productId` or
  `colourBarItemId`. For a colour bar item the quantity is whole packs: a delivery of twelve tubes
  is `12`. A `correction` line names neither, only the movement it undoes.
- **Every line is applied, or none.** A line that takes out more than is in stock gets the whole
  document refused with `insufficient_stock`; `details` names the product or the colour bar item,
  its balance and the quantity asked for.
- **`listValue`** is what a withdrawal is worth at the supplier's list price, VAT excluded. It is
  there when what was taken is linked to an article in a supplier's catalogue that has a list
  price. A location uses it to charge staff for what they take for themselves, with the `reason`
  `own-use`.
- **Give the document your own `externalReference`**, such as the delivery note number. Sending
  the same reference again changes nothing and answers `200` with the movements made the first
  time, however long ago.
- **`staffId` names the staff member who made the change.** Read it from the Staff API: from the
  staff list, or from the PIN check.
- **Two line types need a further scope.** A `correction` undoes a withdrawal and needs
  `stock:correct`. A `delivery` sets what the goods cost and needs `stock:cost`. A key for a
  screen where staff record what they use needs neither.

### List stock movements

`GET /v1/locations/{locationId}/stock-movements`

Scope: `stock:read`

Stability: preview

Every change of a stock balance at the location, oldest first, including sales at the point of sale. The list can be followed: keep the id of the last movement you have read and pass it as `after` to get what has happened since. A movement appears in the list about ten seconds after it is made.

**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 | Only movements made after this one: a `nextCursor` or the id of any movement. |
| `productId` | query | string | No | Only this product's movements. |
| `colourBarItemId` | query | string | No | Only this colour bar item's movements. |
| `type` | query | string | No | Only movements of this `type`. |
| `externalReference` | query | string | No | Only movements with this external reference. |

**Responses**

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

### Record stock movements

`POST /v1/locations/{locationId}/stock-movements`

Scope: `stock:write`

Stability: preview

Records a document of stock changes: a delivery note, products taken from the shelf for the location's own use. Every line is applied, or none: a line that takes out more than is in stock gets the whole document refused with `insufficient_stock`. Answers `201` with the movements made, and `200` with the movements made the first time when `externalReference` was used before. A `correction` line needs the scope `stock:correct` as well, and a `delivery` line the scope `stock:cost`, since it sets what the goods cost.

**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**: StockDocument

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | The `externalReference` was used before. Nothing in this request was applied; the movements made then are returned. | StockDocumentResult |
| 201 | The movements were made. | StockDocumentResult |
| default | Any failure. See the Errors section. | Error |

## Stocktakes

**Coming soon.** Start a count of a location's stock, send what was counted and read the differences.

## Schemas

### 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. |

### Product

Represents an article a location sells or uses: an entry in its product list. Belongs to that location. Amounts are decimal strings in the location's currency. Quantities are whole numbers of the product.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `articleNumber` | string | No | The supplier's article number, as read from a delivery note. Not unique: two suppliers can use the same one. Example: `81650673`. |
| `averageCost` | string | No | The average cost of those in stock, VAT excluded. Only with the scope `stock:cost`. Example: `61.2311`. |
| `brand` | string | No | Example: `Wella`. |
| `createdAt` | string (date-time) | No | When it was created, in UTC. Left out when that is not known. Example: `2026-03-02T08:15:00Z`. |
| `eans` | array of string | Yes | The product's barcodes. A product can be linked to an article in a supplier's catalogue, and then that article's barcodes are included. Can be empty. Example: `["4064666230412"]`. |
| `id` | string | Yes | Product id. Example: `prd_5Tg8mPq1Xc7Jd2Ls9WbNrK`. |
| `latestPurchasePrice` | string | No | What one cost at the latest delivery, VAT excluded. Only with the scope `stock:cost`. Example: `62.4500`. |
| `locationId` | string | Yes | Example: `loc_4fK9xQ2mLr8TzB1vNc7HdW`. |
| `name` | string | Yes | Example: `Color Touch 7/0`. |
| `notForSale` | boolean | Yes | Marked as not sold to customers, such as colour used in a treatment. It is a mark only: the point of sale does not refuse to sell the product. Example: `true`. |
| `price` | string | No | The sale price, VAT included. Left out when none is set. Example: `149.00`. |
| `stockQuantity` | integer (int32) | No | The stock balance. Never negative. Left out when the product is not stock-tracked. Example: `12`. |
| `stockTracked` | boolean | Yes | Whether the location keeps a stock balance for the product. Example: `true`. |
| `updatedAt` | string (date-time) | Yes | When it last changed, in UTC. Example: `2026-10-12T09:24:31.512Z`. |
| `vatPercent` | string | No | VAT rate in per cent. Example: `25`. |

### ProductList

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

### StockDocument

Stock changes that belong together, such as the rows of a delivery note or what one treatment used. Every line is applied, or none.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `comment` | string | No | A note, at most 500 characters. Stored on every movement. Example: `Colour for Anna, 14:00`. |
| `externalReference` | string | No | Your own reference for the document, such as a delivery note number, 1 to 100 characters. A document whose reference has been used at the location changes nothing and is answered with the movements made the first time, so it is safe to send again at any time. Example: `DN-2026-10412`. |
| `lines` | array of StockMovementLine | Yes | 1 to 100 lines, applied in order. |
| `staffId` | string | No | The staff member who did it. Shown as who made the movements. Example: `stf_7Qm2xLd9Kc4Vb1Ts8HpWzE`. |

### StockDocumentResult

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of StockMovement | Yes | The movements made, in the order of the lines. |

### StockMovement

One change of the stock balance of a product or of a colour bar item. A movement is never changed or removed: a mistake is undone by a new movement that corrects it.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `balanceAfter` | integer (int32) | Yes | The balance after this movement. Example: `505`. |
| `colourBarItemId` | string | No | The colour bar item whose balance changed, in packs. Left out when the movement is a product's. Example: `cbi_3Vn8Kq5TzL1xWc7PdM2RbJ`. |
| `colourBarMixId` | string | No | The colour bar mix that made the movement, by finishing a pack or by being undone. Such a movement is not corrected on its own: the mix is undone. Example: `cbm_8Wq3Nd6HtY1mKx4LcP9RvB`. |
| `comment` | string | No | Example: `Colour for Anna, 14:00`. |
| `correctedByMovementId` | string | No | On a movement that has been corrected: its correction. Example: `mov_6Wr3tYu8Io1Pa4Sd7FgHjK`. |
| `correctsMovementId` | string | No | On a correction: the movement it undoes. Example: `mov_9Bd2fHk4Mn7Pq1Sv6XzCtA`. |
| `createdAt` | string (date-time) | Yes | Example: `2026-10-11T09:24:31.512Z`. |
| `externalReference` | string | No | The caller's own reference for what the movement belongs to. Example: `visit-88213`. |
| `id` | string | Yes | Movement id. Example: `mov_3Lp6vRt9Yx2Kc5Ng8QwEzH`. |
| `listValue` | string | No | On a withdrawal: what was taken, at the list price then of the article in a supplier's catalogue that it is linked to, VAT excluded, as a decimal string. Left out when there is no such list price. It is kept as it was: a later change of the list price does not change it. Example: `304.00`. |
| `productId` | string | No | The product whose balance changed. Left out when the movement is a colour bar item's. Example: `prd_5Tg8mPq1Xc7Jd2Ls9WbNrK`. |
| `quantity` | integer (int32) | Yes | The change applied to the balance: negative when stock left. Example: `-2`. |
| `reason` | string | No | Why stock was taken out: a lower-case word such as `consumption` or `damaged`. The list is open, so accept a value you do not know. Example: `consumption`. |
| `residue` | integer (int32) | No | On a withdrawal made by a colour bar mix: the grams that were never taken from the pack it finished. Example: `3`. |
| `shortfall` | integer (int32) | Yes | How much of what was asked for was not in stock; the balance never goes below 0. A sale of 3 with 1 in stock has `quantity` -1 and `shortfall` 2. 0 when everything was in stock. Example: `0`. |
| `staffId` | string | No | Who made it, when a staff member did: look the name up in the staff list. Left out for a movement made without one, such as by an administrator in Cliento. Example: `stf_7Qm2xLd9Kc4Vb1Ts8HpWzE`. |
| `type` | string | Yes | `sale` and `refund`: at the point of sale. `delivery`: goods received, or the adjustment made when a delivery is changed afterwards. `withdrawal`: taken out by hand. `correction`: undoes the movement in `correctsMovementId`. `stocktake`: the difference found by a count. `opening_balance`: the balance when movements began to be recorded for the product. More values may be added. Example: `withdrawal`. |

### StockMovementLine

One line of a stock document.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `colourBarItemId` | string | No | The colour bar item, in place of `productId`. Its `quantity` is in whole packs. Example: `cbi_3Vn8Kq5TzL1xWc7PdM2RbJ`. |
| `movementId` | string | No | For a `correction`: the withdrawal to undo. Example: `mov_9Bd2fHk4Mn7Pq1Sv6XzCtA`. |
| `productId` | string | No | The product. A `delivery` or `withdrawal` line names a product or a colour bar item, never both. Example: `prd_5Tg8mPq1Xc7Jd2Ls9WbNrK`. |
| `quantity` | integer (int32) | No | How many, as a whole number greater than 0. Required for all types but `correction`. Example: `2`. |
| `reason` | string | No | For a `withdrawal`: why, as one lower-case word such as `consumption` or `damaged`. The list is open. Example: `consumption`. |
| `type` | string | Yes | `delivery`: goods received. `withdrawal`: stock taken out. `correction`: undoes the withdrawal in `movementId`. Example: `withdrawal`. |
| `unitCost` | string | No | For a `delivery`: what one cost, VAT excluded, as a decimal string with at most four decimals. 0 when left out. Example: `62.4500`. |

### StockMovementList

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of StockMovement | Yes |  |
| `nextCursor` | string | No | Pass as `after` to fetch the next page. Absent when there is nothing more yet; to follow new movements, call again later with the id of the last movement you have read. Example: `mov_3Lp6vRt9Yx2Kc5Ng8QwEzH`. |
