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 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

TopicPublished whenTypical subscriber
cnc.public.event.order-created.v1an order has been accepted and its store fulfillment createdan ERP or OMS that wants to know work has arrived in a store
cnc.public.event.order-status-changed.v1an order moves between statuses — the main eventanything 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

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

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).

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.

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.