External Events
Goods Received publishes one event, when a delivery — or part of one — is approved. Nothing is published while a delivery is being created or counted, so an event always means "a store has accepted these quantities".
Read more about how to subscribe in External Events.
The event source
| Event source | goods-received |
| Name in the catalogue | GRC: Goods received |
| Version | v1 |
| Content type | application/json |
| Published when | A delivery, a container, or a single line is approved |
Inside Hii Retail the same event feeds Stock services, which is how an approved delivery becomes stock. Subscribing externally does not change or delay that.
The payload
The full schema is on Goods received completed format, and is also published as a JSON Schema you can validate against — the event source entry in the External Events catalogue links it. What the fields mean:
The delivery
| Field | Required | Meaning |
|---|---|---|
tenantId | ✅ | The tenant |
businessUnitId | ✅ | The receiving store |
uniqueId | ✅ | Hii Retail's id for the delivery. Stable across every event for that delivery |
deliveryNoteId | ✅ | The shipper's delivery note number |
shipperId | ✅ | Who sent the goods |
shipperType | ✅ | businessPartnerId — an external supplier or warehouse — or businessUnitId — another store |
deliveryDate | ✅ | The delivery date stated on the delivery note — when the goods were expected, not when they arrived or were approved |
submittedAt | ✅ | When the store approved. This is the event's own timestamp |
submittedBy | ✅ | The user who approved |
comment | ➖ | Free text from the delivery note. May be null |
transportationAmount | ➖ | Delivery-level transport cost. May be null |
customsAmount | ➖ | Delivery-level customs cost. May be null |
lines | ✅ | The lines this approval covered |
⚠ shipperType tells you whether this was a purchase or an internal movement. A delivery from another store is
goods moving inside the customer's own estate; treating it as a supplier receipt will overstate purchases. Branch on
shipperType before booking anything financial.
The lines
| Field | Required | Kind | Meaning |
|---|---|---|---|
lineId | ✅ | — | The line's id within the delivery |
itemId | ✅ | — | The item |
expectedQuantity | ✅ | Level | What the delivery note said |
approvedQuantity | ✅ | Level | What is accepted, as it now stands |
deltaQuantity | ✅ | Delta | The change in approved quantity since this line was last published |
unitPrice | ✅ | Level | Price per unit. May be null — meaning unpriced, not free |
currencyCode | ✅ | — | Three-letter currency of unitPrice. May be null |
reasonCodes | ➖ | Level | The line's complete current set of reasons, each with a signed quantity |
orderId | ➖ | — | The purchase order |
orderLineId | ➖ | — | The purchase order line |
containers | ➖ | — | The line's container chain, innermost first — its own container, then each enclosing one outwards |
batchNumber | ➖ | — | Batch, if the shipper supplied one |
bestBeforeDate | ➖ | — | Best-before date, if supplied |
expirationDate | ➖ | — | Expiration date, if supplied |
serialNumbers | ➖ | — | Serial numbers as expected — not the accepted subset |
🛑 deltaQuantity is a change; approvedQuantity and reasonCodes are totals. Adding up reason code quantities
across events double-counts every correction. The rules and a worked example are in
Delta versus level — read it before writing a consumer.
Message attributes
Set on every message, and usable for server-side filtering so a subscriber only receives what it cares about:
| Attribute | Value |
|---|---|
Tenant-Id | The tenant |
Business-Unit-Id | The receiving store |
Delivery-Id | Same as uniqueId in the payload |
Shipper-Id | Who sent the goods |
Shipper-Type | businessPartnerId or businessUnitId |
Correlation-Id | Traces back to the request that caused this. Your own value if you supplied one on import |
Filtering on Business-Unit-Id is the usual way to feed a per-store integration, and on Shipper-Type to separate
supplier receipts from inter-store movements.
Consuming the events
One delivery, many events
🛑 An event describes one approval, not one delivery. A five-line delivery approved line by line produces five events, each carrying only the lines that approval covered. A consumer that treats the first event as the delivery's complete content will act on a fifth of it.
Accumulate by uniqueId, and expect more events for a delivery you have already seen — including days later, when a
correction is recorded.
Duplicates are possible
Delivery is at least once. The same event can arrive more than once, and a consumer must be idempotent.
A safe deduplication key is uniqueId + submittedAt for the event, or uniqueId + lineId + submittedAt at
line level. A redelivery of the same approval carries identical values; a genuine new approval of the same line has a
later submittedAt.
⚠ Do not deduplicate on uniqueId + lineId alone. That is exactly the pair a legitimate correction reuses, so you
would discard the correction.
Do not rely on arrival order
⚠ External subscribers get no ordering guarantee. Events for the same delivery can arrive out of order, and a correction can overtake the approval it corrects.
Order by submittedAt within a uniqueId. Two practical consequences:
- If you keep a current approved quantity per line, ignore an event whose
submittedAtis older than the one you have recorded. Applying it would revert a correction. - If you accumulate
deltaQuantity, order matters less — the sum is the same — but you must still not apply the same event twice, so deduplication is not optional.
Event time versus expected time
deliveryDate and submittedAt answer different questions:
| Use | For |
|---|---|
submittedAt | Sequencing events, and "when did we learn this?" |
deliveryDate | Accounting period, supplier performance, "when were the goods due?" |
🛑 deliveryDate can fall on either side of submittedAt, by any amount. It is an expectation recorded by whoever
sent the delivery note, not an observation:
- Earlier — goods received on Friday and approved on Monday, or a paper note entered days late.
- Later — goods that arrived ahead of schedule and were approved before the date the note promised.
Only one bound exists anywhere in the service: a delivery created in the store, header-first, is rejected if its date is more than 14 days in the past. There is no upper bound, and GS1 imports are not bounded at all — an imported note carries whatever date the sending system put on it.
So never order events by deliveryDate, never assume it precedes submittedAt, and never assume the two fall in the
same reporting period.
New fields mean a new version
The schema does not permit undeclared fields, so a new field cannot appear on v1. If Goods Received needs to publish
something new, it will publish a new version and the catalogue will list it. You will not silently start receiving
fields you have not seen.
⚠ Reason code ids are open, and enums may grow. Reason codes are the customer's own values, so treat them as opaque
strings and never switch exhaustively on them. Handle a shipperType you do not recognise by falling through to a safe
default rather than rejecting the message.
Nullable is not the same as absent
unitPrice and currencyCode are always present on a line but may be null — the source system did not supply a
price. That is not the same as a price of zero.
🛑 Do not coerce a null price to zero for anything financial. A null price means the delivery carries no cost information for that line and the cost has to come from somewhere else — a purchase order, a price list, or the item master. Booking it as free goods understates cost of goods sold.
The optional fields marked ➖ above may be absent or null. Assume any of them can be missing on any line.