This guide shows you how to consume OFFER_UPSERT events end to end:
- Receive the event.
- Deduplicate and validate it.
- Map it to your channel's offer model.
- Create or update the offer.
- Report the resulting status back, so that Mirakl Connect knows what to send next.
Follow it, and a seller's offer moves from "not on the channel" to live, or to a blocked state that the seller can understand and fix.
A seller's catalog is worth something only once buyers can act on it. An offer is the sellable listing that puts one of a seller's products up for sale under a store on a channel. It carries the pricing and the available stock that a buyer sees at the moment they decide to purchase.
OFFER_UPSERT is how a seller's assortment reaches the channel. When Mirakl Connect decides that an offer must be created or updated on the channel, it sends you this event. Your connector is what turns the event into a real listing.
For the seller, the experience is simple when it works and frustrating when it does not. A seller enables a channel, selects products, sets prices and stock, and expects those offers to appear for sale. Every OFFER_UPSERT you handle correctly is one more offer live and earning. Every event you drop, apply twice, or fail silently is an offer that never appears, or that appears wrong, and the seller gets no signal about why. The status you report back is what closes that gap. It is how a stuck offer becomes a fixable problem instead of a silent gap in the seller's storefront.
Read the Catalog flow first for the definitions this guide builds on: offer against product, the lifecycle states, and the feedback channel. The Concepts and glossary covers the standard price, the discount price, the stock breakdown, and the SKU that keys an offer.
Handle OFFER_UPSERT when Mirakl Connect asks you to create or update an offer definition on the channel. The event's use_case field tells you which one:
OFFER_CREATION: the offer does not exist on the channel yet, so build it. You receive this use case only once you have reported the offer asOFFER_DOES_NOT_EXIST. That status is what asks Mirakl Connect for a creation attempt, and an offer whose status you never reported gets none. Read Report feedback.OFFER_UPDATE: the definition of an already active offer has changed. The change can affect its offer attributes, its identifiers, or the configuration that shapes them. You receive this use case only for offers you have reported asOFFER_ACTIVE.
The channel's catalog configuration gates OFFER_UPSERT. Mirakl Connect emits it only for the offer use cases a channel has declared it supports. If a channel's configuration does not enable offer creation, or offer update, you do not receive these events for it. Enabling that configuration belongs to the Catalog configuration step.
When not to use this. OFFER_UPSERT never carries the steady-state price and stock of a live offer. Those arrive as PRICE_STOCK_UPSERT, which you handle with Sync price and stock. Reporting an offer as OFFER_ACTIVE stops the offer creations for it. It does not stop OFFER_UPSERT altogether, because OFFER_UPDATE applies from that point on.
Creating the underlying product on the channel is also out of scope here. A separate product event drives it, covered in Create or update products, and the product is a prerequisite for an offer to go live. Read Handle unknown products.
Mirakl Connect emits the event. A transport delivers it. Your connector validates it, deduplicates it, maps it, and applies it on the channel. You then report the outcome.
The status you report drives what Mirakl Connect does next. The whole flow is a feedback loop, not a single delivery.
Two properties of this flow shape everything below:
- Delivery is at-least-once, so the same event can arrive more than once.
- There is no synchronous acknowledgement of success. To acknowledge the message on the queue confirms receipt only. You communicate whether the offer went live separately, through the feedback operation.
You receive OFFER_UPSERT on the queue or the topic your subscription delivers to: AWS SQS, Google Pub/Sub, or Azure Service Bus. The body is identical on all three, and it arrives in the standard event envelope: id, type, time, and a data object with the offer payload. For how to set one up, read Receiving events.
{
"id": "01jt31mw7wy3x4zs55kawgg1xe",
"time": "2024-05-20T10:15:30.500Z",
"type": "OFFER_UPSERT",
"data": {
"use_case": "OFFER_CREATION",
"channel_id": "001",
"store_id": "2005",
"product_id": "SKU_123456",
"inventory": {
"action_type": "UPDATE",
"quantity": 500,
"delivery_partner": { "name": "__self__" }
},
"stock": {
"action_type": "UPDATE",
"breakdown": [
{ "type": "SELLER_WAREHOUSE", "warehouse": { "id": "WH_PARIS" }, "quantity": 300 },
{ "type": "SELLER_WAREHOUSE", "warehouse": { "id": "WH_LYON" }, "quantity": 200 }
]
},
"standard_price": {
"action_type": "UPDATE",
"price": { "currency": "EUR", "amount": 29.99 }
},
"discount_price": {
"action_type": "UPDATE",
"price": { "currency": "EUR", "amount": 24.99 },
"start_date": "2024-05-20T10:15:30.500Z",
"end_date": "2024-06-20T10:15:30.500Z"
},
"offer_attributes": [
{ "type": "TEXT", "id": "label", "value": "My first SKU" },
{ "type": "NUMERIC", "id": "warranty", "value": 12 },
{ "type": "LIST", "id": "color", "values": [ { "value": "red" }, { "value": "blue" } ] }
],
"identifiers": [
{ "type": "GTIN", "value": "1234567890123" }
]
}
}Key fields in data:
use_case:OFFER_CREATIONorOFFER_UPDATE.channel_idandstore_id: the channel and the seller store this offer belongs to.product_id: the product reference, also called the SKU. It identifies the offer within the store, it is the partition key for the ordering (step 3), and it is the identifier you report feedback against (step 6).standard_priceanddiscount_price: the two prices, each with anaction_typeand a{ currency, amount }value.stock: the quantity for each location, which is the model to build against.inventoryis the legacy aggregated form, kept for backward compatibility.offer_attributes: the channel-specific attributes, each one typedTEXT,NUMERIC,BOOLEAN, orLIST(step 4).identifiers: the standard product codes, such as a GTIN.
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 offer went live. You report that separately in step 6. 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. Record the ids you have processed. This small store is the most important piece of state your connector owns for correctness.
Then validate the shape of the payload, and reject early anything you cannot map, such as a missing required attribute for the channel or a price you cannot parse. Do not blindly retry a validation failure. Report it as a blocked offer in step 6, so that the seller can fix it.
Each field carries its own action_type:
UPDATEsets the value.DELETEclears it.IGNOREleaves it unchanged.
Apply the event as an upsert, and honour the action_type of each field. A single event can set the price and leave the stock untouched. Applying the same complete event twice must leave the offer in the same state as applying it once.
Events that share a product_id are delivered in order relative to one another. There is no ordering guarantee across different products. Events for two SKUs can arrive in any order, and often in parallel. For how the transports preserve the ordering for each object, read Ordering per destination.
In practice, keep the processing of one product_id sequential, so that a later event for a SKU does not overtake an earlier one. Process different SKUs concurrently. Per-SKU ordering, together with the idempotent upserts of step 2, is enough to converge on the correct state even when a duplicate or a retry slips in.
Translate the event's fields into your channel's offer model. Keep the distinction between the offer and the product clear. This event is about the offer: its pricing, its stock, its identifiers, and its offer attributes. It does not create the product, and the product must already exist on the channel (step 5).
Map the offer_attributes by their type. Each attribute has an id, which is the attribute key on the channel, and a value whose JSON shape depends on the type:
type | Value field | JSON shape |
|---|---|---|
TEXT | value | a string, such as "My first SKU" |
NUMERIC | value | a number, such as 12 |
BOOLEAN | value | a boolean, such as true |
LIST | values | an array of { "value": "..." } objects |
Map the entries of stock.breakdown by their type too. Each entry carries its own quantity:
SELLER_CENTRALIZED: no location.SELLER_WAREHOUSE: awarehouse.id.FULFILLMENT_PARTNER: apartner.id.
Mirakl Connect sends the event only when the offer carries every field that the use case lists in required. On an update, a field that the use case lists in neither required nor optional does not reach you (read What required and optional change from the Catalog Configuration guide).
Validate the other fields against the channel's rules, so that you catch a missing attribute before the channel rejects it.
An offer sells a product, and it cannot go live before that product exists on the channel. Product creation is a separate flow, covered in Create or update products. Its own ProductUpsertEvent drives it, and its own product_status reports it. Because the ordering is guaranteed only for each product_id, and because the two flows are independent, you can receive an OFFER_UPSERT for a product that is not yet created on the channel.
When that happens, do not drop the event and do not fail silently. Report the item's product_status as PRODUCT_DOES_NOT_EXIST (step 6), with a product diagnostic that explains that the product is not yet on the channel. That is the one blocked state Mirakl Connect resumes on its own. It parks the offer as waiting for its product, and the moment you report product_status: PRODUCT_CREATED, it sends a fresh OFFER_UPSERT so that the offer can go live.
Do not report an offer-level ACTION_REQUIRED for this case and then wait. That status makes the problem visible to the seller, but it does not queue another creation attempt. The offer stays parked until you report OFFER_DOES_NOT_EXIST again. The product status is what carries an automatic resume.
After you act, close the loop and report the outcome for each store catalog item with the updateStoreCatalogItems operation. This is what advances the offer lifecycle, because Mirakl Connect acts on the offer_status you report.
POST https://miraklconnect.com/api/channel-platform/v1/channel-catalog/001/store-catalog-items/2005Report:
offer_status: one ofOFFER_ACTIVE,OFFER_DOES_NOT_EXIST, orACTION_REQUIRED. ReportOFFER_ACTIVE, and Mirakl Connect stops the creations and switches toPRICE_STOCK_UPSERTplusOFFER_UPDATE. ReportACTION_REQUIRED, and it surfaces the problem to the seller but attempts no further creation. You get a new attempt when you reportOFFER_DOES_NOT_EXISTonce the blocker clears. What your report triggers specifies which combination of statuses triggers which event.offer_diagnostics: the offer'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 of active diagnostics every time, and send an empty list to clear them. Diagnostics never change a status, so send them alongside the status they describe.
The item's id is the product_id, the SKU from the event. When an offer is blocked, for example because the channel rejected it for a missing required attribute, report it like this:
{
"store_catalog_items": [
{
"id": "SKU_123456",
"offer_status": "ACTION_REQUIRED",
"offer_diagnostics": [
{
"message": "The required offer attribute \"condition\" is missing. Set it so the offer can be published.",
"channel_attribute_id": "condition"
}
]
}
]
}The operation is asynchronous (202 Accepted) and applies to each item separately, so one blocked offer does not affect the other items in the same request. An unknown store returns 404. A malformed body returns 400. Mirakl Connect silently ignores the products it does not know. For how to batch feedback efficiently across many items, read Report feedback.
One offer, start to finish. The channel's catalog configuration already enables offer creation, and the product SKU_123456 already exists on the channel.
1. The event arrives. Mirakl Connect sends an OFFER_UPSERT with use_case: OFFER_CREATION for store 2005 on channel 001:
{
"id": "01jt31mw7wy3x4zs55kawgg1xe",
"time": "2024-05-20T10:15:30.500Z",
"type": "OFFER_UPSERT",
"data": {
"use_case": "OFFER_CREATION",
"channel_id": "001",
"store_id": "2005",
"product_id": "SKU_123456",
"inventory": {
"action_type": "UPDATE",
"quantity": 500,
"delivery_partner": { "name": "__self__" }
},
"stock": {
"action_type": "UPDATE",
"breakdown": [
{ "type": "SELLER_WAREHOUSE", "warehouse": { "id": "WH_PARIS" }, "quantity": 300 },
{ "type": "SELLER_WAREHOUSE", "warehouse": { "id": "WH_LYON" }, "quantity": 200 }
]
},
"standard_price": {
"action_type": "UPDATE",
"price": { "currency": "EUR", "amount": 29.99 }
},
"discount_price": {
"action_type": "UPDATE",
"price": { "currency": "EUR", "amount": 24.99 },
"start_date": "2024-05-20T10:15:30.500Z",
"end_date": "2024-06-20T10:15:30.500Z"
},
"offer_attributes": [
{ "type": "TEXT", "id": "label", "value": "My first SKU" },
{ "type": "NUMERIC", "id": "warranty", "value": 12 },
{ "type": "LIST", "id": "color", "values": [ { "value": "red" }, { "value": "blue" } ] }
],
"identifiers": [
{ "type": "GTIN", "value": "1234567890123" }
]
}
}2. Your connector acts. In order:
- Acknowledge the message on the queue.
- Check the envelope
id01jt31mw7wy3x4zs55kawgg1xeagainst your deduplication store. It is new, so continue and record it. - Validate the payload:
SKU_123456exists on the channel, and every required offer attribute is present. - Map the offer into your channel's offer model, then create the offer on the channel. The offer carries a standard price of
29.99 EUR, a discount price of24.99 EURvalid for one month,500units split into300atWH_PARISand200atWH_LYON, the three offer attributes, and the GTIN. - The channel accepts the offer, and the offer goes live.
3. You report the outcome. Post the offer's new status against store 2005 on channel 001, keyed by the SKU:
POST https://miraklconnect.com/api/channel-platform/v1/channel-catalog/001/store-catalog-items/2005{
"store_catalog_items": [
{
"id": "SKU_123456",
"offer_status": "OFFER_ACTIVE",
"offer_diagnostics": []
}
]
}4. The lifecycle advances. Mirakl Connect records the offer as OFFER_ACTIVE. From now on it sends no more offer creations for this SKU. Ongoing price and stock changes arrive as PRICE_STOCK_UPSERT, which you handle with Sync price and stock. A later change to the offer's definition arrives as an OFFER_UPSERT with use_case: OFFER_UPDATE. The empty offer_diagnostics list clears any earlier problem, which confirms that the offer has none.
If a missing required attribute had blocked the offer instead, you would report offer_status: ACTION_REQUIRED with a diagnostic, as in step 6, so that the seller could see why. You would then report OFFER_DOES_NOT_EXIST once the data was corrected, to trigger a fresh attempt. If a product that is not on the channel had blocked it, you would report product_status: PRODUCT_DOES_NOT_EXIST instead, as in step 5, and Mirakl Connect would resume on its own.
What it looks like: the handler creates or increments something on the channel on every delivery, and assumes that each event is seen exactly once.
The failure: because delivery is at-least-once, an event delivered again produces a duplicate offer, a change applied twice, or a corrupted quantity. Duplicates surface intermittently, and they are hard to trace back to a repeat delivery.
The fix: deduplicate on the envelope id, and make the handler an idempotent upsert keyed on product_id that honours the action_type of each field. Applying the same event twice must equal applying it once.
What it looks like: the connector treats each event as a unique, guaranteed delta, one event for each change, and relies on receiving every event exactly once to stay consistent.
The failure: Mirakl Connect provides at-least-once delivery, not exactly-once, and it orders events only for each product_id. A design that assumes exactly-once delivery applies a change twice on a repeat delivery. A design that assumes global ordering misorders changes across SKUs. Neither problem shows up until production load.
The fix: treat every event as a full upsert of the state it carries, not as an incremental delta. Keep the processing of each product_id sequential, and let idempotency absorb the duplicates. For how the transports preserve the ordering for each object, read Ordering per destination.
What it looks like: the connector retries a single unprocessable event forever at the head of the queue, such as an attribute it cannot map or a payload that throws, and stalls every later event behind it. Because the ordering is per product_id, one poison event can hold up a whole partition of unrelated SKUs.
The failure: the offers of many products stop updating because the event of one product cannot be processed. The blockage is silent from the seller's point of view. Their other offers simply stop moving.
The fix: isolate the failing item instead of blocking the batch. Report that offer as ACTION_REQUIRED with a diagnostic, set it aside for reconciliation, and let the remaining events proceed. Never stake the progress of a whole partition on the one item you cannot handle.
What it looks like: when an offer cannot go live, the connector logs the error and moves on without reporting anything back, or it silently skips events that lack an expected field such as an identifier. The offer can be blocked by a missing required attribute, by a product that is not yet on the channel, or by a rejection from the channel.
The failure: Mirakl Connect never learns that the offer is blocked, so it keeps the item where it is. The seller sees an offer that never appears, with no explanation. The problem is invisible to everyone who could fix it.
The fix: always close the feedback loop. Report offer_status: ACTION_REQUIRED with an offer_diagnostics message the seller can act on, and reference the channel_attribute_id at fault where you can, so that the reason is visible. Then report OFFER_DOES_NOT_EXIST once the blocker clears, which is what asks for a new creation attempt. Reserve a silent skip for genuine no-ops, never for a blocked offer.
What it looks like: the connector reports what it knows about a product, most often product_status: PRODUCT_CREATED after it created the product on the channel, and then waits for an OFFER_UPSERT 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 ask for an offer to be created. Because PRICE_STOCK_UPSERT keeps flowing for the item, nothing looks broken, and the offer never appears for the seller.
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. For the full matrix, read What your report triggers.
- Catalog flow: offer against product, price and stock, the lifecycle states, and the feedback channel this guide relies on.
- Create or update products: how to create the product an offer needs, for the channels and the sellers where that step applies.
- Sync price and stock: how ongoing price and stock changes arrive once an offer is
OFFER_ACTIVE. - 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.
- Catalog configuration: the configuration that enables
OFFER_UPSERT. - Receiving events: the transports, the ordering, and the delivery behaviour behind these events.