This guide shows you how to consume ProductUpsertEvent events end to end:
- Receive the event.
- Recognize whether the product belongs to a group of related products.
- Map the transformed attributes and the product type onto your channel's product model.
- Create or update the product.
- Report the product status back, so that the offer waiting on it can go live.
A product describes what is sold. An offer is a seller's sellable proposition for that product, and it cannot go live before the product exists on the channel. This workflow covers the case where the product is not on the channel yet and the channel requires it to exist before an offer can attach to it.
The channel usually has the last word on whether a product is acceptable, sometimes immediately and sometimes after a review. The status and the diagnostics you report here are what turn a rejected product into something the seller can fix.
Read the Catalog flow first for the definitions this guide builds on: product against offer, the lifecycle states, and the feedback loop.
Handle ProductUpsertEvent when Mirakl Connect asks you to create or correct a product on the channel. Mirakl Connect sends it while the product is not available on the channel, which means while the product_status you last reported is PRODUCT_DOES_NOT_EXIST, PRODUCT_PENDING_APPROVAL, or a product-level ACTION_REQUIRED, and the offer was never active.
Four properties decide whether you receive it at all:
- It requires a registered taxonomy. Mirakl Connect sends a product only after it transforms the product against the channel's product types, so the taxonomy must be registered first. The
product_type_idand every attribute in the event arrive already expressed in the channel's own vocabulary. - Mirakl gates it, for the channel. Registering the taxonomy is not enough on its own. Mirakl must activate that taxonomy in the Catalog Transformer and train the model on it, and that step is not self service. Read Activation by Mirakl.
- Catalog Transformer gates it, for each seller. Read Product creation requires Catalog Transformer below. For sellers without the capability, Mirakl Connect never sends this event, whatever you report as their product status.
- Attempts are periodic, not instant. Mirakl Connect sweeps for products that need creation on its own cadence. It re-sends a product when that product's transformed data changes, so a corrected product comes back to you and the seller does not have to do anything else.
The product status drives this event while the offer was never active. Once you report OFFER_ACTIVE for the item, the event stops for good.
The event is not sent for a product you reported as PRODUCT_CREATED or PRODUCT_REFUSED.
ProductUpsertEvent is the only catalog event that Mirakl Connect gates on a paid capability. Mirakl Connect emits it only for sellers who have Catalog Transformer enabled. For every other seller, Mirakl Connect never attempts product creation, and an offer can only go live only against a product that already exists on the channel.
The seller's Mirakl Connect plan decides whether that seller has the capability. Nothing that the channel or your connector configures decides it:
- Sellers on the free plan do not have Catalog Transformer. They cannot push a new product to a channel, so their offers can only attach to products already in the channel's catalog. Your connector never receives a ProductUpsertEvent for them.
- Sellers on a paid plan have Catalog Transformer. If the seller has activated Catalog Transformer for the channel, Mirakl Connect transforms their products into channel products against the channel's registered taxonomy.
Expect stores whose items never reach the product creation stage, and treat that as normal rather than as a stalled synchronization.
Mirakl Connect emits one event for each product. A transport delivers it. Your connector validates it, deduplicates it, assembles any group it belongs to, and creates the product on the channel. You then report the product's status.
Whether the channel accepts a product at once or reviews it first is the channel's behaviour. That behaviour decides how many times you report for one product.
Two properties of this flow shape everything below:
- Delivery is at-least-once, and ordered only for each
product_id. The same event can arrive twice, and events for different products can overtake one another. - A product's outcome is not always known when you submit it. A channel that reviews products returns its verdict later, which makes the reporting iterative rather than a single call.
You receive PRODUCT_UPSERT on the queue or the topic your subscription delivers to. For how to set one up, read Receiving events.
{
"id": "01jt31mw7wy3x4zs55kawgg1xe",
"time": "2024-05-20T10:15:30.500Z",
"type": "PRODUCT_UPSERT",
"source": "01jv2a4c8xk3m9p1r5t7y2w4qs",
"sequenceType": "PROGRESS",
"sequence": 1,
"sequenceSize": 3,
"data": {
"channel_id": "001",
"store_id": "2005",
"product_id": "SKU_123456",
"product_type_id": "Cell Phones",
"attributes": [
{ "id": "product_name", "value": "iPhone 14" },
{ "id": "description", "value": "Smartphone 128 GB", "locale": "en_GB" },
{ "id": "description", "value": "Smartphone 128 Go", "locale": "fr_FR" },
{ "id": "storage", "value": "128 GB" },
{ "id": "color", "value": "red" }
],
"stock": {
"action_type": "UPDATE",
"breakdown": [
{ "type": "SELLER_WAREHOUSE", "warehouse": { "id": "WH_PARIS" }, "quantity": 500 }
]
},
"standard_price": {
"action_type": "UPDATE",
"price": { "currency": "EUR", "amount": 899.99 }
},
"discount_price": {
"action_type": "UPDATE",
"price": { "currency": "EUR", "amount": 859.99 },
"start_date": "2024-05-20T10:15:30.500Z",
"end_date": "2024-06-20T10:15:30.500Z"
},
"offer_attributes": [
{ "type": "TEXT", "id": "label", "value": "iPhone 14 - Good Condition" }
],
"identifiers": [
{ "type": "GTIN", "value": "1234567890123" }
]
}
}Key fields in data:
channel_idandstore_id: the channel and the seller store this product belongs to.product_id: the seller's product reference, also called the SKU. It is the partition key for the ordering, and it is the identifier you report feedback against (step 5).product_type_id: the product type in the channel's format, resolved from the taxonomy you registered.attributes: the product's descriptive attributes, already transformed into the channel's format (step 4).stock,inventory,standard_price,discount_price,offer_attributes, andidentifiers: the offer data carried alongside the product, for channels that create a product and its offer together. Build againststock, which holds the quantity for each location.
The envelope carries four fields that appear on no other catalog event: source, sequenceType, sequence, and sequenceSize. They describe the group this product belongs to, see Step 3.
Acknowledge the message on your queue once you have stored the event. A message you leave unacknowledged returns to the queue, and you receive it again. An acknowledgement is not a statement that the product was created. You report that separately in step 5. Read Acknowledging a message.
Delivery is at-least-once, so the same event arrives more than once from time to time. Before you act, deduplicate on the envelope id, a ULID that identifies the event. Make a repeat delivery a no-op.
Then validate that you can build a product from the payload. Do not blindly retry a product type your channel does not recognize, or a required channel attribute that the transformation did not supply. Report it as ACTION_REQUIRED with a diagnostic in step 5, so the seller can correct the data and Mirakl Connect can send the product again.
Events that share a product_id are delivered in order relative to one another. There is no ordering guarantee across different products. Keep the processing of one product_id sequential, and process different products concurrently.
Some channels cannot accept a product on its own. They need every variant of a garment, or every product of a brand, in one submission. To support that, a channel can ask Mirakl Connect to group related products, and the envelope announces the grouping. The connector turns the grouping on in Catalog configuration. The envelope carries four fields:
source: the group's identifier. Every product in the same group carries the same value.sequence: this product's position in the group, counting from1.sequenceSize: how many products the group contains in total.sequenceType: alwaysPROGRESS.
The catalog configuration sets the grouping on one of two attributes:
VARIANT_GROUP_CODE: every product that shares a variant group code travels together, such as a garment's sizes and colours.BRAND: every product of the same brand in that store travels together.
When a channel does not group products, the four envelope fields carry no group and each event stands alone. Treat a source that is missing or empty as a product on its own.
When the channel does group products, note two behaviours:
- A group can contain a single product. A product with no value for the grouping attribute, or the only product of its kind in the store, arrives as
sequence: 1ofsequenceSize: 1. Treat a group of one as the ordinary case, not as a special one. - Mirakl Connect assembles the group from the whole store, not from what is eligible. As soon as one product in a group needs creation, Mirakl Connect pulls in every other live product that shares that grouping value, including products it would not have sent on their own. Expect events for products you have already created.
A group is not guaranteed to arrive complete. A product can be deleted between the moment Mirakl Connect counts a group and the moment it sends the group, so you can receive fewer events than sequenceSize promises. Handling an unfinished group is the connector's responsibility.
Never make the creation of a product depend on the completion of its group. Set a bound on how long you wait for the remaining members, then submit what you have and report each product you received. A connector that blocks until sequence reaches sequenceSize waits forever on any group that lost a member.
Translate the event into your channel's product model. Two fields carry the work, and both arrive already expressed in the channel's own vocabulary, because the transformation against your registered taxonomy happened before Mirakl Connect sent the event:
product_type_idis the channel's product type identifier, not a Mirakl Connect category. Use it as it is.attributesis a flat list of{ id, value, locale }. Theidis the attribute identifier on the channel, and thevalueis the value in the format the channel expects.
Do not derive or map again what the transformation already resolved. Your work is to carry the attributes onto the channel, and to validate that the attributes the channel requires are present. A required attribute that the transformation could not supply is the most common reason a product cannot be built, and to report it in step 5 is what gets it fixed.
The same attribute id can appear more than once, once for each locale. Mirakl Connect sends a localized attribute as one entry for each locale, each entry with the same id and a different locale. The description attribute in the example above shows this. The locale property is present only on localized attributes, in the format <ISO-639> or <ISO-639>_<ISO-3166>.
Key the attributes by id and locale. A connector that collapses the list into a map keyed on id alone keeps one arbitrary translation and silently drops the others.
The event also carries the offer's stock, inventory, prices, offer_attributes, and identifiers. Use them here if your channel creates a product and its offer in one operation. Each price and stock field carries its own action_type, exactly as on the other catalog events:
UPDATEsets the value.DELETEclears it.IGNOREleaves it unchanged.
Once you have acted, report the outcome for each store catalog item with the updateStoreCatalogItems operation. This is what advances the lifecycle, because Mirakl Connect acts on the product_status you report.
POST https://miraklconnect.com/api/channel-platform/v1/channel-catalog/001/store-catalog-items/2005Report:
product_status:PRODUCT_CREATEDonce the product exists on the channel.PRODUCT_PENDING_APPROVALwhile the channel reviews it.PRODUCT_REFUSEDif the channel refused it.ACTION_REQUIREDif something must be fixed before the channel can create it.
There is no pending value for work you have submitted but have no outcome for. Leave the item out of the call, and report it once its result lands.
product_diagnostics: the product's currently active problems. Each one is amessagethat a human can read, with an optionalchannel_attribute_idthat points at the attribute at fault. The list is full-replace: send the complete set every time, and send an empty list to clear it.
Report offer_status: OFFER_DOES_NOT_EXIST in the same call as PRODUCT_CREATED. Creating the product does not ask for an offer. PRODUCT_CREATED only releases an offer that was already waiting for its product. If you never reported the offer status, the item lands in a state where price and stock events keep flowing and no offer is ever created. The connector looks healthy, and the seller's listing never appears.
There is one exception. If you reported PRODUCT_DOES_NOT_EXIST on an earlier pass, that parked the offer as waiting, and PRODUCT_CREATED alone does release it. Reporting both statuses together is correct either way. Read What your report triggers.
Report a created product whose offer is still missing like this:
{
"store_catalog_items": [
{
"id": "SKU_123456",
"product_status": "PRODUCT_CREATED",
"product_diagnostics": [],
"offer_status": "OFFER_DOES_NOT_EXIST",
"offer_diagnostics": []
}
]
}The operation is asynchronous (202 Accepted) and applies to each item separately, so one blocked product does not affect the other items in the same request. For how to batch feedback across many items, read Report feedback.
On channels that review products before they publish them, to submit the product is not the end of the work. Report PRODUCT_PENDING_APPROVAL when the product goes into review, then report again when the verdict arrives:
- Approved: report
PRODUCT_CREATED, together with the offer status, as in step 5. - Refused: report
PRODUCT_REFUSEDwith aproduct_diagnosticsmessage that explains why. This status is terminal. Mirakl Connect stops the creation attempts and does not retry on its own, and the offer status stays frozen at its previous value. The seller sees the product as rejected, with your diagnostic as the reason.
While a product is PRODUCT_PENDING_APPROVAL, Mirakl Connect sends it again if its data changes, and otherwise waits for you. It leaves the offer status untouched, so an offer you already reported as active keeps selling and keeps receiving price and stock updates.
What it looks like: the connector creates the product on the channel, reports product_status: PRODUCT_CREATED, and waits for an offer event to build the offer.
The failure: no offer event ever arrives. PRODUCT_CREATED unblocks an offer that was already waiting for its product. It does not put an offer into that state, and it is not a request to create one. Because price and stock events keep flowing for the item, nothing looks broken, and the seller sees an offer that never appears.
The fix: report offer_status: OFFER_DOES_NOT_EXIST in the same call as the product status. It is the only status that asks Mirakl Connect for a creation attempt. Treat "the product now exists" and "the offer is still missing" as two facts that you must both state.
What it looks like: the connector buffers a group and submits it only when it has received sequenceSize events for that source, with no bound on the wait.
The failure: a group whose member was deleted between the counting and the sending never reaches its promised size, so the connector never flushes the buffer. Every product in the group stays uncreated, and because the connector never submitted the products, there is no status to report. The items simply go quiet, and nothing marks them as failed.
The fix: set a bound on the wait. Submit what you have when the bound expires, report each product you received, and treat an incomplete group as an expected condition rather than as an error. Mirakl Connect sends a product again when its data changes, so a member that was genuinely missing comes back.
What it looks like: the connector assumes that the first event it sees for a new item is a product event, and it treats a price and stock event for an unknown product as an error or as an out-of-order delivery.
The failure: Mirakl Connect enters the workflow as late as it can. The first event for a newly selected product is normally a PriceStockUpsertEvent, sent on the assumption that the offer is already live. A connector that rejects it stalls before it starts. So does a connector that waits for a product event, because that event does not come until the connector reports the product as missing.
The fix: treat the price and stock event as the entry point, and answer it with the truth about the item. Report PRODUCT_DOES_NOT_EXIST when the product is absent, which is what asks for the product event described here. Read How synchronization begins.
What it looks like: the handler reads attributes into a map keyed on id, so each later entry overwrites the earlier one.
The failure: a product localized into several languages keeps whichever translation came last and loses the others. The connector creates the product with an arbitrary locale's text. The channel can reject it outright, or publish it in the wrong language for its market. Nothing in the payload looked malformed, so no error is reported anywhere.
The fix: key the attributes by id and locale. Entries without a locale are not localized and appear once. Localized entries repeat the same id for every locale the seller provides.
What it looks like: after it reports PRODUCT_REFUSED, the connector keeps submitting the product to the channel, or reports the status again on a schedule and expects Mirakl Connect to retry.
The failure: PRODUCT_REFUSED is terminal by design. Mirakl Connect stops the creation attempts and freezes the offer status, so nothing you report moves the item on its own. Submitting the product again spends channel quota on a product the channel has already ruled on.
The fix: report PRODUCT_REFUSED once, with a product_diagnostics message that tells the seller why the channel refused the product. A change to the product's data by the seller is what produces a fresh event. Until then, there is nothing to retry.
- Catalog flow: product against offer, how synchronization begins, and which event carries which stage.
- Create and update offers: how to build the offer once its product exists on the channel.
- Report feedback: every status value, the event each pair of statuses triggers, and how to batch the export and report the statuses and diagnostics at scale.
- Taxonomy: how to register the product types that this event's
product_type_idrefers to. - Receiving events: the transports, the ordering, and the delivery behaviour behind these events.