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
eanorarticleNumberto the list. - Quantities are whole numbers: ten bottles are a
stockQuantityof 10. stockQuantityis 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 scopestock: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
deliveryorwithdrawalline names a product or a colour bar item, withproductIdorcolourBarItemId. For a colour bar item the quantity is whole packs: a delivery of twelve tubes is12. Acorrectionline 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;detailsnames the product or the colour bar item, its balance and the quantity asked for. listValueis 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 thereasonown-use.- Give the document your own
externalReference, such as the delivery note number. Sending the same reference again changes nothing and answers200with the movements made the first time, however long ago. staffIdnames 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
correctionundoes a withdrawal and needsstock:correct. Adeliverysets what the goods cost and needsstock: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. |