Skip to main content

Events

Store Transfer publishes two events. Most integrations only ever hear about the second one and then wonder why they find out about transfers so late — the first is the one that tells you goods are coming.

EventPublished whenTells you
str.public.event.store-transfer-requested.v1the request is sent to the other storea transfer has been raised, what was asked for, and from where
str.public.event.store-transfer-completed.v1the transfer is completedwhat actually shipped, with quantities and values

Subscribe by the topic name. If you route on the Event-Type attribute instead, the values are STORE_TRANSFER_REQUESTED and STORE_TRANSFER_COMPLETED.

Message attributes​

Both events carry the same attributes, usable as subscription filters:

AttributeValue
Tenant-Idthe tenant
Business-Unit-Idthe business unit that triggered this step
Correlation-Idtraces back to the API call that caused it
Event-TypeSTORE_TRANSFER_REQUESTED or STORE_TRANSFER_COMPLETED

⚠ Business-Unit-Id is the acting store, not the transfer's direction. Do not use it to decide whether you are the sender or the receiver — read fromBusinessUnitId and toBusinessUnitId from the payload instead.

Store transfer requested​

Sent when a store sends its request to the other store. The goods have not moved; they are reserved.

{
"tenantId": "tenant-1",
"fromBusinessUnitId": "store-42",
"toBusinessUnitId": "store-17",
"storeTransferId": "0f8b7d3c-6f1a-4a2e-9a1c-2b7d0e5f4a3b",
"requestedDateTime": "2026-09-01T08:14:22.145Z",
"orderCode": "S4K9M2X",
"comment": "weekend top-up",
"createdBy": "user-abc",
"requestedBy": "user-abc",
"items": [
{ "lineId": "line-1", "itemId": "7350053850019", "quantity": 6, "comment": null }
]
}
FieldNotes
tenantId, fromBusinessUnitId, toBusinessUnitId, storeTransferId, requestedDateTime, itemsalways present
orderCodethe short human-readable code, e.g. S4K9M2X. Always begins S for a store transfer. May be null on transfers raised before codes existed
comment, createdBy, requestedBymay be null
items[].quantitythe requested quantity — what was asked for, not what will arrive

Store transfer completed​

Sent when the transfer is completed. The goods have left the sending store.

{
"tenantId": "tenant-1",
"fromBusinessUnitId": "store-42",
"toBusinessUnitId": "store-17",
"storeTransferId": "0f8b7d3c-6f1a-4a2e-9a1c-2b7d0e5f4a3b",
"transferDateTime": "2026-09-01T11:02:57.881Z",
"comment": "weekend top-up",
"createdBy": "user-abc",
"requestedBy": "user-abc",
"completedBy": "user-def",
"items": [
{
"lineId": "line-1",
"itemId": "7350053850019",
"quantityRequested": 6,
"quantityApproved": 4,
"unitPrice": 12.5,
"currencyId": "SEK",
"comment": null,
"lineInfos": [
{ "lineInfoId": "li-1", "quantity": 4, "batchNumber": "B-2291", "bestBeforeDate": "2026-12-01" }
]
}
]
}
FieldNotes
tenantId, fromBusinessUnitId, toBusinessUnitId, storeTransferId, transferDateTime, itemsalways present
items[].quantityApprovedthe quantity that actually moved. This is the number to act on
items[].quantityRequestedwhat was originally asked for. Kept so the shortfall is visible
items[].unitPrice, items[].currencyIdvaluation at completion time. Both may be null — see below
items[].lineInfosbatch, serial and expiry detail where the goods are tracked at that level
items[].substituteForLineIdset when this line is a substitute for another — see below. Null on ordinary lines
completedBywho completed it; may be null

⚠ bestBeforeDate and expirationDate are plain dates — 2026-12-01, not a timestamp. Only transferDateTime and requestedDateTime carry a time.

🛑 A null unitPrice means "not known", not "free". The valuation lookup can fail or find no price, and the line is still published rather than being held back. Summing a null as zero understates the value of the movement.

⚠ store-transfer-completed does not carry orderCode. If you need the code, take it from the requested event and key on storeTransferId, or read it from the transfer through the API.

Substitutions add lines that were never requested​

A picker who cannot find the requested item may pick something else instead. When that happens the completed event carries more lines than the request did, and the original line is still there:

LineitemIdquantityRequestedquantityApprovedsubstituteForLineId
line-1the requested item60null
sub-9f2c…the item actually picked04line-1

🛑 The original line is never removed. It stays on the event with its quantityApproved reduced to whatever was picked of the original item — 0 when the substitute replaced it entirely, or a smaller number when the picker found some of both. A consumer that reports "line-1 was short" without looking for a substitute understates what actually arrived.

The rules:

  • Sum quantityApproved across all lines to get what shipped. Original and substitute lines never double-count the same units.
  • Match lines on lineId, not on position or order. A substitute's lineId is derived from its parent and the item, in the form sub-<hash>, and is stable — a redelivery of the same event updates the same line rather than adding another.
  • substituteForLineId names the line it stands in for, and that line is always on the same event.
  • One line can have several substitutes. Two half-measures of different items against one request is an ordinary outcome.
  • quantityRequested on a substitute line is always 0. Nobody asked for it; it was offered.

Substitution is a Click and Collect picking feature, so it only appears on transfers picked through the app. A transfer completed through :submit carries whatever lines were written to it.

Consuming these events​

Delivery is at-least-once — expect duplicates. Deduplicate on storeTransferId together with the event type: a transfer produces exactly one requested and one completed event, so the pair (storeTransferId, Event-Type) is a safe key. Redelivery of a completed event is not a second transfer.

Do not rely on arrival order. Order by the event's own timestamp — requestedDateTime on the requested event, transferDateTime on the completed one. Do not order by the time you received the message, and do not assume the requested event arrives before the completed one.

Order by event time, not processing time. The timestamps above are when the business step happened. A redelivered or replayed message keeps its original timestamp, which is what makes it usable for ordering.

Handle unrecognised values. Treat enumerations as open — new values can be added without a new event version, so a value you do not recognise should be passed through or ignored, never treated as an error.

New optional fields arrive on the existing version. Adding an optional field is not a breaking change, so it is added to the current version rather than published as a new one, and the published schema gains the field before any event carries it. Ignore fields you do not recognise, and if you validate against the schema, use the current published copy rather than one you saved. A new version is published only for a breaking change — a field removed, renamed, retyped or made required.

Both stores see both events. The events are not addressed to one party. If you are integrating on behalf of one store, filter on fromBusinessUnitId / toBusinessUnitId yourself.

Pairing the two events​

A transfer is only fully described by both events together:

  • the requested event tells you a transfer exists and what was asked for
  • the completed event tells you what actually shipped

They share storeTransferId, which is the key to join them. Expect a gap between them — minutes if someone picks straight away, longer if the transfer waits.

⚠ Not every requested transfer completes. A transfer that is never picked simply stays requested, and no further event is published. If you are tracking transfers in flight, age them out yourself rather than waiting indefinitely for a completed event that may never come.

Subscribing from outside Hii Retail​

External systems do not read these topics directly — they receive them through Hii Retail's External Events, which is the supported route for anything outside the platform. Both events are published there with their schemas.