The API
All endpoints are addressed by business unit:
/business-units/{businessUnitId}/store-transfersβ¦
The business unit in the URL must be one of the two stores on the transfer. A request for a transfer that involves neither is refused β this is how a store is prevented from reading or changing another store's transfers.
π Writes are asynchronousβ
Every write returns 202 Accepted. The response means the instruction was accepted, not that it
has been applied. The change lands a moment later.
Two consequences, and both bite in practice:
- Reading straight back can return the old state. If you need to confirm a write, poll the transfer until it shows what you expect rather than reading once.
- A write that depends on an earlier write can arrive too early. Creating a transfer and then immediately adding a line to it can fail, because the line write checks that the transfer exists and the create may not have landed yet.
The second one has a proper fix β use the composite create below rather than sequencing two calls.
Reads (GET) are ordinary synchronous calls and answer from stored state.
Raising a transferβ
Create the transfer and its lines in one call. This is the recommended way to raise a transfer:
POST /business-units/{businessUnitId}/store-transfers:with-lines
{
"fromBusinessUnitId": "store-42",
"toBusinessUnitId": "store-17",
"comment": "weekend top-up",
"lines": [
{ "itemId": "7350053850019", "quantity": 6 },
{ "itemId": "7350053850026", "quantity": 2 }
]
}
fromBusinessUnitId, toBusinessUnitId and at least one line are required, and each line needs
itemId and quantity. lineId and comment are optional β omit lineId and one is minted.
β The line field is quantity, not quantityRequested. The transfer returns
quantityRequested and quantityApproved as two separate fields, but you write to them through the
single quantity field, and which one it lands in depends on which store you are calling as β see
below.
The whole draft is written together, so there is no window in which the transfer exists without its lines and no second call that can arrive too early.
A plain POST /business-units/{businessUnitId}/store-transfers without lines also exists, and is
still the right call if you genuinely have nothing to add yet β a transfer being built up
interactively, for instance. If you already know the lines, send them together.
Every endpointβ
Two groups, and the distinction matters more than it looks:
- Used by the supplied flow β reading, building the draft, and
:send-request. This is what the Hii Retail Inventory app calls, and what you would call to raise transfers from your own system. - Integration-only β writing approved quantities, line infos, and
:submit. Supported, but nothing in the supplied flow calls them: a transfer picked in Click and Collect completes on its own. Reach for these only if you are replacing the picking app with your own system.
Readingβ
| Endpoint | Answers |
|---|---|
GET /business-units/{businessUnitId}/store-transfers | all transfers this store is involved in, incoming and outgoing |
GET /business-units/{businessUnitId}/store-transfers/{storeTransferId} | one transfer, with its lines |
Each transfer in the list carries a line count (itemCount) and a total unit count (totalUnits),
so a dashboard does not have to fetch every transfer to show its size. Both endpoints also return
the transfer's orderCode β the short human-readable code, e.g. S4K9M2X.
Query parameters on the list:
| Parameter | Values | Default |
|---|---|---|
status | CREATED, REQUESTED, IN-PROGRESS, COMPLETED, DELETED β repeatable | CREATED and REQUESTED only |
direction | FROM β what this store is sending Β· TO β what it is receiving | both |
limit | 0β100 | β |
offset | 0 or more | β |
π Omitting status does not mean "all statuses". It defaults to CREATED and REQUESTED β the
transfers still needing attention. A caller that lists without status and concludes a transfer has
vanished is looking at a filtered view: the transfer completed. Always pass status explicitly
when you want history, listing every value you care about.
β Drafts and deleted transfers are only ever visible to the receiving store, whichever
direction is asked for.
The list is ordered newest first, by creation time. With limit set, older transfers fall off
the end rather than being reachable by another filter β page with offset to reach them.
Building the draftβ
| Endpoint | Does |
|---|---|
POST β¦/store-transfers:with-lines | creates a transfer together with its lines |
POST β¦/store-transfers | creates an empty transfer |
PUT β¦/store-transfers/{storeTransferId} | changes the transfer's comment |
PUT β¦/store-transfers/{storeTransferId}/lines | adds a line, or changes one that is already there |
DELETE β¦/store-transfers/{storeTransferId}/lines/{lineId} | removes a line |
DELETE β¦/store-transfers/{storeTransferId} | abandons the draft |
Removing a line, and abandoning the draft, can only be done by the receiving store. See Lifecycle for the full matrix.
The line endpoint means two different thingsβ
PUT β¦/store-transfers/{storeTransferId}/lines takes one quantity field:
{ "itemId": "7350053850019", "quantity": 4, "comment": "only four on the shelf" }
π Which stored field that quantity lands in depends on which business unit you call as.
| Calling as | quantity sets | Rules |
|---|---|---|
the receiving store (toBusinessUnitId) | quantityRequested | must be at least 1 |
the sending store (fromBusinessUnitId) | quantityApproved | may be 0 β that is how a line is refused |
The first row is the ordinary case: building the request.
The second row is integration-only. A store never writes approved quantities this way β when a transfer is picked in Click and Collect, the pick sets them. It is there for a customer who has replaced the picking app with their own system and is deciding what ships themselves; see Completing from your own system.
β Calling as the wrong business unit does not fail. The request succeeds and the number lands in the other field β an intended approval turns up as a changed request, and nothing reports it.
Omit lineId to add a line; pass an existing one to change it.
Batch, serial and expiry detail β integration-onlyβ
A line can carry one or more line infos β the specific units being sent, when that matters:
| Endpoint | Does |
|---|---|
PUT β¦/store-transfers/{storeTransferId}/lines/{lineId}/lineInfo | adds or changes a line info |
DELETE β¦/store-transfers/{storeTransferId}/lines/{lineId}/line-info/{lineInfoId} | removes one |
Each line info carries a quantity plus whichever of these apply: batch number, serial number, best before date, expiry date. When they are present the detail travels on the completed event, so the receiving store knows exactly which batches arrived.
β Nothing in the supplied flow writes these. A transfer raised and picked through the Hii Retail
apps carries no line infos, and lineInfos arrives as an empty array on the completed event. They
exist for an integrator who tracks goods at batch or serial level and is writing the transfer from
their own system. Do not treat an empty lineInfos as missing data β treat quantityApproved on the
line as the quantity that shipped.
Moving it alongβ
| Endpoint | Does |
|---|---|
POST β¦/store-transfers/{storeTransferId}:send-request | sends the request to the other store |
POST β¦/store-transfers/{storeTransferId}:submit | completes the transfer |
:send-request requires the transfer to be a draft with at least one line β an empty transfer
cannot be sent.
:submit requires the caller to be the sending store, and is
integration-only β a transfer picked in Click and
Collect completes on its own. See Lifecycle for the full set of guards.
Permissionsβ
Each endpoint requires its own permission, so a role can be given the ability to build a transfer without the ability to send or complete one:
| Group | Permissions |
|---|---|
| Reading | str.transfer.get, str.transfer-line.get |
| Building | str.transfer.create, str.transfer.update, str.transfer.delete, str.transfer-line.create, str.transfer-line.delete, str.line-info.delete |
| Progressing | str.transfer.send-request, str.transfer.submit |
The composite create requires both str.transfer.create and str.transfer-line.create, since it
does both jobs.
Tracing a requestβ
Pass a Correlation-Id header and it is carried through every downstream step β the write, the
events, and the stock movements that follow. It is returned on the write responses. If you do not
send one, one is generated.
Referenceβ
The OpenAPI specification is the definitive source for request and response shapes: Store Transfer API.