Exchange sellers, offers, inventory, prices, and orders between commercetools and a multi-vendor marketplace service, in whichever direction your role requires.
This guide describes how sellers, offers, inventory, prices, and orders move between commercetools and an external marketplace service through a Connect Connector. That service can be a platform such as Mirakl, Marketplacer, or Convictional, a channel manager, or a service your organization runs itself. The approach is the same for all of them, because the commercetools side of the contract doesn't change.
Some related topics are covered in other guides.
| Topic | Guide |
|---|---|
| Product Selections, Stores, Channels, and assortment modeling with no external marketplace in the loop | Model assortments with Product Selections |
| Catalog enrichment and product data mastered in a PIM, including the bulk Import API path | Integrate product data |
| Allocation, fulfillment status, Delivery, Parcel, and tracking write-back owned by an order management system | Integrate an order management system |
| Sellers, offers, inventory, prices, and orders exchanged with a marketplace service | This guide |
The third row is a hard boundary. When an order management system (OMS) is part of your topology, it owns Delivery, Parcel, and tracking. The marketplace fulfillment application reads that data and forwards it to the marketplace, but never writes it. If two systems write the same Delivery and tracking data, the result is a data-integrity bug.
Before you begin, make sure you have the following:
- a commercetools Project
- access to Connect
- credentials for a marketplace test environment
- an agreed owner for each data domain, as described in Fix the role and the direction
Define the marketplace integration requirements
Marketplace integrations usually fail in the modeling, not in the transport. Record the following decisions before you select, configure, or build anything. Each one changes which applications you need and what they write.
- Marketplace service and API access: identify the service, the account environment, the credentials, and how the service delivers changes. Webhooks that the service calls map to a
serviceapplication. Endpoints you poll and batch feeds map to ajobapplication. - Seller count and growth: record how many sellers you have today and how many you expect. Published limits, not preference, decide whether a Store per seller is viable.
- Seller isolation: decide whether sellers need separate storefronts, separate Merchant Center permissions, or separate assortments. Isolation is the only reason to create a Store per seller.
- Same-SKU sellers: decide whether several sellers can sell the same SKU. This is the most consequential decision in the design, and Model sellers and offers covers it.
- Which entities synchronize: for seller records, offer content, inventory, prices, orders, and fulfillment status, record whether each one moves in, out, or not at all.
- Order direction: decide whether marketplace orders arrive in commercetools, whether commercetools Orders are routed out to sellers, or both.
- Onboarding and offboarding: decide what creates a seller, what suspends one, and what a departed seller leaves behind. Offboarding means deactivation, not deletion, for the reason given in Offboard a seller by deactivating it.
- Latency and drift tolerance: these set your reconciliation schedule and your alerting thresholds.
- Region, Project, and Stores: record these, along with any multi-currency, multi-market, or business-to-business requirements.
If a requirement doesn't fit one of these categories, record it as its own item rather than forcing it into one. Cross-border tax obligations, seller-specific shipping contracts, and per-seller returns policies each change the design.
Fix the role and the direction
Your role and the direction of the data determine which applications you build. Decide both before any other architectural question, because everything that follows depends on the row you select.
| Role | Direction | Applications |
|---|---|---|
| Operator: third-party sellers' offers sell through your commercetools storefront | In: sellers, offers, inventory, and prices. Out: order lines and fulfillment status. | A service webhook or a job poll for seller and offer sync. An event application on Order creation that routes each seller's lines. An optional reconciliation job. |
| Seller or channel: your own catalog sells on someone else's marketplace | In: marketplace orders. Out: catalog, price, stock, shipment, and tracking. | An event application that exports catalog, price, and stock changes, or a job for batch feeds. A service webhook or a job that imports orders. An event application that pushes tracking back. |
| Both | Both | Both sets, as separate deployments. |
Name the source of truth per domain
Don't answer "which system is authoritative" once for the whole integration. Offer content, stock, price, orders, and the seller record are usually mastered on different sides, so a single global answer creates sync loops. Assign exactly one owner per data domain, and make the other side read-only for that domain.
| Data domain | Owner as operator | Owner as seller or channel |
|---|---|---|
| Seller record | The marketplace service | Not applicable |
| Offer content | The seller, through the marketplace service | commercetools |
| Price | The seller, through the marketplace service | commercetools |
| Inventory quantity | The seller, through the marketplace service | commercetools |
| Order capture | The marketplace service or commercetools checkout, depending on where the shopper buys | The marketplace service |
| Fulfillment status | The seller or the OMS | commercetools or the OMS |
Where an OMS is present, it owns the fulfillment row in both roles, and the marketplace application forwards what the OMS wrote.
Keep the two roles in separate deployments
If you operate in both roles, build two sets of applications and deploy them separately. Don't build one application with a mode flag. Combining the roles creates constraints that neither role has on its own.
- Keep Channel roles separate. An operator's seller Channels carry
InventorySupplyandProductDistribution. TheOrderExportdestination is a different Channel again, and a seller-role import destination carriesOrderImport. If one Channel carries all four roles, asyncInfoentry can't tell an export from an import. syncInfobecomes shared state. Both the outbound and the inbound application read it, so filter entries by Channel and role before you act on them. Otherwise an inbound import marker satisfies an outbound export check, and the Order is never routed.- Loop risk doubles. A domain that's inbound in one role and outbound in the other becomes a two-way domain, even though neither application is bidirectional on its own. Apply the self-change filter in both directions.
Model sellers and offers
Settle the modeling before the transport. If you choose the wrong transport, you rewrite one application. If you choose the wrong model, you splinter the catalog, and you can't undo that on data that already exists.
commercetools has no seller resource. Instead, you assemble a seller from primitives that already exist.
| Marketplace concept | commercetools primitive | Notes |
|---|---|---|
| Seller identity | A Channel keyed on the marketplace seller identifier | The key doubles as the idempotency key for every later write. |
| Seller capability | Channel roles InventorySupply, and ProductDistribution when the seller prices independently | Roles control what the Channel can scope. |
| Filterable seller attributes | Channel Custom Fields | Use these for anything a query predicate must filter on. |
| Seller profile payload | A Custom Object in a seller container keyed on the marketplace identifier | Creating a Custom Object with an existing container and key updates it, so this write is idempotent without a read. |
| Offer or listing | A Product or Product Variant | For a shared SKU, use one Product for all sellers. |
| Offer price | A Standalone Price carrying the seller's distribution Channel | The channel is what ties the price to the seller. |
| Offer stock | An Inventory Entry carrying the seller's supply Channel, with a key of <sku>-<sellerId> | The key is optional on the API and mandatory for the Import API and Merchant Center import, so set it from the start. |
| Seller assortment | A Product Selection | Use when a seller's visible assortment differs from the catalog. |
| Seller storefront isolation | A Store | Only when isolation is a requirement. |
| Marketplace order | An Order created through checkout or Order Import | Import creates an Order. It doesn't merge one. |
| Export or import correlation | SyncInfo against a Channel with the OrderExport or OrderImport role | An Order has no top-level external identifier field. |
| Commission, payout, settlement | Not modeled | See Define the marketplace integration requirements. |
Answer the same-SKU question first
The consequence of getting this wrong goes beyond performance. Search returns several near-identical products. Ratings and reviews split across those duplicates, and reporting counts the same item several times. Recommendation and merchandising features treat the duplicates as unrelated products. Merging them later means rewriting the catalog, the search index, and your historical reporting.
The per-seller difference lives on the price and the stock record, not on the Product:
- Every seller price is a Standalone Price that carries that seller's distribution Channel. A price written without a channel isn't a seller price at all. Diagnose common failures describes what you see when that happens.
- Every seller stock record is an Inventory Entry that carries that seller's supply Channel. An Inventory Entry written without a supply channel isn't scoped to a seller, and it behaves as global stock for the SKU.
Choose Channel-per-seller before Store-per-seller
Start with a Channel per seller. A Channel scopes both price and stock, which covers what most operators need, and no per-Project limit on Channels constrains a marketplace in practice.
languages, countries, distributionChannels, supplyChannels, and productSelections, and those fields deliver the isolation.Four published limits apply to a Store-based design, and two of them are decisive.
- A Project holds up to
300 000Stores. - A Store references up to
100supply Channels. - A Store references up to
100distribution Channels. - A Store holds up to
100Product Selections.
The two channel-count limits rule out one shared Store that lists every seller Channel. That design works during a pilot, then stops accepting sellers as soon as the count passes the limit.
Offboard a seller by deactivating it
Plan offboarding as deactivation from the beginning:
- Stop the inbound offer and inventory sync for that seller.
- Set the seller's Inventory Entries to zero, or remove them where no Order references them.
- Remove the seller's Standalone Prices, or remove the seller's Products from the Product Selections that publish them.
- Remove the Channel from any Store that references it.
- Mark the seller inactive on the Channel Custom Field and in the seller Custom Object, and leave the Channel in place.
Choose an integration path
Rule out the built-in capabilities first
commercetools models multi-party assortments without any external service. Check these capabilities before you plan a Connector, because a Connector adds another system to own, secure, monitor, and pay for.
| Built-in capability | Covers |
|---|---|
| Channels | Scoping stock and price to a supplier or a distribution partner, with roles that gate each use. |
| Stores | Isolating an assortment, its languages, its countries, and its channels behind a storefront boundary. |
| Product Selections | Defining which Products a Store sells, including per-Store price and availability behavior. |
| Standalone Prices | Holding several prices for one SKU, each scoped to a channel, a country, a currency, or a Customer Group. |
| Inventory | Holding a separate stock record per SKU and supply channel. |
Check for a deployable Connector
view_connectors scope to call the Search Connectors endpoint, and filter on the marketplace IntegrationType.GET https://connect.{region}.commercetools.com/connectors/search?integrationTypes=marketplace&private=false
private=false so that the results include Connectors that aren't yet assigned to your Project. For the host to use in each Region, see Hosts and authorization. Record the Connector key, the version, and the date you checked.Confirm a root connect.yaml before treating a listing as deployable
A listing in the Marketplaces category doesn't mean that Connect can deploy it. The category also lists partner-operated platforms that you configure entirely in the vendor's own dashboard, accelerators that deploy as cloud functions or serverless handlers on infrastructure you supply, and iPaaS middleware that connects the two systems from outside. None of these deploy through Connect, and none of them hand you a mapping that you own.
connect.yaml file exists at the root of the repository or of the Connect application, as described in Connector development. If the repository has no connect.yaml, it's a reference implementation you can read, not a Connector you can deploy.Two further checks protect against a common mistake:
- Recency doesn't mean deployability. For a given marketplace platform, the most recently updated artifact is often the cloud-function accelerator rather than the Connect application. If you compare candidates by last-commit date, you select the one that Connect can't deploy.
- Read how the repository describes its own runtime. If an artifact describes its endpoint as a cloud function, a serverless handler, or an on-premises web service, that's its deployment model, whatever category the listing sits in.
Work down the ladder
Choose the first path that satisfies the requirements:
- Configure an existing deployable Connector when one exists for your marketplace service and its seller, offer, order, and fulfillment coverage fits.
- Prove that a gap is not configuration before you fork. Field mappings, the Message selection, the publish-or-stage decision, and the Store and Channel keys are often settings rather than code.
- Fork a deployable Connector when the gap is genuine and the license permits it. Add only the missing behavior, keep the existing language and framework, and deploy the result as an Organization Connector. If you port the codebase to another language, you discard the mapping that was the reason to fork.
- Build the applications when no deployable Connector exists for your marketplace service.
--template values, so paths 3 and 4 are the realistic outcomes. Plan for a build unless your Connector search returns a deployable candidate.--template flag offers a different set that includes product-ingestion and fulfilment-integration. Neither set includes a marketplace template, and a template in the overview isn't guaranteed to be a valid --template value. Check the CLI help output for the values your installed version accepts, and treat the documented templates as application structures to study rather than as scaffold commands.Assemble the integration from Channels, Stores, Product Selections, Inventory, and Order Import. These are the primitives that the pattern depends on.
Design and configure the Connector
Keep every flow off the cart hot path
That leaves the two resource types an Extension can target in this domain:
- Don't register marketplace order export on an
orderExtension. An Extension runs synchronously inside the API request that creates the Order, so marketplace latency or an outage delays or blocks Order creation. Export belongs in aneventapplication driven by a Subscription. - Check cross-seller cart validation against the Extension time budget. Validating a multi-seller Cart is the one case where a synchronous Extension is arguable. By default, the platform allows 1 second to connect and 2 seconds to respond, and the documented target is to answer within 50 ms. See API Extension time limits before you commit to it, and keep that Extension separate from the sync applications.
Fan out inside the handler, never per seller
50 Subscriptions and up to 25 API Extensions. A design that registers one per seller stops accepting new sellers at that number, and you find out during onboarding rather than during development.Declare the applications in connect.yaml
connect.yaml at the root of the repository or at the root of the Connect application, as described in Connector development. The Verify Connector step of the validation pipeline checks that connect.yaml exists and that every application it declares has a matching source folder. A manifest that the deployment can't resolve fails that step.Keep each direction in its own application. If one application both receives seller offers and pushes orders out, it's harder to scale, harder to debug when one direction fails, and it can't use a different retry policy for each direction.
deployAs:
- name: marketplace-inbound
applicationType: service
endpoint: /marketplace
scripts:
postDeploy: npm run connector:post-deploy
preUndeploy: npm run connector:pre-undeploy
configuration:
standardConfiguration:
- key: MARKETPLACE_BASE_URL
description: Base URL of the marketplace service
required: true
- key: SELLER_CHANNEL_KEY_PREFIX
description: Prefix applied to seller Channel keys
required: true
- key: PUBLISH_MODE
description: Whether imported offers publish immediately or stage for review
required: true
- key: DEFAULT_CURRENCY
description: Fallback currency when an offer payload omits one
required: true
securedConfiguration:
- key: MARKETPLACE_API_KEY
description: API key for outbound marketplace calls
required: true
- key: INBOUND_WEBHOOK_SECRET
description: Shared secret used to authenticate inbound marketplace requests
required: true
- name: order-router
applicationType: event
endpoint: /orders
scripts:
postDeploy: npm run connector:post-deploy
preUndeploy: npm run connector:pre-undeploy
configuration:
standardConfiguration:
- key: MARKETPLACE_BASE_URL
description: Base URL of the marketplace service
required: true
- key: ORDER_EXPORT_CHANNEL_KEY
description: Key of the Channel carrying the OrderExport role
required: true
securedConfiguration:
- key: MARKETPLACE_API_KEY
description: API key for outbound marketplace calls
required: true
- name: reconciler
applicationType: job
endpoint: /reconcile
configuration:
standardConfiguration:
- key: RECONCILE_PAGE_SIZE
description: Page size for each reconciliation batch
required: true
inheritAs:
apiClient:
scopes:
- manage_subscriptions
- manage_products
- manage_orders
- manage_standalone_prices
- manage_key_value_documents
# Add manage_stores only when the design creates a Store per seller.
inheritAs.apiClient.scopes block tells Connect to generate a runtime API Client at installation. A Connector can instead accept pre-generated commercetools credentials through secured configuration, which is a common shape in existing marketplace accelerators. Prefer the generated client: it keeps permissions least-privilege and removes a manual credential-handling step from installation.securedConfiguration for credentials and shared secrets.Create the correlation Channels before the first order moves
OrderExport or OrderImport. Without that role, the action fails with an InvalidInput error after the marketplace has already created its order, which is the most expensive moment to fail.postDeploy, and make the script idempotent: fetch by key and update, rather than creating unconditionally. In preUndeploy, remove the Subscriptions, so that an undeployed Connector doesn't leave the platform delivering to an endpoint that no longer answers. Leave the Channels in place, because Orders reference them. For how a deployment behaves in each environment, see Deployment behavior and environments.The operator round trip
syncInfo before it pushes anything, because syncInfo is what turns a redelivery into a no-op.Implement the sync applications
Upsert by the marketplace identifier
One rule applies to every application: key each commercetools resource on a stable identifier from the marketplace, and write it as an upsert rather than a create. Duplicate-resource failures in this domain almost always trace back to a handler that created unconditionally.
| Marketplace identifier | commercetools key | Write behavior |
|---|---|---|
| Seller identifier | Channel key | Get by key, then create or update. |
| Seller identifier | Custom Object container and key | Creating a Custom Object with an existing container and key updates it. |
| Listing or SKU | Product key or the shared SKU | Get by key, then create or update. |
| SKU and seller | Standalone Price key, such as <sku>-<sellerId>-<currency> | Get by key, then create or update. |
| SKU and seller | Inventory Entry key, such as <sku>-<sellerId> | Get by key, then create or update. |
| Marketplace order identifier | Order orderNumber | Query first, then create only when absent. See the next paragraph. |
| Marketplace order identifier | SyncInfo.externalId against a role-bearing Channel | Read syncInfo and skip entries already recorded. |
orderNumber field must be unique across the Project, and you can't change the value once it's set. A second import with the same orderNumber is rejected rather than merged, and you can't correct a wrong key on Orders that already exist. Query Orders by orderNumber first, and import only when there's no match. Settle your key convention before the first import runs.Sync sellers
Authenticate every inbound request before it reaches any handler logic. The endpoint is reachable from the internet, and it creates Channels, Products, and Orders in your Project. Verify a shared secret, or fully validate a token including its signature, issuer, audience, and expiry. Keep the secret in secured configuration, and leave only a liveness route unauthenticated.
AuthorizationHeader Destination authentication on a Subscription or an Extension is the header that commercetools sends when it calls your endpoint. It doesn't authenticate a marketplace that calls you. An inbound webhook that relies on it looks configured but is unprotected.For each seller record:
- Get the Channel by the seller key. On a match, update it with the returned version. On no match, create it.
- Set the roles the seller needs:
InventorySupplyalways, andProductDistributionwhen the seller sets its own prices. - Write filterable seller attributes to Channel Custom Fields, so that predicates can select on them.
- Write the free-form seller profile to the Custom Object container, which needs no prior read.
- Where Store-per-seller applies, create or update the Store and reference the seller Channel from it.
The seller Channel and its roles must exist before you write any offer, price, or inventory for that seller, because both stock and price scope to it.
Sync offers, inventory, and prices
For each offer:
- Resolve the seller Channel by key. Stop and record a failure if it does not exist, rather than writing an unscoped price or stock record.
- Resolve the Product. For a shared SKU, find the existing Product rather than creating a second one.
- Write the price as a Standalone Price carrying the seller's distribution Channel.
- Write the stock as an Inventory Entry carrying the seller's supply Channel and a deterministic
key. - Apply the publish-or-stage decision you recorded. If you publish every imported offer without review, unreviewed seller content reaches the storefront.
- Handle delisting as an explicit action. A seller that stops carrying an item sends a removal or a zero quantity, and in batch contracts, omission from the next feed is also a delisting signal. An integration that only ever adds keeps selling items that no seller carries.
Three mapping details cause defects that are hard to trace:
- Money is an integer in minor units. Multiply, then round. A floating-point cast drops or inflates cents on every offer, with no error and no exception.
- Derive currency, locale, country, and Region. Don't hardcode them. A hardcoded value works for the first seller and the first market, then breaks for every other one.
- Localized fields follow the payload locale. If you derive them from a single default, the catalog is correct in one language and empty in the rest.
Import marketplace orders
This flow applies to the seller or channel role, where the marketplace captures the order and commercetools records it.
- Authenticate the caller and validate the payload. The platform accepts negative prices and negative quantities on an import, so validate those yourself rather than relying on an error.
- Query Orders by
orderNumberset to the marketplace order identifier. Import only when no match exists. - Build an OrderImportDraft. Set
orderNumberto the marketplace identifier, and settotalPriceexplicitly. commercetools doesn't calculatetotalPricefrom the Line Item and Custom Line Item totals, so an incorrect value produces a wrong Order with no error. - Set the per-line
supplyChannelanddistributionChannel, and record the marketplace line identifier in a per-line Custom Field so that later status updates can address a single line. - Choose the InventoryMode deliberately. The marketplace already committed the stock, so any mode other than
Nonedecrements it a second time. For the behavior of each inventory mode, see Inventory modes. - Record the correlation with Update SyncInfo against a Channel carrying the
OrderImportrole, whichpostDeploycreated.
store on the import draft, because it has a silent side effect. An import that references a Store filters localized strings by the Store languages, filters prices by its distributionChannels and countries, and filters inventory by its supplyChannels. Anything outside that configuration is dropped from the imported Order without an error. Two documented exceptions apply: a Line Item distributionChannel or supplyChannel that the Store doesn't define rejects the import instead, and an unmatched language falls back to the next language in the Project languages. Prices that carry no distribution channel are kept, which is a separate hazard described in Diagnose common failures.Order Import produces the Order Imported Message, not Order Created. Every downstream Subscription that reacts to an imported Order must include that Message type.
Route order lines to sellers
This flow applies to the operator role, where a shopper buys from several sellers in one Order.
- Subscribe to both
OrderCreatedandOrderImported. An order-routing application subscribed only toOrderCreatedsilently skips every imported Order, with no error and no failed delivery. Add the recurring-order Message where Orders generated on a schedule must also route. - Decode the envelope. The delivery carries base64-encoded data, so a handler that treats it as JSON crashes or logs unreadable output. Validate the Message type, and acknowledge anything this application doesn't act on.
- Get the Order by
resource.id. Don't map from the payload. Subscriptions deliver at least once with no ordering guarantee, so a payload can describe an older state. If a payload exceeds the queue size limit, it's omitted entirely and arrives withpayloadNotIncluded, which means your largest multi-seller Orders are the ones that fail. - Read
syncInfobefore you push anything. Skip every seller Channel already recorded there. This is the platform's own already-exported marker, and it survives a restart, which an in-process set does not. - Group Line Items by supply channel, or by whatever seller reference the Cart set at add-to-cart time. Build one payload per seller, carrying only that seller's lines.
- Record each success immediately. Write Update SyncInfo per seller Channel with the version you read. On a
409ConcurrentModification conflict, re-read the Order and re-evaluate which sellers still need a push. Partial failure is normal. If seller A succeeds and seller B fails, thesyncInfoentry for A must persist before the retry, so that the retry reaches only the missing seller instead of submitting to A twice. - Acknowledge deliberately. Acknowledge only once every seller is either recorded or deliberately left for redelivery. Unacknowledged messages are retained for 7 days, and push backoff applies after sustained negative acknowledgments. For details, see Event application behavior. Event applications and API Extensions use different response contracts, and the acknowledgment status codes are defined in Acknowledge deliberately. Don't reuse acknowledgment logic between the two application types.
Forward fulfillment status
Where no OMS is present, this application owns per-line state and Deliveries, and the same guide supplies the mechanism.
syncSource Custom Field or through the API Client that made them, or exit early when the Order already matches the state the marketplace reported.Reconcile drift
TemporaryError, and a shorter window on a ConfigurationError. After either window, notifications can be dropped. Marketplace services also throttle. Both leave gaps that no event-driven path recovers, so run a scheduled job application that compares the two sides and repairs the differences.- Route repairs through the same handlers as the event path. Repairs are then upserts by construction, and the reconciler can't develop its own divergent write behavior.
- Checkpoint after each page, so that an interrupted run resumes rather than restarting.
- Handle overlapping runs yourself. Connect doesn't document whether a scheduled run finishes before the next one starts, and a job has a 30-minute execution limit. Take a durable lock with a time-to-live longer than that limit, and release it at the end, so that two runs can't repair the same records at once.
- Size the reconciler against the job ceiling. A
jobapplication uses a maximum of 2 CPU and 4 GB memory, and Connect advises against job applications for bulk or batch operations that demand more extensive processing or high memory. - Re-push Orders with no matching
syncInfoentry through the routing handler. - Keep the initial migration separate. The first bulk load has a different volume, time budget, and failure tolerance from ongoing reconciliation. Running it through the reconciler makes both harder to reason about.
Test the applications
You can run these tests without a deployed Connector and without marketplace credentials. Mock the outbound boundary, then assert on what the code decided: which endpoint it called, what it sent, and what it did with the response.
Test the modeling and mapping behavior:
- A second seller for the same SKU adds a Standalone Price and an Inventory Entry, and creates no second Product.
- Every written price carries a distribution channel, and every written Inventory Entry carries a supply channel and a deterministic key.
- Money conversion is asserted on a value with non-zero cents, and on a value that rounds.
- Currency, locale, and country are read from the payload and from configuration, and no test passes with a hardcoded value.
- A seller payload that arrives before its Channel exists creates the Channel with its roles before any price or stock write.
- A delisting payload removes the offer rather than leaving it sellable.
Test the order behavior:
- A redelivered Order Created Message pushes nothing to the marketplace, because
syncInfoalready records every seller. - A multi-seller Order produces one payload per seller, and each payload carries only that seller's lines.
- A partial failure retries only the failed seller and does not resubmit the successful one.
- An Order Imported Message reaches the routing handler, rather than being filtered out with the Messages the application ignores.
- A duplicate marketplace order identifier imports once, and the second attempt is recognized by the query rather than by the rejection.
- An import draft sets
totalPriceexplicitly, and a draft carrying a negative price or quantity is rejected by your validation. - A
payloadNotIncludeddelivery still results in a complete push, because the handler fetched the Order.
Test the operational behavior:
- Running the deployment scripts more than once leaves one Subscription and one correlation Channel per role, with no duplicates.
- The undeploy script removes the Subscriptions and leaves the Channels.
- Reconciliation resumes from its checkpoint after an interrupted run, and a second concurrent run does not start while the lock is held.
- An inbound request without a valid signature is rejected before any write.
Verify and operate the integration
- Verify the seller. Send a seller payload and read the Channel by its key. The key matches the marketplace identifier, and the roles include
InventorySupply, plusProductDistributionwhere the seller prices independently. Send the same payload again and confirm that one Channel exists, not two. A seller that exists only as a Custom Object is an incomplete model, because stock and price have nothing to scope to. - Verify that the offer is sellable. The Product existing is not evidence. Add the SKU to a Cart in the seller's context, with the seller's distribution channel, and confirm that the Line Item resolves the seller's price. If the offer can't be added at the seller's price, it isn't synchronized, however successful the import log looks.
- Verify the shared SKU. Send an offer for the same SKU from a second seller. One Product exists, two Standalone Prices exist with different channels, and two Inventory Entries exist with different supply channels.
- Verify delisting. Send a removal or a zero quantity and confirm that the offer stops being sellable at that seller's price.
- Verify the order flow once. For the operator role, place a multi-seller Order and confirm one push per seller, each carrying only that seller's lines, and one
syncInfoentry per seller Channel. For the seller role, deliver a marketplace order and confirm one imported Order, an explicittotalPrice, and asyncInfoentry against theOrderImportChannel. - Verify redelivery. Deliver the same Order Message twice and confirm that the second delivery pushes nothing and creates nothing.
- Verify the fulfillment split. Advance one seller's lines and confirm that the marketplace receives status for that seller only, and that the other seller's lines are unchanged.
- Verify offboarding. Deactivate a seller through the procedure in Offboard a seller by deactivating it and confirm that its offers stop selling while its Orders remain intact.
syncInfo entry. That last number is the most direct measure of orders that never reached a seller. Use the marketplace seller identifier and the marketplace order identifier as the correlation keys across both systems.Keep secrets and personal data out of logs, and return a status rather than a stack trace from the inbound endpoint. A marketplace payload carries buyer names, addresses, and contact details, and an error response that echoes the payload publishes them.
Diagnose common failures
Several correct platform behaviors in this domain look like defects. Start from the symptom.
| Symptom | Likely cause | Resolution |
|---|---|---|
| One seller's price appears on another seller's storefront, and Store price filtering looks broken. | A price was written without a distribution channel, so it is not scoped to any seller. | Write every seller price with the seller's distribution channel. Find and remove the channel-less prices. |
| Stock is deducted across sellers, and the marketplace oversells. | An Inventory Entry was written without a supply channel, so it is not scoped to a seller and behaves as global stock for the SKU. | Write every Inventory Entry with the seller's supply channel and a deterministic key. |
| The storefront shows one blended stock number for a multi-seller SKU, and it lags behind reality. | Expected. availability on a Product Variant aggregates across supply channels and can lag real-time stock by a few seconds, and Order- and Cart-driven stock changes can take up to 10 seconds to appear. Direct Inventory API updates are strongly consistent. | Read per-channel availability for a seller-specific number, and do not build a live stock ledger on the aggregated field. |
| Search returns several near-identical products for one item, and ratings and reporting are split. | One Product was created per seller for a shared SKU. | Model one Product with per-seller prices and inventory. Merging existing duplicates means rewriting the catalog and the reporting. |
| Duplicate sellers, Products, or Orders appear after a webhook redelivery. | The handler creates unconditionally instead of getting by key first. | Upsert on the marketplace identifier, as described in Upsert by the marketplace identifier. |
| A repeat import of the same marketplace order is rejected. | Expected. orderNumber is unique across the Project, and Order Import creates rather than merges. | Query by orderNumber and import only when absent. Treat the rejection as confirmation, not as an error to retry. |
| An imported Order carries a wrong total, and nothing failed. | totalPrice was assumed to be calculated from the line totals, which it isn't. | Set totalPrice explicitly on the import draft, and assert it in a test. |
| An imported Order is missing prices, inventory, or translations. | Expected. The import draft set store, so prices, inventory, and localized strings outside that Store's channels, countries, and languages were filtered out. | Remove store from the draft, or add the seller's channels and languages to the Store. |
| Imported Orders are never routed, never redeemed, and never exported, with no error. | The Subscription covers OrderCreated only. Order Import produces the Order Imported Message. | Subscribe to both Message types. |
| Stock is deducted twice for one marketplace order. | The import used an inventory mode that reserves or tracks stock the marketplace already committed. | Set the inventory mode to None on imports where the marketplace owns the commitment. |
| A multi-seller Order is pushed twice after a redelivery. | The handler pushed before reading syncInfo. | Read syncInfo and skip recorded seller Channels before building any payload. |
A correlation write returns a 409 conflict once redelivery and reconciliation overlap. | Every seller in the loop writes updateSyncInfo to the same Order, so ConcurrentModification is the normal path rather than an edge case. | Re-read the Order, re-evaluate which sellers still need a push, and re-send with the current version rather than replaying the original actions. |
| The largest multi-seller Orders fail while small ones succeed. | The notification exceeded the queue size limit and arrived with payloadNotIncluded, and the handler mapped from the payload. | Get the Order by resource.id in every handler. |
| A stale offer or status overwrites a newer one. | The handler applied the delivery payload as a delta. Deliveries are at-least-once with no ordering guarantee. | Re-fetch by resource.id and converge on current state rather than replaying deltas. |
The correlation write fails with InvalidInput after the marketplace already created its order. | The Channel named in the Update SyncInfo action does not carry the OrderExport or OrderImport role. | Create the role-bearing Channels in postDeploy, before the first export or import runs. |
| A correlation query returns two Orders, or none. | The API does not enforce uniqueness on SyncInfo.externalId. | Reject anything other than exactly one match, and enforce uniqueness in the integration. |
| The handler crashes on a delivery, or logs unreadable output. | The transport envelope was not decoded before the payload was read. | Decode the base64 data, validate the Message type, and acknowledge types this application does not handle. |
| A handled message is redelivered without end, or a real failure disappears. | The acknowledgment status code does not match the outcome. | Follow the status mapping in Acknowledge deliberately. |
| Status ping-pongs between the two systems, and call volume climbs with no business cause. | A two-way domain has no self-change filter, so each system reacts to the other's write. | Filter your own writes by Custom Field or API Client, and exit early when the state already matches. |
| Cents are wrong on every offer by a small amount. | A floating-point value was cast to minor units instead of being multiplied and rounded. | Convert with integer arithmetic and cover it with a test on a value with non-zero cents. |
| The integration works for the first seller or market and breaks for the next. | Currency, locale, country, or Region was hardcoded. | Derive all four from the payload and from configuration. |
Deleting a seller Channel returns a ReferenceExists error, and the seller is half-removed. | Expected. You can't delete a Channel that an Inventory Entry, Line Item, Store, Price, Standalone Price, or Cart Discount gift Line Item references, and an Order Line Item holds that reference for as long as the Order exists. | Offboard by deactivating, as described in Offboard a seller by deactivating it. |
| Registering a Subscription or an Extension fails once the seller count grows. | The design registers one per seller, and it reached the per-Project limit. | Register one Subscription per Message type, and fan out to sellers inside the handler. |
| Assigning a seller Channel to a shared Store fails. | The Store reached its limit for supply channels or distribution channels. | Split the sellers across Stores, or move to Channel-only scoping without a shared Store. |
| Offers stop arriving, and nothing in the logs looks like an error. | The marketplace service is throttling the integration, and the responses are treated as ordinary retries. | Detect the throttling response, back off exponentially, and alert on sustained throttling rather than absorbing it. |
| Every platform call to the Connector returns a not-found response. | The served route does not match the endpoint declared in connect.yaml. | Align the declared endpoint with the mounted route, and register destinations with the endpoint path. |
| The deployment succeeds but registers no Subscription, Channel, or Type. | The scripts block is absent from connect.yaml, so postDeploy never runs. | Declare postDeploy and preUndeploy per application, and confirm that connect.yaml sits where Connector development specifies. |
| The first call after an idle period fails, and only in testing. | A sandbox deployment scaled to zero, and the cold start exceeded the caller's timeout. | Expected for that environment. Verify against a deployment that does not scale to zero. |
| Contract tests pass but production behaves differently under load. | The marketplace sandbox differs from production in rate limits, volume, and retention. | Validate throughput and retention against production limits rather than against the sandbox. |
| Unreviewed seller content appears on the storefront. | Imported offers publish immediately. | Set the publish-or-stage configuration to stage, and review before publishing. |
| The storefront keeps selling items no seller carries. | Delisting is not handled explicitly, so the integration only ever adds. | Handle removals, zero quantities, and omission from a batch feed as delisting signals. |