Events published by Click and Collect
Click and Collect publishes to Google Cloud Pub/Sub. Subscribing is how an external system learns that an order has been created, has changed status, or is ready to be paid — without polling the API.
This page is the contract. Read it before writing a subscriber.
The topics
| Topic | Published when | Typical subscriber |
|---|---|---|
cnc.public.event.order-created.v1 | an order has been accepted and its store fulfillment created | an ERP or OMS that wants to know work has arrived in a store |
cnc.public.event.order-status-changed.v1 | an order moves between statuses — the main event | anything tracking order progress |
Orders enter Click and Collect from Customer Order rather than over a topic you publish to. See Receiving orders.
order-status-changed — the one most integrations need
The richest event, and the one to build against if you are only consuming one.
Payload
| Field | Type | Notes |
|---|---|---|
orderId | string | the customer order |
fulfillmentId | string | 🛑 the unit of work in a store — not the same as orderId. See below |
tenantId | string | |
businessUnitId | string | the store fulfilling this work |
previousStatus | string, optional | absent on the first transition |
newStatus | string | one of the order statuses |
changedBy | string, optional | |
changedAt | string | ISO 8601, always present. Order by this, not by arrival |
orderType | string, nullable | CUSTOMER_ORDER or STORE_TRANSFER — see order types |
orderLines[] | array, optional | orderLineId, itemId, pickedQuantity, status, and substitutions[] of itemIdentifier, itemId, itemName, quantity, unitOfMeasure |
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
orderIdare not duplicates. They may be two stores reporting on their own halves. Deduplicating onorderIdloses one of them. fulfillmentIdis the correlation key for everything about a single store's work — and it is also the ordering key (below).
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.
| Attribute | Value |
|---|---|
Tenant-Id | the tenant |
Business-Unit-Id | the fulfilling store |
Correlation-Id | traces one logical operation across services; empty string when absent |
Event-Type | ORDER_STATUS_CHANGED |
Order-Type | CUSTOMER_ORDER or STORE_TRANSFER |
Content-Type | application/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.
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.
Related
- Order statuses — the values
newStatustakes and what each means - Order types —
CUSTOMER_ORDERversusSTORE_TRANSFER