Skip to content

Create or update products

This guide shows you how to consume ProductUpsertEvent events end to end:

  1. Receive the event.
  2. Recognize whether the product belongs to a group of related products.
  3. Map the transformed attributes and the product type onto your channel's product model.
  4. Create or update the product.
  5. Report the product status back, so that the offer waiting on it can go live.

Business context

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.

When to use this

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_id and 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.

Product creation requires Catalog Transformer

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.

How it works

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.

ChannelMiddlewareMiraklChannelChannel ConnectorTransportMirakl ConnectChannelChannel ConnectorTransportMirakl ConnectA product needs creationThe offer creation followsalt[The channel creates it outright][The channel reviews it first][The product cannot be built]ProductUpsertEvent (one per product)Deliver event (at-least-once, ordered per product_id)Validate, deduplicate, assemble the groupCreate or update the product (product type + transformed attributes)Product createdupdateStoreCatalogItems (PRODUCT_CREATED + OFFER_DOES_NOT_EXIST)Submitted for reviewupdateStoreCatalogItems (PRODUCT_PENDING_APPROVAL)Approved, or refused with a reasonupdateStoreCatalogItems (PRODUCT_CREATED, or PRODUCT_REFUSED + diagnostics)Rejected, a required attribute is missing or invalidupdateStoreCatalogItems (ACTION_REQUIRED + product_diagnostics)
ChannelMiddlewareMiraklChannelChannel ConnectorTransportMirakl ConnectChannelChannel ConnectorTransportMirakl ConnectA product needs creationThe offer creation followsalt[The channel creates it outright][The channel reviews it first][The product cannot be built]ProductUpsertEvent (one per product)Deliver event (at-least-once, ordered per product_id)Validate, deduplicate, assemble the groupCreate or update the product (product type + transformed attributes)Product createdupdateStoreCatalogItems (PRODUCT_CREATED + OFFER_DOES_NOT_EXIST)Submitted for reviewupdateStoreCatalogItems (PRODUCT_PENDING_APPROVAL)Approved, or refused with a reasonupdateStoreCatalogItems (PRODUCT_CREATED, or PRODUCT_REFUSED + diagnostics)Rejected, a required attribute is missing or invalidupdateStoreCatalogItems (ACTION_REQUIRED + product_diagnostics)

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.

Step by step

1. Receive the event

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_id and store_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, and identifiers: the offer data carried alongside the product, for channels that create a product and its offer together. Build against stock, 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.

2. Deduplicate and validate

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.

3. Handle grouped products

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 from 1.
  • sequenceSize: how many products the group contains in total.
  • sequenceType: always PROGRESS.

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: 1 of sequenceSize: 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.

4. Map the product to the channel

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_id is the channel's product type identifier, not a Mirakl Connect category. Use it as it is.
  • attributes is a flat list of { id, value, locale }. The id is the attribute identifier on the channel, and the value is 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:

  • UPDATE sets the value.
  • DELETE clears it.
  • IGNORE leaves it unchanged.

5. Report the product status

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

Report:

  • product_status:

    • PRODUCT_CREATED once the product exists on the channel.
    • PRODUCT_PENDING_APPROVAL while the channel reviews it.
    • PRODUCT_REFUSED if the channel refused it.
    • ACTION_REQUIRED if 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 a message that a human can read, with an optional channel_attribute_id that 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.

6. Follow a review to its outcome

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_REFUSED with a product_diagnostics message 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.

Common mistakes

Reporting PRODUCT_CREATED without declaring the offer missing

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.

Blocking until a product group completes

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.

Expecting the product event to arrive first

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.

Collapsing localized attributes into one value for each id

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.

Retrying a product the channel refused

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_id refers to.
  • Receiving events: the transports, the ordering, and the delivery behaviour behind these events.