Skip to main content

Events

Stock Count publishes two public events. Both are available to external systems through External Events.

EventPublished whenUse it for
stc.public.event.stock-count-completed.v1a count is submittedthe counted quantities — applying a count to your own stock, loss and shrinkage reporting
stc.public.event.stock-count-changed.v1a count is created or editedplanning views, reminders, knowing a count is coming

A test count is never published on either.

Stock count completed​

stc.public.event.stock-count-completed.v1

One submitted count, with every item it covers — counted by staff, or set to 0 because it was mandatory and nobody counted it. Items that were optional and not counted are not in it.

{
"tenantId": "acme",
"businessUnitId": "store-42",
"stockCountId": "0b8e4f6a-2f0c-4a57-9d61-6d1f6f2b7c10",
"type": "CYCLE",
"submittedAt": "2026-09-23T15:02:11.000Z",
"submittedBy": "a5f460606be52a0b91f57c4b2e5c037d",
"comment": "Dairy, week 39",
"numberOfItemsCounted": 2,
"numberOfItemsNotCounted": 0,
"messageNumber": 1,
"lastMessageNumber": 1,
"expectedCostAmount": 118.5,
"actualCostAmount": 94.8,
"firstCountedDateTime": "2026-09-23T12:00:04.512Z",
"lastCountedDateTime": "2026-09-23T15:02:10.998Z",
"items": [
{
"itemId": "7350053850019",
"itemIdentifier": "7350053850019",
"itemName": "Yoghurt natural 1 l",
"expectedQuantity": 25,
"expectedCostAmount": 98.75,
"actualQuantity": 24,
"actualCostAmount": 94.8,
"unitCostPrice": 3.95,
"firstCountedDateTime": "2026-09-23T12:00:04.512Z",
"lastCountedDateTime": "2026-09-23T12:10:31.207Z",
"locations": [
{
"location": "1",
"actualQuantity": 10,
"firstCountedDateTime": "2026-09-23T12:00:04.512Z",
"lastCountedDateTime": "2026-09-23T12:00:04.512Z",
"lines": [
{ "lineId": "7c1e…", "actualQuantity": 10, "unitCostPrice": 3.95, "countedDateTime": "2026-09-23T12:00:04.512Z" }
]
},
{
"location": "2",
"actualQuantity": 14,
"firstCountedDateTime": "2026-09-23T12:10:31.207Z",
"lastCountedDateTime": "2026-09-23T12:10:31.207Z",
"lines": [
{ "lineId": "d402…", "actualQuantity": 14, "unitCostPrice": 3.95, "countedDateTime": "2026-09-23T12:10:31.207Z" }
]
}
]
},
{
"itemId": "7350053850026",
"itemIdentifier": "7350053850026",
"itemName": "Yoghurt vanilla 1 l",
"expectedQuantity": 5,
"expectedCostAmount": 19.75,
"actualQuantity": 0,
"actualCostAmount": 0,
"unitCostPrice": 3.95,
"firstCountedDateTime": "2026-09-23T15:02:10.998Z",
"lastCountedDateTime": "2026-09-23T15:02:10.998Z",
"locations": [
{
"location": "UNKNOWN",
"actualQuantity": 0,
"firstCountedDateTime": "2026-09-23T15:02:10.998Z",
"lastCountedDateTime": "2026-09-23T15:02:10.998Z",
"lines": [
{ "lineId": "91aa…", "actualQuantity": 0, "unitCostPrice": 3.95, "countedDateTime": "2026-09-23T15:02:10.998Z" }
]
}
]
}
]
}

The second item was mandatory and nobody counted it, so it was set to 0 at submission — location UNKNOWN, dated at the moment of submission. The full schema is on Stock count completed — event schema.

The count​

FieldAlways presentMeaning
tenantId, businessUnitIdyesThe tenant and the store counted
stockCountIdyesThe count
typeyesFULL, CYCLE or CONTROL — see Types of count. Treat it as an open list.
submittedAtyes, may be nullWhen the count was submitted
submittedByyes, may be nullOpaque identifier of the user who submitted it — not a name
commentnoFree text, if the count has one
numberOfItemsCountedyesItems in the whole count with at least one line, including those set to 0 at submission
numberOfItemsNotCountedyesListed items with no line at all — the optional items nobody counted. Always 0 for a control count.
messageNumber, lastMessageNumberyesWhich part of the count this message is — see below
expectedCostAmount, actualCostAmountnoThe expected and counted value of the whole count, where known
firstCountedDateTime, lastCountedDateTimenoThe earliest and the latest line in the whole count
itemsyesThe items — at most 1 000 per message

An item​

FieldAlways presentMeaning
itemId, itemIdentifieryes, one may be nullThe item's id and its identifier (a barcode such as a GTIN). Both keys are always present; at least one of the two is a string, and the other may be null.
itemNamenoThe item's name when it was counted
actualQuantityyes🛑 The counted quantity — the total across all locations. A level, not a change.
expectedQuantitynoThe quantity Stock Count expected when the item was counted — see below
unitCostPricenoThe item's cost per unit used to value the count
actualCostAmount, expectedCostAmountnoactualQuantity and expectedQuantity valued at unitCostPrice
firstCountedDateTimeyes, may be null🛑 When the item was counted — its earliest line, at any location. The time to apply the count as of.
lastCountedDateTimeyes, may be nullIts latest line
locationsyesWhere it was counted, each with its own actualQuantity, first and last time, and lines

A line​

FieldAlways presentMeaning
lineIdyesThe line
actualQuantityyesThe quantity registered on this line
countedDateTimeyes, may be nullWhen the line was registered in Stock Count
batchNumber, serialNumber, businessPartnerIdnoPresent when the line was counted per batch or serial number
itemIdentifier, unitCostPricenoPresent when known

