# Integrate a marketplace service Exchange sellers, offers, inventory, prices, and orders between commercetools and a multi-vendor marketplace service, in whichever direction your role requires. Learn more about integrations in the self-paced [Integration patterns](/learning-integrate-with-commercetools/integration-patterns/overview.md) module. In commercetools, "marketplace" means two different things. The **Connect marketplace** is the catalog where Connectors are listed for installation. A **marketplace business model** means selling third-party sellers' assortments through your own storefront, or selling your own assortment on someone else's marketplace. This guide covers the business model. To publish your own Connector on the Connect marketplace, see [Connect](/connect). 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](/guides/product-selections.md) | | Catalog enrichment and product data mastered in a PIM, including the bulk Import API path | [Integrate product data](/guides/product-data-integrations.md) | | Allocation, fulfillment status, Delivery, Parcel, and tracking write-back owned by an order management system | [Integrate an order management system](/guides/oms-integration.md) | | 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](/connect) - credentials for a marketplace test environment - an agreed owner for each data domain, as described in [Fix the role and the direction](/guides/marketplace-integration.md#fix-the-role-and-the-direction) For broader context on building integrations, see [Integration essentials](/learning-integrate-with-commercetools/integration-patterns/overview.md). ## 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 `service` application. Endpoints you poll and batch feeds map to a `job` application. - **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](/guides/marketplace-integration.md#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](/guides/marketplace-integration.md#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. Commission, payout, and settlement stay outside this integration. commercetools has no payout, commission, or settlement resource. A [Payment](/api/projects/payments.md) records the payment service provider, the payment method, the related transactions, and the current state of the Payment, and an external provider carries out the financial process itself. Keep the payout ledger in the marketplace service or with the provider. You can mirror commission values onto [Custom Fields](/api/projects/custom-fields.md) for reporting, but commercetools doesn't disburse funds against them. 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 `InventorySupply` and `ProductDistribution`. The `OrderExport` destination is a different Channel again, and a seller-role import destination carries `OrderImport`. If one Channel carries all four roles, a `syncInfo` entry can't tell an export from an import. - **`syncInfo` becomes 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](/api/projects/channels.md) 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](/api/projects/custom-fields.md) | Use these for anything a query predicate must filter on. | | Seller profile payload | A [Custom Object](/api/projects/custom-objects.md) 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](/api/projects/standalone-prices.md) carrying the seller's distribution Channel | The channel is what ties the price to the seller. | | Offer stock | An [Inventory Entry](/api/projects/inventory.md) carrying the seller's supply Channel, with a `key` of `-` | 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](/api/projects/product-selections.md) | Use when a seller's visible assortment differs from the catalog. | | Seller storefront isolation | A [Store](/api/projects/stores.md) | Only when isolation is a requirement. | | Marketplace order | An [Order](/api/projects/orders.md) created through checkout or [Order Import](/api/projects/orders-import.md) | Import creates an Order. It doesn't merge one. | | Export or import correlation | [SyncInfo](/search.md?urn=ctp:api:type: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](/guides/marketplace-integration.md#define-the-marketplace-integration-requirements). | ### Answer the same-SKU question first When several sellers sell the same SKU, model **one Product with per-seller prices and per-seller inventory**. Do not create one Product per seller. 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](/guides/marketplace-integration.md#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. Add a Store per seller only when you need isolation: a separate storefront, separate Merchant Center permissions for seller staff, or a seller-specific assortment that stays private. A Store carries `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 **** Stores. - A Store references up to **** supply Channels. - A Store references up to **** distribution Channels. - A Store holds up to **** Product 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. To model assortments with Product Selections, Stores, and Channels when no external marketplace is involved, see [Model assortments with Product Selections](/guides/product-selections.md). This guide uses the same primitives to scope a third party's offers. ### Offboard a seller by deactivating it You can [delete a Channel](/api/projects/channels.md#delete-channel) only if no Inventory Entry, Line Item, Store, Price, Standalone Price, or Cart Discount gift Line Item references it. As soon as a seller sells anything, an Order Line Item references its Channel for as long as that Order exists. To release the Channel, you would have to delete the Carts that reference it, then the Orders, then the Stores, and lose the order history along the way. Treat a seller Channel as permanent. 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. Attempting the deletion instead returns a [ReferenceExists](/api/errors.md#referenceexists) error and leaves the seller half-removed, with sync stopped and stock still visible. ## 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](/api/projects/channels.md) | Scoping stock and price to a supplier or a distribution partner, with roles that gate each use. | | [Stores](/api/projects/stores.md) | Isolating an assortment, its languages, its countries, and its channels behind a storefront boundary. | | [Product Selections](/api/projects/product-selections.md) | Defining which Products a Store sells, including per-Store price and availability behavior. | | [Standalone Prices](/api/projects/standalone-prices.md) | Holding several prices for one SKU, each scoped to a channel, a country, a currency, or a Customer Group. | | [Inventory](/api/projects/inventory.md) | Holding a separate stock record per SKU and supply channel. | Stop here if the assortment comes from parties you already administer, such as concessions, franchise partners, or internal business units, and no external system holds the seller record. [Model assortments with Product Selections](/guides/product-selections.md) covers that case from end to end. Continue if an external marketplace service holds the seller record, the offer catalog, or the order, because data then has to move between two systems. ### Check for a deployable Connector Check the live catalog when you start the implementation, rather than relying on what was available last time. Use an API Client with the `view_connectors` scope to call the [Search Connectors](/connect/connectors.md#search-connectors) endpoint, and filter on the `marketplace` [IntegrationType](/search.md?urn=ctp:connect:type:IntegrationType). ```http title="Search for marketplace Connectors" GET https://connect.{region}.commercetools.com/connectors/search?integrationTypes=marketplace&private=false ``` Pass `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](/connect/hosts-and-authorization.md). 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. Before you plan around a candidate, apply one test: open the repository and confirm that a `connect.yaml` file exists at the root of the repository or of the Connect application, as described in [Connector development](/connect/development.md). 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: 1. **Configure an existing deployable Connector** when one exists for your marketplace service and its seller, offer, order, and fulfillment coverage fits. 2. **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. 3. **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. 4. **Build the applications** when no deployable Connector exists for your marketplace service. No marketplace Connect template exists in either the documented template list or the CLI `--template` values, so paths 3 and 4 are the realistic outcomes. Plan for a build unless your Connector search returns a deployable candidate. Scaffolding is also less direct here, because the documented template list and the CLI flag don't match. The [Application templates overview](/connect/templates/templates-overview.md) documents the payment, product export, tax, and email templates. The [Connect CLI](/connect/cli.md) `--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 Seller sync, offer sync, inventory updates, price updates, and order export are all asynchronous. For most of them, the platform rules out the synchronous alternative entirely. [ExtensionResourceTypeId](/search.md?urn=ctp:api:type:ExtensionResourceTypeId) lists the resources an API Extension can target, and it omits Products, Product Types, Categories, Standalone Prices, and Inventory Entries. Offer, price, and stock sync therefore have no API Extension path at all. That leaves the two resource types an Extension can target in this domain: - **Don't register marketplace order export on an `order` Extension.** 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 an `event` application 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](/api/projects/api-extensions.md#time-limits) before you commit to it, and keep that Extension separate from the sync applications. ### Fan out inside the handler, never per seller Register one Subscription per Message type, then fan out to sellers inside the handler. One Subscription per seller, or one Extension per seller, hits a hard Project limit as the marketplace grows: a Project holds up to **** Subscriptions and up to **** 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` Place `connect.yaml` at the root of the repository or at the root of the Connect application, as described in [Connector development](/connect/development.md). The [Verify Connector](/connect/validation-pipeline.md) 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. ```yaml title="connect.yaml, abbreviated to the settings this guide discusses" 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. ``` The `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. Confirm every scope against the [API scopes](/api/scopes.md) reference rather than assuming coverage. Keep non-secret values such as the Project key, Store keys, and Channel key prefixes in standard configuration, and reserve `securedConfiguration` for credentials and shared secrets. ### Create the correlation Channels before the first order moves The [Update SyncInfo](/search.md?urn=ctp:api:type:OrderUpdateSyncInfoAction) action requires a Channel whose roles include `OrderExport` or `OrderImport`. Without that role, the action fails with an [InvalidInput](/api/errors.md#invalidinput) error after the marketplace has already created its order, which is the most expensive moment to fail. Create those Channels and register the Subscriptions in `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](/connect/deployment-behavior-and-environments.md). ### The operator round trip ```mermaid title="Operator round trip from seller onboarding to order routing" sequenceDiagram participant MP as Marketplace service box commercetools Connect participant Inbound as Inbound sync application participant Router as Order router end participant CT as commercetools MP->>Inbound: Send an authenticated seller webhook Inbound->>CT: Get the Channel by seller key CT-->>Inbound: Existing Channel and version, or not found Inbound->>CT: Create or update the seller Channel and roles MP->>Inbound: Send an authenticated offer webhook Inbound->>CT: Resolve the seller Channel, then the Product by SKU Inbound->>CT: Write a Standalone Price with the distribution channel Inbound->>CT: Write an Inventory Entry with the supply channel CT-->>Router: Deliver the OrderCreated or OrderImported Message Router->>CT: Get the Order by resource.id and read syncInfo CT-->>Router: Current Order, version, and existing syncInfo entries Router->>Router: Group Line Items by supply channel, skip recorded sellers Router->>MP: Push one payload per remaining seller MP-->>Router: Return one marketplace order identifier per seller Router->>CT: Update SyncInfo per seller Channel Router-->>CT: Acknowledge the delivery ``` In each exchange, the caller reads the current state before it writes or sends anything. The inbound application resolves the seller Channel before it writes a price or a stock record, because both scope to that Channel. The router reads the Order and its `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 `--` | Get by key, then create or update. | | SKU and seller | Inventory Entry `key`, such as `-` | 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. | Order Import is the exception. [Order Import](/api/projects/orders-import.md) creates an Order, and it doesn't upsert one. The `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. One setting is often mistaken for this control. The `AuthorizationHeader` [Destination](/search.md?urn=ctp:api:type: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: 1. Get the Channel by the seller key. On a match, update it with the returned version. On no match, create it. 2. Set the roles the seller needs: `InventorySupply` always, and `ProductDistribution` when the seller sets its own prices. 3. Write filterable seller attributes to Channel Custom Fields, so that predicates can select on them. 4. Write the free-form seller profile to the Custom Object container, which needs no prior read. 5. 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 Offer sync moves the offers that sellers supply. It doesn't move your master catalog content. If your product data is mastered in a PIM, that pipeline is a separate integration, described in [Integrate product data](/guides/product-data-integrations.md). The two pipelines write to different parts of the same Product. For each offer: 1. 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. 2. Resolve the Product. For a shared SKU, find the existing Product rather than creating a second one. 3. Write the price as a Standalone Price carrying the seller's distribution Channel. 4. Write the stock as an Inventory Entry carrying the seller's supply Channel and a deterministic `key`. 5. Apply the publish-or-stage decision you recorded. If you publish every imported offer without review, unreviewed seller content reaches the storefront. 6. 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. Put bulk loading in a separate application. Deltas that arrive through webhooks and a full catalog load have different consistency models, idempotency requirements, and time budgets, so one application that handles both satisfies neither. For the [Import API](/api/import-export/overview.md) path for bulk product, price, and inventory loading, see [Integrate product data](/guides/product-data-integrations.md). ### Import marketplace orders This flow applies to the seller or channel role, where the marketplace captures the order and commercetools records it. 1. 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. 2. Query Orders by `orderNumber` set to the marketplace order identifier. Import only when no match exists. 3. Build an [OrderImportDraft](/search.md?urn=ctp:api:type:OrderImportDraft). Set `orderNumber` to the marketplace identifier, and set `totalPrice` explicitly. commercetools doesn't calculate `totalPrice` from the Line Item and Custom Line Item totals, so an incorrect value produces a wrong Order with no error. 4. Set the per-line `supplyChannel` and `distributionChannel`, and record the marketplace line identifier in a per-line Custom Field so that later status updates can address a single line. 5. Choose the [InventoryMode](/search.md?urn=ctp:api:type:InventoryMode) deliberately. The marketplace already committed the stock, so any mode other than `None` decrements it a second time. For the behavior of each inventory mode, see [Inventory modes](/api/inventory-overview.md#inventory-modes). 6. Record the correlation with [Update SyncInfo](/search.md?urn=ctp:api:type:OrderUpdateSyncInfoAction) against a Channel carrying the `OrderImport` role, which `postDeploy` created. Decide whether to set `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](/guides/marketplace-integration.md#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. 1. **Subscribe to both `OrderCreated` and `OrderImported`.** An order-routing application subscribed only to `OrderCreated` silently 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. 2. **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. 3. **Get the Order by `resource.id`.** Don't map from the payload. Subscriptions deliver [at least once with no ordering guarantee](/api/projects/subscriptions.md#delivery-guarantees), so a payload can describe an older state. If a payload exceeds the queue size limit, it's omitted entirely and arrives with `payloadNotIncluded`, which means your largest multi-seller Orders are the ones that fail. 4. **Read `syncInfo` before 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. 5. **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. 6. **Record each success immediately.** Write [Update SyncInfo](/search.md?urn=ctp:api:type:OrderUpdateSyncInfoAction) per seller Channel with the version you read. On a `409` [ConcurrentModification](/api/errors.md#concurrentmodification) 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, the `syncInfo` entry for A must persist before the retry, so that the retry reaches only the missing seller instead of submitting to A twice. 7. **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](/connect/deployment-behavior-and-environments.md#event). Event applications and API Extensions use different response contracts, and the acknowledgment status codes are defined in [Acknowledge deliberately](/guides/oms-integration.md#acknowledge-deliberately). Don't reuse acknowledgment logic between the two application types. ### Forward fulfillment status An order-level state can't express that seller A shipped while seller B canceled. Use per-line [States](/api/projects/states.md) to carry per-seller progress. Each seller reads progress from the state machine in the marketplace service. Where an OMS is present, this application reads and forwards only. The OMS owns Delivery, Parcel, tracking, and the correlation write. For that mechanism, including the shipping data an Add Delivery action requires and the Multiple Shipping Mode differences, see [Read the Order before writing to it](/guides/oms-integration.md#read-the-order-before-writing-to-it). Don't add a second writer for that data. Where no OMS is present, this application owns per-line state and Deliveries, and the same guide supplies the mechanism. In both cases, apply a self-change filter. The inbound flow writes update actions, and those actions produce their own Messages. An outbound application that reacts to those Messages pushes your own write back to the marketplace, the marketplace answers with a status change, and that change produces another Message. Identify your own writes through a `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 Subscription delivery is retried for a limited window: up to 48 hours on a `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 `job` application uses a maximum of 2 CPU and 4 GB memory, and Connect [advises against job applications for bulk or batch operations](/connect/best-practices.md#job-application) that demand more extensive processing or high memory. - **Re-push Orders with no matching `syncInfo` entry** 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 `syncInfo` already 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 `totalPrice` explicitly, and a draft carrying a negative price or quantity is rejected by your validation. - A `payloadNotIncluded` delivery 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 in a non-production Project against a marketplace test environment. For running an event application against local code before deploying it, see [Test applications locally](/connect/test-applications-locally.md). Work through these checks: 1. **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`, plus `ProductDistribution` where 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. 2. **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. 3. **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. 4. **Verify delisting.** Send a removal or a zero quantity and confirm that the offer stops being sellable at that seller's price. 5. **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 `syncInfo` entry per seller Channel. For the seller role, deliver a marketplace order and confirm one imported Order, an explicit `totalPrice`, and a `syncInfo` entry against the `OrderImport` Channel. 6. **Verify redelivery.** Deliver the same Order Message twice and confirm that the second delivery pushes nothing and creates nothing. 7. **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. 8. **Verify offboarding.** Deactivate a seller through the procedure in [Offboard a seller by deactivating it](/guides/marketplace-integration.md#offboard-a-seller-by-deactivating-it) and confirm that its offers stop selling while its Orders remain intact. After launch, monitor Subscription health, redelivery counts, reconciliation lag and repair volume, marketplace throttling responses, and the number of Orders with no `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. Two environment behaviors change what you see during testing. A Connector deployed to a sandbox environment can scale to zero, so the first call after an idle period can fail even though the integration is correct. For details, see [Deployment behavior and environments](/connect/deployment-behavior-and-environments.md#environments). Marketplace sandbox environments also differ from production in rate limits, data volume, and retention, so a contract test that passes in a sandbox doesn't establish production throughput. 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](/guides/marketplace-integration.md#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](/api/errors.md#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](/guides/oms-integration.md#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](/guides/marketplace-integration.md#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](/connect/development.md) 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. | ## Related pages - [Area overview page with navigation](/guides.md) - [Previous page: Integrate product data](/guides/product-data-integrations.md) - [Next page: Integrate ERP](/guides/erp-integration.md) - [Search documentation and API specs](/search.md)