Skip to main content

Events published by Click and Collect

Click and Collect publishes to Google Cloud Pub/Sub. Subscribing is how an external system learns that a store has received picking work and how that work progresses, without polling the API.

This page is the contract. Read it before writing a subscriber.

The topics​

TopicPublished whenTypical subscriber
cnc.public.event.order-created.v1an order has been accepted and its store fulfillment createda system that plans around the store's work, such as a TMS
cnc.public.event.order-status-changed.v1an order moves between statuses — the main eventanything following the picking, such as a TMS waiting for the goods to be ready

⚠ An ERP or OMS follows the order through Customer Order's events, not these. The order also changes in Hii Retail outside the store's picking, and only Customer Order's events carry those changes too. Your own updates are confirmed by the sync response rather than sent back (what is published). Each carries the whole order, so keep the newest version; see Keep the newest version of the order. The topics here are for systems that work with the picking itself.

Orders enter Click and Collect from Customer Order rather than over a topic you publish to. See Receiving orders.

order-status-changed — the main event​

The richest event here, and the one to build against if you consume only one of these topics.

Payload

FieldTypeNotes
orderIdstringthe customer order
fulfillmentIdstring🛑 the unit of work in a store — not the same as orderId. See below
tenantIdstring
businessUnitIdstringthe store fulfilling this work
previousStatusstring, optionalabsent on the first transition
newStatusstringone of the order statuses
changedBystring, optional
changedAtstringISO 8601, always present. Order by this, not by arrival
orderTypestring, nullableCUSTOMER_ORDER or STORE_TRANSFER — see order types
orderLines[]array, optionalorderLineId, itemId, pickedQuantity, status, and substitutions[] of itemIdentifier, itemId, itemName, quantity, unitOfMeasure, and optional unitPrice (the substitute's regular sales price in this store), taxableGroupId and itemCategoryId

How to read an order-status-changed event. orderId is what the customer bought and several stores can share it. fulfillmentId is this store's share: correlate, deduplicate and order on it. newStatus is an open set, so skip a value you do not know. changedAt is when the change happened: order by it, never by arrival. orderType tells customer orders from store transfers, so a subscriber can keep to the type it handles. Deduplicate on fulfillmentId, newStatus and changedAt together.

Order id and fulfillment id are different things​

🛑 One customer order can be fulfilled by more than one store. orderId identifies what the customer bought; fulfillmentId identifies one store's share of picking it.

Consequences, and both bite:

  • Two events carrying the same orderId are not duplicates. They may be two stores reporting on their own halves. Deduplicating on orderId loses one of them.
  • fulfillmentId is the correlation key for everything about a single store's work — and it is also the ordering key (below).

One customer order can be picked by two stores. Order C3R8W5Q is split: the Stockholm store picks the milk and bananas, and the Uppsala store picks the baked beans. Each store's share is a fulfillment with its own fulfillmentId. Both stores publish a PICKED event with the same orderId, and they are not duplicates: key on fulfillmentId.

Message attributes​

Set on every order-status-changed message. Attributes can be used in a Pub/Sub subscription filter, so you can subscribe to a subset without receiving and discarding the rest.

AttributeValue
Tenant-Idthe tenant
Business-Unit-Idthe fulfilling store
Correlation-Idtraces one logical operation across services; empty string when absent
Event-TypeORDER_STATUS_CHANGED
Order-TypeCUSTOMER_ORDER or STORE_TRANSFER
Content-Typeapplication/json

⚠ Order-Type is always present and always one of the two values. An unrecognised or missing value on the way in is normalised to CUSTOMER_ORDER rather than passed through, so a filter on attributes.Order-Type = "STORE_TRANSFER" is safe.

Consuming the events​

The part most integrations get wrong. Each item below has cost somebody a defect.

Delivery is at-least-once — you will receive duplicates​

Pub/Sub redelivers on any unacknowledged message, including ones your service actually processed but failed to ack in time. Make your handler idempotent.

Deduplicate on fulfillmentId + newStatus + changedAt, not on orderId, and not on the Pub/Sub message id — a redelivery reuses the message id, but a republish does not.

order-status-changed is ordered; the others are not​

order-status-changed is published with an ordering key of fulfillmentId, and the subscription can be created with message ordering enabled. Within one store fulfillment, statuses arrive in the order they happened.

🛑 That guarantee is per fulfillment, and only for this topic. It says nothing about:

  • two fulfillments of the same order — they are separate keys and interleave freely;
  • the other topics, which set no ordering key;
  • a replay from a dead-letter topic, which re-injects an old message after newer ones have landed.

So still order by changedAt rather than by arrival, and ignore a transition older than the one you have already stored. Ordering reduces how often you need that; it does not remove the need.

Ordering holds within one fulfillment only. Stockholm's share goes IN_PROGRESS at 10:02 and PICKED at 10:18, and Uppsala's at 10:05 and 10:31; each arrives in order, but the two interleave. A replay from a dead-letter topic can bring Stockholm's IN_PROGRESS of 10:02 back after its PICKED: compare changedAt and ignore a transition older than the one you have stored.

Handle values you do not recognise​

Statuses and order types are open sets. A new order status can be added without a new topic version, and your subscriber must not fail on one it has not seen. Log it and skip it; do not throw, and do not nack — a nack on an unrecognised value just redelivers it forever until it dead-letters.

Event time versus processing time​

changedAt is when the change happened. It is not when you received the message, and the two can differ by a lot after an incident or a replay. Persist changedAt, order by it, and never substitute your own clock — a redelivered event stamped on arrival always looks newer than what you have stored, which defeats the comparison entirely.

Optional fields really are optional​

previousStatus, changedBy, orderType and orderLines may all be absent. Always present: orderId, fulfillmentId, tenantId, businessUnitId, newStatus and changedAt. Code defensively about the rest rather than assuming a full payload.

⚠ changedAt is guaranteed, and the ordering advice above depends on it. Every publishing path sets it. If you ever receive an event without one, treat it as a defect worth reporting rather than designing a fallback around — substituting your own clock is precisely the failure the ordering rule exists to prevent.

Receiving orders​

Click and Collect does not accept orders directly. An order reaches it from Customer Order, which owns the customer's order and sends Click and Collect the share of it that a particular store has to pick — a fulfillment. Click and Collect creates the order and its lines from that, then publishes order-created.

So the integration point for a webshop or an OMS is Customer Order, not this service. What you send there, and in what shape, is Customer Order's contract.

What arrives here is deliberately narrow: the store's work. Customer identity, delivery choices and payment stay with the systems that own them — see customer information.