The stock model
This page describes what a stock quantity from STP actually means. Read it before building an integration — most integration mistakes come from one of the four points below rather than from the API itself.
Stock types
Stock is not one number per item. It is one number per (business unit, item, stock type), because goods in different states are not interchangeable.
| Stock type | Meaning |
|---|---|
SalesStock | Available to sell in this business unit. This is what a POS shows as "in stock". |
InOrder | Ordered from a supplier, or incoming from another store, but not yet received. |
InTransit | Has left the sending store and is on its way to the receiving store. |
Reserved | Physically present but set aside for a customer, so it must not be sold to anyone else. |
ReservedInOrder | Reserved for a customer against stock that has been ordered but not yet arrived. |
Returned | Returned by a customer, and not yet put back into sales stock. |
SalesStockmeans "available in this store", not "on the shelf". It covers the goods wherever they physically sit — shelf, overstock above or below it, back room, anywhere in the business unit. STP tracks quantities per business unit, not per location within one, so a shelf-versus-back-room distinction is not part of this model. A store colleague looking at a quantity of 12 may find 4 of them out and 8 still in the back.
A single business operation usually moves several of these at once — that is the point of separating them. A
store transfer moves goods out of SalesStock, into InTransit, and into the receiving store's InOrder, all
from one event. See Multiple transactions.
🛑
SalesStockis the only one that means "sellable". Summing all stock types for an item gives a number that means nothing — it double counts goods that are reserved out of stock on hand, and it counts goods that have not arrived. If you want "how many can I sell", readSalesStock.
Quantity signs
Quantities are signed, and the sign follows the effect on the stock type named in the transaction:
| Movement | Stock type | Sign |
|---|---|---|
| Sale | SalesStock | negative |
| Return | Returned | positive |
| Goods received | SalesStock | positive |
| Stock count showing less than expected | SalesStock | negative (the loss) |
🛑 A return does not increase
SalesStock. It increasesReturned. This is deliberate — returned goods may be damaged and are not automatically sellable — but it surprises almost every new integration. If a customer asks "the item was returned, why didn't stock go up?", this is the answer.
What happens to Returned stock next
STP has no way to know whether a returned article is fit to sell again — a sealed box and a damaged one look
identical on a receipt. So it never assumes: every return lands in Returned, and the business decides what
happens to it. That decision is an explicit operation, which means it is recorded and auditable rather than
implied.
Two things a store can do, both through the Input API:
Put it back into sellable stock — a stock transfer within the same business unit, moving between stock types:
POST /stock-transfers
{
"fromBusinessUnitId": "store-1", // the same store on both sides
"toBusinessUnitId": "store-1",
"fromStockType": "Returned",
"toStockType": "SalesStock",
...
}
Because the two stock types differ, this produces exactly two transactions — Returned down, SalesStock up —
with no InTransit or InOrder legs. Both business unit ids are required, so set both to the same store.
Write it off, if the goods are scrapped — a generic stock transaction against Returned with a negative
quantity:
POST /business-units/{businessUnitId}/stock-transactions
{ "lines": [ { "stockType": "Returned", "quantity": -1, ... } ] }
To set Returned to an absolute figure instead — after a stock take of the returns area, say —
POST /business-units/{businessUnitId}/synchronize-stocks also accepts a stockType.
⚠ stockType defaults to SalesStock on both the stock-transaction and synchronize-stock lines. Omitting it
does not mean "whichever type this item has" — it means sellable stock.
Delta or level? Both are published, and they are different fields
This is the single most common integration error.
| Event | Field | Meaning |
|---|---|---|
| Processed transaction | quantity | the change — how much this movement added or removed |
| Processed transaction | remainingStockQuantity | the level — the resulting quantity after this movement |
| Stock level update | quantity | the level — the current quantity for that stock type |
So quantity means a delta on one event and a level on the other. If you are keeping a projection of current
stock, use the stock level updates — they carry the level directly and are the smaller, simpler contract. Use
processed transactions when you need to know why the level changed.
Batches and lots
Within one (business unit, item, stock type), stock is tracked as batches — one per arrival. A batch records:
batchNumberandbusinessPartnerId— which delivery, from which supplierserialNumber— for individually tracked unitsbestBeforeDateandexpirationDate— for date-sensitive goods- its own remaining quantity and its own cost
Batches are what make cost calculation possible: FIFO, FEFO and LIFO are all rules for which batch to draw down, and the cost of the movement is the cost of the batch it drew from.
When the requested batch cannot satisfy the movement
A sale may reference a specific serial number or batch that no longer has stock — because it was already sold, or because a stock count reduced it, or because of a mistake earlier in the chain. STP applies one rule in that situation:
Keeping the total quantity correct takes priority over keeping batch and serial attribution correct.
So the movement is applied to another batch rather than being rejected or silently dropped. The store's total quantity for the item stays right, and the batch-level attribution absorbs the discrepancy. For a business that uses serial numbers for traceability this is worth knowing explicitly: the quantity is the guarantee, the serial attribution is best effort.
Stock can go negative — and it must
If more is sold than the system believes exists, the quantity goes negative rather than stopping at zero. This is not an error state being tolerated; it is what keeps the arithmetic correct in a workflow that happens every day.
The everyday case. A delivery arrives and an item is out of stock, so staff put it straight out to sell rather than waiting for the paperwork. Availability beats bookkeeping order, which is the right call for the business — but it means sales are recorded before the delivery is. From the system's point of view, goods are leaving that it does not yet know arrived.
stock = 0 item is sold out; a delivery of 50 physically arrives and goes straight out
sell 3 → stock = -3 the sales are real and must be recorded
register the
goods received → stock = 47 50 added to -3
47 is correct, and only reachable because the −3 survived. Had the negative been clamped to 0, registering the delivery would have given 50 — three more than are physically there, and the three sales would have vanished from the stock arithmetic while remaining as revenue.
This is why a goods-received registration adds its quantity rather than asserting a total. A stock count or a synchronize stock operation is different — those do assert an absolute quantity, so counting 50 in the situation above would set the stock to 50 and book a gain of 53 that never happened. Register the delivery; do not count it in.
The negative is therefore expected to be short-lived: it closes itself when the delivery is registered. If negatives
persist for an item, that is a signal worth acting on — deliveries not being registered, or genuine shrinkage — and
the suspicious items report flags exactly that as NEGATIVE_STOCK.
For an integration: expect negative quantities, display them as they are, and never clamp them to zero. A consumer that floors at 0 reintroduces precisely the error described above, in its own copy of the data.
Precision
- Quantities carry up to 2 decimal places — stock can be counted in kilograms or metres, not only in whole units.
- Costs and sales amounts carry up to 3 decimal places.
- Every amount has a
currencyCode. Do not assume a single currency per tenant.
Read next
- Operations — which business operations produce which movements
- Multiple transactions — one operation, several stock types
- Cost calculations — how a batch is chosen and priced
- External Events — the published event contracts