Locations are named by whoever counted: the Hii Retail Inventory app numbers them 1, 2, 3; an integration can use any name. UNKNOWN marks a line Stock Count added itself, for a mandatory item nobody counted.

🛑 One count can arrive as several messages​

A count is published in parts of at most 1 000 items. Each message carries messageNumber (from 1) and lastMessageNumber, and repeats the count-level fields. An item is only ever in one part, so each message can be applied on its own. To know you have the whole count, collect every messageNumber from 1 to lastMessageNumber.

The count-level fields — the counters, the cost totals, firstCountedDateTime and lastCountedDateTime — describe the whole count and are the same in every part. Do not add them up across parts.

🛑 actualQuantity is what was counted — not the change, and not the new stock figure​

The event says how many were found, and when. It does not say how the stock changed, because that depends on what happened after the count: sales that continued while the count was open, deliveries registered in the afternoon, and so on. Stock Transaction Processing works out the change by applying the counted quantity as of the item's firstCountedDateTime — see Counting while the store is open.

expectedQuantity is Stock Count's copy of STP's quantity at the moment the item was counted, kept for the deviation review and for reporting. It is never negative — negative stock shows as 0 — and it takes no account of movements between the count and the submission. Do not compute the stock adjustment as actualQuantity − expectedQuantity; that is a report of the deviation seen while counting, not the correction to book.

⚠ The cost amounts value the count, not the stock movement. STP values the resulting STOCK_COUNT transaction with its own costing. A financial consumer that takes both this event's amounts and STP's processed transactions for the same store books the same loss twice — take one source. See Stock ownership.

Message attributes​

AttributeValue
Tenant-Idthe tenant
Business-Unit-Idthe store
Stock-Count-Idthe count — equals stockCountId
Correlation-Idties the message to the request that submitted the count
Event-Typealways SUBMIT_STOCK_COUNT

Stock count changed​

stc.public.event.stock-count-changed.v1

A snapshot of a count's settings and status, published when the count is created or edited. Each message is complete in itself — never a delta — so a consumer that missed earlier messages can act on any one of them.

{
"tenantId": "acme",
"businessUnitId": "store-42",
"recordId": "0b8e4f6a-2f0c-4a57-9d61-6d1f6f2b7c10",
"parentStockCountId": "0b8e4f6a-2f0c-4a57-9d61-6d1f6f2b7c10",
"type": "CYCLE",
"status": "CREATED",
"startDate": "2026-09-28T00:00:00.000Z",
"dueDate": "2026-10-01T00:00:00.000Z",
"modifiedDatetime": "2026-09-23T09:14:02.076Z"
}
FieldAlways presentMeaning
tenantId, businessUnitIdyesThe tenant and the store
recordIdyesThe count, as it is known in its store — the same id the completed event calls stockCountId
parentStockCountIdyesThe parent the count belongs to. Present even when it equals recordId, which it does for every count on this topic today.
typeyesFULL, CYCLE or CONTROL
statusyesThe count's status when the message was published
startDateyesThe day the count starts, as midnight UTC
dueDatenoThe day it is due, as midnight UTC. Absent — not null — when the count has no due date.
modifiedDatetimeyesWhen the count was last written. Use it to discard stale messages — see below.
Event-Type attributePublished when
STOCK_COUNT_CREATEDa count is created with POST /stock-counts — including full counts created in the app — or generated from a recurring definition
STOCK_COUNT_UPDATEDa count's settings are edited with PUT /stock-counts/{stockCountId}

The kind of change is an attribute, not a payload field, so a subscription can filter on it without reading the body: attributes.Event-Type = "STOCK_COUNT_CREATED". Every message also carries Tenant-Id, Business-Unit-Id, Stock-Count-Id (equal to recordId) and Correlation-Id.

⚠ It is not a status feed. Nothing is published on this topic when counting starts, when a count is submitted, or when it is deleted. To learn that a count was completed, subscribe to stock count completed.

All three timestamps have exactly three fractional digits in UTC (2026-09-23T09:14:02.076Z), so comparing modifiedDatetime values as strings gives the right order.

Consuming the events​

Delivery is at least once. A message can arrive more than once — a redelivery, or a submission that is retried after a failure, publishes again.

EventDeduplicate on
stock count completedstockCountId + messageNumber
stock count changedrecordId + modifiedDatetime

Do not rely on arrival order. For the changed event, keep the modifiedDatetime you last applied for each count and discard any message that is not newer. For the completed event, order does not matter within a count — the parts are independent. When one item is counted by two different counts, apply them by the item's firstCountedDateTime, not by arrival.

Use the attributes to filter. Tenant-Id and Business-Unit-Id are on every message of both events; filter on them rather than parsing the body.

New optional fields and new values do not mean a new version. Adding an optional field is not a breaking change, so it is added to the existing version, and the published schema gains it 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. Enumerations such as type and status can gain values, so handle one you do not recognise rather than rejecting the message. A new version is published only for a breaking change — a field removed, renamed, retyped or made required.

Levels, not deltas. Every quantity on the completed event is a level — what was found, or what was expected — never a change. The changed event carries no quantities.

Three different times. countedDateTime and firstCountedDateTime say when something was counted; submittedAt says when the count was closed; neither is when you received the message. If you apply a count to your own stock, apply it as of firstCountedDateTime per item — see Stock ownership.

What may be missing. comment, itemName, the cost fields, expectedQuantity and the per-line batch, serial and business-partner fields can be absent or null; submittedAt, submittedBy and the counted times are always present but may be null. An item always carries both itemId and itemIdentifier, and one of them may be null. Only dueDate is omittable on the changed event.