Integrate product data

Ask about this Page
Copy for AI
View as Markdown

Replicate a product catalog from a PIM, an ERP, or both into commercetools, and keep Products, Prices, and Inventory current.

Product data is the foundation of a commercetools catalog. Whether your products originate in a Product Information Management (PIM) system, an enterprise resource planning (ERP) system, or a combination of both, a well-planned integration keeps your catalog accurate, searchable, and current without letting one slow feed block another.

This guide covers the decisions that shape a product data integration: which system owns each attribute, whether to configure a Connector or build the flows yourself, how to map an external product model onto the commercetools catalog, and how to load the catalog in bulk and keep it synchronized. It treats product content, Prices, and Inventory as separate synchronization concerns, because they change at different rates and fail independently.

It does not cover the post-order lifecycle, such as Order status, fulfillment, and returns, which belongs to Integrate an order management system. It does not cover financial and account master data, which belongs to Integrate ERP, or tax calculation, which belongs to Integrate tax.
Before you start, you need a commercetools Project, a PIM or ERP with an API or a scheduled extract mechanism, and an agreed owner for each attribute described in the next section. For the wider planning framework, see Integration planning and patterns.

Plan your product data integration

A product data integration is a set of one-way flows that differ in direction, volume, and urgency, and that fail independently. Before you configure or build anything, decide which flows exist and who owns each one. For the wider decision framework, see Plan integrations.

Assign a source of truth per attribute

Designate one system as the authoritative writer for each attribute, and make the commercetools copy read-only for it. A PIM typically owns enriched content such as names, descriptions, images, and specifications; an ERP typically owns SKUs, list Prices, and stock. Treat each row as a one-way contract, and reject or reconcile changes that originate on the read-only side.

Data domainTypical ownerDirectionWhere the design lives
Core product data and classificationPIM, or the ERP when no PIM existsExternal system to commercetoolsThis guide
Product content and enrichmentPIMExternal system to commercetoolsThis guide
Categories and category assignmentPIMExternal system to commercetoolsThis guide
Media and assetsPIM, or a DAMExternal system to commercetoolsThis guide
List and base PricesERPExternal system to commercetoolsThis guide
Contract and customer-specific PricesERPExternal system to commercetoolsIntegrate ERP
Inventory quantitiesThe system that owns OrdersUsually ERP or OMS to commercetoolsIntegrate an order management system

Confirm the Inventory and contract-Price rows against the linked guides before you rely on the defaults, because those flows are owned by the systems that own Orders and financial data.

Answer the questions that change the design

Record the following before choosing an approach, because each answer moves the architecture:

  • How many sources feed the catalog? A single PIM with a clear source of truth is the simplest case. Multiple systems, such as a PIM for content, a DAM for media, and an ERP for Prices and stock, add sequencing and conflict rules.
  • Is the flow one-way or bi-directional? One-way, from the external system to commercetools, is the recommended default. Bi-directional synchronization, where some attributes edited in commercetools flow back, is more expensive: you must define which attributes sync back and how conflicts resolve. Do not assume bi-directional.
  • Is product data editable in the Merchant Center? If product managers edit attributes in commercetools, decide which attributes remain owned by the external system, and restrict who can edit them.
  • What volume, and how often does each domain change? Core product data changes infrequently, Prices change on a schedule, and stock changes constantly. These are different integrations with different cadences, even when they leave the same system.
  • Which locales, currencies, and Channels are in scope? This determines the localization, Price, and Inventory mapping, and whether one extract serves every Store.
  • What must not be synchronized? Not every PIM attribute belongs in commercetools. Map only what search, display, pricing, and fulfillment need, and leave the rest in the source system.

Record special requirements

Capture constraints that could require custom extraction, transformation, or hosting, because each one can move the decision in the next section from configuring a Connector toward forking or building one:

  • Reference or related entities, such as cross-sells and related products, that the PIM models as first-class links.
  • Attribute values scoped by a PIM channel or context, where one attribute holds different values per scope.
  • Store- or market-specific catalogs that need Product Selections or Product Tailoring.
  • Measurement-unit conversion, quirks in variant and family modeling, or publish and staging rules.
  • Data residency, retention, or audit constraints that restrict where the integration runs.

Multiple sources of product data

When several systems write to the same Product, define the ownership boundary explicitly so no source overwrites another:

  1. The source of truth for each attribute. Determine which attributes come from each system, for example descriptions from the PIM and stock from the ERP. To keep externally owned attributes from being changed by hand in the Merchant Center, group them in a restricted AttributeGroup and grant editing permission only to the responsible teams. This is a Merchant Center permission boundary, not an API-level lock: it does not restrict an API Client, so keep the Product write scopes of each integration narrow as well.
  2. The process for creating Products. Send the initial version of a Product to commercetools once, when it is first created. Restrict subsequent updates from each source to only the attributes that source owns, and never perform a blind full overwrite that replaces another system's fields.
  3. The number of integrations required. Choose between a single integration that consolidates all sources, or a primary process that creates the Product and secondary processes that add data afterward.
  4. The write-back path, for bi-directional flows only. Identify the attributes for which commercetools is the source of truth, and ensure only those attributes sync back to the source system. Filter your own writes so an inbound update does not loop back out.
Offers supplied by third-party sellers through an external marketplace service are a distinct source with their own per-seller price and stock scoping, and they are covered in Integrate a marketplace service.

Choose a connector

Start with the option that requires your team to build and maintain the least custom integration logic. A deployable Connector that already syncs your PIM is almost always the right choice; build a custom integration only when no Connector fits a recorded requirement.

Check for an existing product data Connector

Check the current Connector inventory instead of relying on a remembered list, because published Connectors and their versions change. Use an API Client with the view_connectors scope to call the Search Connectors endpoint and filter by integration type:
Search for PIM and product data Connectorshttp
GET https://connect.{region}.commercetools.com/connectors/search?integrationTypes=pim&integrationTypes=erp
Query both pim and erp, because a Connector that replicates core product and Price data can be published under either type. For the full list of values, see IntegrationType. For the host to use in each Region, see Hosts and authorization. You can also browse and install Connectors in Connect in the Merchant Center.
A listing in the technology partner directory is not automatically an installable Connect Connector. Confirm that a candidate is available through Connect and synchronizes from the external system into commercetools before you plan a Connect deployment. Review its documentation and repository to determine how it installs and what it covers.

Evaluate each candidate against the attributes, directions, mappings, Regions, locales, and peak volume you recorded, rather than against its headline. Record the Connector key and version you evaluated.

Prove that a gap is not configuration

When a Connector matches your PIM but appears to be missing a behavior, check its field, Category, locale, and Channel configuration before you treat the behavior as a code gap. The data mapping decisions below are the same whether you configure a Connector or build one.

Fork or build

If a Connector matches your PIM and has a genuine gap that configuration cannot close, and its source is available, fork it. Add only the difference, such as a missing resource type or a bi-directional write-back, and deploy the result as an Organization Connector. You keep the working sync engine, keying, and dependency handling.

If no Connector matches, build one with Connect or with middleware your organization already operates. For a comparison of the runtime options, including vendor middleware, integration platforms, cloud services, and Connect, see Compare middleware and runtime options. Use a service application to receive pushed changes over an authenticated webhook, and a job application to run scheduled loads and reconciliation. Make scheduled runs restart-safe. Review the application behavior and limitations, and keep bulk file processing and long-running transformations outside Connect. For other workloads, review when to use an alternative to Connect. Scaffold each application with the Connect CLI:
Scaffold a Connector and add an applicationbash
commercetools connect init my-pim-connector
commercetools connect application add
For a bi-directional flow, add an event application that subscribes to Product Messages and writes the commercetools-owned attributes back to the source. Messages are delivered at least once and without ordering guarantees, so make the consumer idempotent and tolerate out-of-order delivery. See Subscriptions for the delivery model.
Grant each application only the scopes it writes with, such as manage_products, manage_categories, and manage_product_types, rather than manage_project. A narrow scope bounds the damage when a mapping defect or a misrouted event writes to the wrong resource. See scopes.

Authenticate the inbound webhook

When the source pushes changes, your service endpoint is the entry point an attacker reaches first, so two obligations come with it:
  • Register the endpoint with the source's event system so it delivers at all. Do this in an idempotent postDeploy automation script or through the source system's own configuration, and use the same script to create the Product Types and AttributeGroups the mapping depends on.
  • Verify the signature on every request before you read the payload. Most PIM and ERP systems sign with an HMAC over the raw request body using a shared secret, which you store in securedConfiguration. This is not the JSON Web Token (JWT) scheme commercetools uses for its own destinations, so read the source system's current documentation for the header name and algorithm rather than assuming. Reject unsigned, mis-signed, and replayed requests, and keep the handler idempotent so a redelivery is a no-op. For the status codes to return to the calling system, see Return a status the OMS can act on, which applies to any inbound webhook.

Choose your integration approach

The approach depends on the frequency of updates, the volume of data, and the capabilities of your source. Most catalogs need more than one approach: a bulk load for the initial catalog and periodic refreshes, and an incremental flow for time-critical changes.

Full versus incremental uploads

Full uploads send the entire catalog to commercetools. They take longer and leave the catalog partially updated during the run, so use them for the initial load and periodic complete refreshes.

Incremental uploads send only the products created or modified since the last run. They are faster and suit time-critical fixes: correct a description in the PIM and run the integration immediately, without waiting for the next full cycle. Not every source tracks changes natively; when it can't, the integration needs its own change detection, such as a stored hash or updatedAt per product, rather than reprocessing everything.

A robust integration does both: an incremental flow for freshness, and a scheduled full reconciliation that catches missed events and repairs drift.

Event-driven versus bulk, and the application shape

Match the delivery mechanism to the cadence, then pick the runtime shape it implies:

  • Event-driven flows handle changes as they occur and give near real-time updates. The source pushes each change to an authenticated endpoint, which maps to a service webhook application. Choose this when real-time accuracy matters.
  • Bulk flows run at scheduled intervals and are more efficient for large volumes where immediate updates are not essential. A schedule maps to a job application. Choose this for catalog refreshes and periodic synchronization.
The two shapes pair naturally with the two ingestion APIs: an event-driven service typically upserts through the HTTP API for an immediate result, and a scheduled job typically submits to the Import API for asynchronous bulk processing. Solution A and Solution B below implement each path.
For recommended start offsets and jitter for scheduled integrations, see the scheduled sync recommendations.

Data mapping

Mapping determines whether product data remains useful and maintainable. A PIM model is optimized for enrichment; the commercetools catalog is optimized for commerce: search, display, pricing, and fulfillment. Mapping is a transform and a filter, not a copy. Ground each decision in the Product catalog overview, and apply the same mapping whether you configure a Connector or build one.

Map only commerce-relevant data

Not every PIM attribute belongs in commercetools. Transfer only what search, display, pricing, or fulfillment needs, and leave the rest in the PIM, where it remains the source of truth. Every attribute you sync is one more thing to keep consistent.

Do not map Product Types one-to-one with PIM families

Linking a Product Type directly to each PIM family, type, or category makes the catalog sensitive to structural changes in the PIM. Product Type changes are expensive to absorb, because a Product Type constrains every Product already assigned to it, so each restructure in the PIM turns into a migration in commercetools. Instead, design a small set of flexible Product Types driven by how products are sold and searched, and absorb the PIM's structural variety through attribute values.

Type search-critical attributes precisely, and consolidate the rest

The product data mapping determines which fields an external search engine can index reliably. If your storefront uses one, coordinate these choices with the search document design in Integrate external search.

Split PIM attributes into two buckets:

  • Search, filter, and display-critical attributes, such as brand, color, size, and material, map to their own typed Product Type attributes. Type each one precisely: enum or lenum for controlled vocabularies so faceting works, number with a unit for measurements, and boolean for flags. Key each enum option on the PIM's stable option code, not its localized label, because labels change per translation and would silently break faceting.
  • Supplementary attributes that are displayed but never filtered consolidate into a single JSON text attribute rather than exploding into dozens of rarely used fields. This keeps the Product Type lean and the catalog queryable.

Map locales explicitly

commercetools models translatable text as LocalizedString, such as { "en-US": "…", "de-DE": "…" }. Map each PIM locale to a commercetools locale explicitly, because the codes do not always match: a PIM en_US must become en-US. Sync only the locales you decided are in scope, and verify the mapped values after import.

Resolve categories and references by key

Map the PIM category hierarchy to the commercetools Category tree. Give each Category a stable key derived from a stable PIM identifier, never from a localized name. A Product references its Categories by key, and a child Category references its parent by key. The Import API can resolve key-based dependencies asynchronously. Design the extract around matching keys, and follow the reference resolution rules for timing and failure handling.

Reference or related entities, such as cross-sells, are often the gap a public Connector does not cover, so confirm the Connector maps them or treat it as a reason to fork.

Give every resource a stable key

Use stable identifiers for every synchronized resource: Product.key, Product Variant key, Product Variant sku when it is the cross-system SKU, Price key, Category key, and Product Type key. Derive each identifier from a stable PIM identifier, then make every write an upsert by key. Stable identifiers prevent duplicate resources and support restart-safe processing. For platform behavior, see resource identifiers and Import API upserts.

Keep media, Prices, and Inventory on their own paths

Map PIM image and asset URLs onto Product Variant images, or onto Assets when you need richer metadata. If the PIM holds only references into a DAM, sync the resolved public URLs, and handle large binary sets in the bulk path rather than on a real-time webhook. Prices and Inventory each follow their own path, described next.

Manage price and inventory separately

Price and Inventory change far more often and are more time-critical than descriptions and images. Implement them as separate, event-based integrations, even when they originate in the same system as product content, so a slow nightly catalog run never blocks a Price or stock update.

Map Prices to embedded Prices on a Product Variant, or to Standalone Prices when Prices are selected by Customer Group or Channel or maintained independently of the Product. A Product Variant has a soft limit of 50 000 Standalone Prices, so size the account, Channel, validity, and tier combinations before choosing that model.
Track InventoryEntry records per SKU and optional supply Channel. Use a stable Inventory Entry key for Import API and HTTP API upserts, and keep sku aligned with the Product Variant SKU. The SKU does not need to exist as a Product Variant yet. Use Inventory Entry records for fulfillment and accurate checks, and Product Variant availability for non-critical storefront display. For the consistency model, see Product Variant availability.
Before you write Inventory from a PIM or ERP feed, decide whether commercetools also deducts it. The InventoryMode of a Cart or Line Item controls the platform-side deduction, and any mode other than None deducts the same units a second time when the system that owns Orders has already decremented them. That decision, and the reconciliation it implies, is in Synchronize Inventory.

Solution A: bulk import with the Import API

The Import API is purpose-built for asynchronous bulk ingestion with automatic resolution of KeyReference values. It is the right choice for the initial catalog load from a PIM or ERP, bulk migration from another commerce platform, and periodic full refreshes. The resource-specific import endpoints are in public beta; the Import Container, Import Operation, and Import Summary resources are generally available. Confirm the current status of each surface in the Import API overview.

Five Import API behaviors shape monitoring, retry, and ownership boundaries:

  • Queued processing. An Import Response confirms acceptance for asynchronous processing as ImportOperation resources, not completion.
  • Asynchronous reference resolution. A resolved reference can still fail commerce API validation. Follow the reference resolution behavior when designing retry and monitoring.
  • Updates remove omitted fields. When you import an update to an existing Product or Product Variant, the import removes the values of any fields you omit. Include every value you want to keep, and restrict each source to the fields it owns. See Choose the right Product import endpoint.
  • Bounded batch sizing. One import request carries at most 20 resources, so split every extract into batches of that size and send them in parallel. Size batches and containers according to the Import API best practices.
  • Publication is explicit. An import does not publish a Product for you, and importing an update to a published Product behaves differently from creating one. Decide whether the sync publishes, and follow Manage published state of Products.
Create one ImportContainer to group the operations. Leave its resourceType unset so a single container can accept both Inventory and Product data. A container created without a retentionPolicy expires 72 hours after creation, so set a TimeToLiveRetentionPolicy when a recurring load reuses the same container key. Because references resolve by key within the operation lifetime, you can submit Inventory before the Products exist: each Inventory Entry links to its Product Variant by sku once the Product is created.

Import inventory data from the ERP

Inventory data often originates in an ERP system. A common pattern is to export the ERP data as a CSV file, parse it, and submit it to the Import API.

Use an API Client with manage_import_containers:{projectKey} to manage the Import Container and manage_products:{projectKey} to submit the Inventory and Product Draft import requests in this example. To monitor the operations, also grant view_import_containers:{projectKey} and view_products:{projectKey}. For details, see Import API authorization.

Consider the following example CSV from the ERP:

ERP inventory exportcsv
sku,quantityOnStock,restockableInDays,expectedDelivery
product-sku-1,100,5,2026-05-01T10:00:00.000Z
product-sku-2,250,3,2026-04-15T10:00:00.000Z
product-sku-3,0,10,2026-06-01T10:00:00.000Z
Parse the CSV and submit the inventory data as import operations. Keep BATCH_SIZE aligned with the maximum of 20 resources per import request:
Import Inventory Entriestypescript
import { parse } from "csv-parse/sync";
import * as fs from "fs";

// Must not exceed the Import API limit on resources per request
const BATCH_SIZE = 20;

function batch<T>(items: T[], size: number): T[][] {
  const batches: T[][] = [];
  for (let i = 0; i < items.length; i += size) {
    batches.push(items.slice(i, i + size));
  }
  return batches;
}

// Parse the ERP CSV export
const csvContent = fs.readFileSync("erp-inventory-export.csv", "utf-8");
const records = parse(csvContent, { columns: true, skip_empty_lines: true });

// Map CSV rows to Import API InventoryImport resources
const inventoryImports = records.map((row) => ({
  key: `inventory-${row.sku}`,
  sku: row.sku,
  quantityOnStock: parseInt(row.quantityOnStock, 10),
  restockableInDays: parseInt(row.restockableInDays, 10),
  expectedDelivery: row.expectedDelivery,
}));

// Submit the inventory import operations, one batch per request
for (const resources of batch(inventoryImports, BATCH_SIZE)) {
  await apiRoot
    .inventories()
    .importContainers()
    .withImportContainerKeyValue({ importContainerKey: "pim-product-import" })
    .post({
      body: {
        type: "inventory",
        resources,
      },
    })
    .execute();
}

Import product data from the PIM

Product data typically comes from a PIM system, also often as a CSV export. Products contain KeyReference fields for resources such as Product Types and Categories. The Import API resolves these references automatically.

Consider the following example CSV from the PIM:

PIM product exportcsv
key,name.en,slug.en,description.en,productType,categories,sku,priceKey,price
blue-widget,"Blue Widget","blue-widget","A high-quality blue widget.","standard-product","widgets;accessories","blue-widget-sku","blue-widget-price","EUR 1999"
red-gadget,"Red Gadget","red-gadget","A versatile red gadget.","standard-product","gadgets","red-gadget-sku","red-gadget-price","EUR 2499"

Parse the CSV and submit the product data as import operations:

Import Product Draftstypescript
import { parse } from "csv-parse/sync";
import * as fs from "fs";

// Must not exceed the Import API limit on resources per request
const BATCH_SIZE = 20;

function batch<T>(items: T[], size: number): T[][] {
  const batches: T[][] = [];
  for (let i = 0; i < items.length; i += size) {
    batches.push(items.slice(i, i + size));
  }
  return batches;
}

// Parse the PIM CSV export
const csvContent = fs.readFileSync("pim-product-export.csv", "utf-8");
const records = parse(csvContent, { columns: true, skip_empty_lines: true });

// Map CSV rows to Import API ProductDraftImport resources
const productDraftImports = records.map((row) => {
  const [currencyCode, centAmountStr] = row.price.split(" ");
  return {
    key: row.key,
    name: { en: row["name.en"] },
    slug: { en: row["slug.en"] },
    description: { en: row["description.en"] },
    productType: {
      key: row.productType,
      typeId: "product-type",
    },
    categories: row.categories.split(";").map((catKey) => ({
      key: catKey.trim(),
      typeId: "category",
    })),
    masterVariant: {
      key: row.sku,
      sku: row.sku,
      prices: [
        {
          key: row.priceKey,
          value: {
            type: "centPrecision",
            currencyCode: currencyCode,
            centAmount: parseInt(centAmountStr, 10),
          },
        },
      ],
    },
  };
});

// Submit the product import operations, one batch per request
for (const resources of batch(productDraftImports, BATCH_SIZE)) {
  await apiRoot
    .productDrafts()
    .importContainers()
    .withImportContainerKeyValue({ importContainerKey: "pim-product-import" })
    .post({
      body: {
        type: "product-draft",
        resources,
      },
    })
    .execute();
}
The productType and categories fields use key references. The Import API resolves these dependencies asynchronously. If the referenced Product Type or Category does not exist yet, the operation stays unresolved for the 48-hour lifetime of the Import Operation and is retried up to 5 times once the dependency arrives. After that the operation expires.
After submitting import operations, monitor the ImportSummary. Treat imported, rejected, validationFailed, canceled, and partiallyImported as terminal states. Treat processing, unresolved, and waitForMasterVariant as non-terminal states. Inspect an individual ImportOperation when the summary reports a resource that needs attention.
For guidance on organizing Import Containers, handling retries, rate limits, and anti-patterns to avoid, see the Import API best practices.

Solution B: incremental sync with the HTTP API

The HTTP API is the right choice for real-time, incremental synchronization where you need immediate feedback on each operation. It suits real-time synchronization from event-driven sources, incremental updates where only a few resources change at a time, and any flow that needs synchronous confirmation that each operation succeeded.
The core pattern is an upsert by key: fetch a resource by its key, then create it if it does not exist or update it if it does. When the source pushes changes to a service webhook, authenticate the caller, verify the payload signature, and make the handler idempotent so a redelivered event is a no-op.

Upsert inventory entries

The upsert pattern for inventory uses the key to check existence. If the resource doesn't exist (HTTP 404), create it. If it exists, update it with the latest data.
Upsert an Inventory Entrytypescript
async function upsertInventoryEntry(
  apiRoot,
  inventoryKey: string,
  sku: string,
  quantityOnStock: number
) {
  try {
    // Attempt to fetch the existing inventory entry by key
    const existing = await apiRoot
      .inventories()
      .withKey({ key: inventoryKey })
      .get()
      .execute();

    // Resource exists: update it
    const updated = await apiRoot
      .inventories()
      .withKey({ key: inventoryKey })
      .post({
        body: {
          version: existing.body.version,
          actions: [
            {
              action: "changeQuantity",
              quantity: quantityOnStock,
            },
          ],
        },
      })
      .execute();

    return updated.body;
  } catch (error) {
    if (error.statusCode === 404) {
      // Resource doesn't exist: create it
      const created = await apiRoot
        .inventories()
        .post({
          body: {
            key: inventoryKey,
            sku: sku,
            quantityOnStock: quantityOnStock,
          },
        })
        .execute();

      return created.body;
    }
    throw error;
  }
}

Upsert products

The same pattern applies to Products. Fetch by key, then create or update depending on whether the Product already exists.

Upsert a Producttypescript
async function upsertProduct(
  apiRoot,
  productKey: string,
  productData: {
    name: { en: string };
    slug: { en: string };
    productType: { key: string; typeId: "product-type" };
    description?: { en: string };
  }
) {
  try {
    // Attempt to fetch the existing Product by key
    const existing = await apiRoot
      .products()
      .withKey({ key: productKey })
      .get()
      .execute();

    // Resource exists: update it
    const updated = await apiRoot
      .products()
      .withKey({ key: productKey })
      .post({
        body: {
          version: existing.body.version,
          actions: [
            {
              action: "changeName",
              name: productData.name,
            },
            {
              action: "changeSlug",
              slug: productData.slug,
            },
          ],
        },
      })
      .execute();

    return updated.body;
  } catch (error) {
    if (error.statusCode === 404) {
      // Resource doesn't exist: create it
      const created = await apiRoot
        .products()
        .post({
          body: {
            key: productKey,
            name: productData.name,
            slug: productData.slug,
            productType: productData.productType,
            description: productData.description,
          },
        })
        .execute();

      return created.body;
    }
    throw error;
  }
}

Build a resilient HTTP API client

When synchronizing product data at scale using the HTTP API, treat resilience as part of the integration contract. Ramp up traffic gradually, limit concurrency, and retry transient failures with exponential backoff and a maximum attempt count. For 409 ConcurrentModification responses, fetch the latest resource version and retry only if the update is still required. For implementation guidance, see client resiliency and error handling through timeouts and retries.

Choose the right API

Most product data integrations use both APIs: the Import API for initial loads and reconciliation, and the HTTP API for event-triggered updates that need immediate confirmation. For the canonical comparison, see Why choose the Import API?. Apply the bulk and incremental examples on this page to the corresponding product data flows.

Verify the integration

Before the first production run, test the mapping in isolation. A mapping is a pure transformation from a source record to a commercetools draft, so you can test it with fixtures and no credentials. Cover the cases that silently corrupt a catalog: locale codes that differ between the systems, controlled vocabularies keyed on a stable code rather than a translated label, measurement units normalized to one measure, keys derived deterministically from a source identifier, references emitted by key, and out-of-scope attributes omitted rather than sent as empty values.

Only then run a live sync, and guard it, because a sync writes. Point it at a disposable test Project, never production, and make the run fail closed when it cannot confirm that: load credentials from an ignored environment file, require an explicit marker such as CT_ENV=test before the run proceeds, and refuse any Project key that is not on an allowlist. Use the same narrow scopes the integration will run with in production.

Count before you write. Ask the source how many records the run covers, print the count, and refuse a run above a threshold unless the operator confirms it explicitly. An unbounded first sync can pull a full PIM export of hundreds of thousands of products, which is slow, expensive, and hard to undo in a shared Project. Bound the first run to a small representative sample, and reserve full-catalog runs for the Import API path.

With that sample, prove the following:

  • The Products, Categories, Prices, and Inventory exist with the expected keys, localized values, and references.
  • Re-running the same sample changes nothing and creates no duplicates, which confirms the upsert is idempotent.
  • A record changed in the source reaches commercetools within the agreed window.
  • A field another system owns remains unchanged after an update from a different source.
  • A product discontinued in the source produces the agreed outcome, such as unpublish or deactivate, rather than remaining sellable.
  • Reconciliation detects a deliberately skipped update and repairs it.
For a bulk run, poll the Import Summary until the operations reach a terminal state. Inspect operations that require attention, including rejected, validationFailed, and partiallyImported operations. Use the Import Operation states, reference resolution behavior, and Product Variant availability to distinguish defects from expected asynchronous processing. Clean up the test data afterward.

Troubleshoot observable symptoms

Match the symptom you observe to its likely cause and resolution:

SymptomLikely causesChecks and resolution
An import created a second resource instead of updating the existing oneThe existing resource has no key, or it has a different one. The Import API cannot set a key on an existing resourceSet keys through the HTTP API first, then re-run the import and remove the duplicates
Products imported successfully but the storefront returns nothingThe import created the Products without publishing them, so only the staged data existsDecide where the sync publishes and follow Manage published state of Products
An import request was rejected before any operation was createdThe request carried more than 20 resourcesSplit the extract into batches of that size and send the batches in parallel
A Price or stock load blanked descriptions or imagesAn import update omitted fields another system owns, and the omitted values were removedInclude every field to keep in the import, or restrict each source to the resources and fields it owns
Import operations stay unresolved and eventually expireA referenced Product Type, Category, or Channel does not exist, and the reference never arrived within the operation lifetimeCreate the prerequisite, then resubmit within the resolution window
An operation resolves its references but the resource is still missingReference resolution is not validation; the operation entered validationFailed because the data violated a constraint such as sku uniquenessRead the operation errors field, correct the source record, and submit a new import
Facets or filters break for a translated catalogAn enum attribute was keyed on a localized label instead of a stable option codeKey each enum option on the PIM option code and re-map the affected attribute
A translation is missing after synchronizationA PIM locale code such as en_US was not mapped to the commercetools form en-USMap each PIM locale explicitly, drop out-of-scope locales, and verify the imported LocalizedString values
Stock imported but the storefront still shows the old availabilityThe Product Variant availability field is eventually consistent and lags the InventoryEntryQuery the InventoryEntry for accurate stock; allow a few seconds for availability to settle
Stock is deducted twice for the same OrderThe feed decrements Inventory and commercetools decrements it too, because the Cart InventoryMode is not NoneSet the mode to None, or subtract the platform-side deduction in the mapping
An externally owned attribute keeps being changed in the Merchant CenterThe attribute is not restricted, so a team edits it and the next sync overwrites itGroup externally owned attributes in an AttributeGroup and grant editing permission only to the responsible teams
Discontinued products remain sellableNo flow handles removal, because the Import API cannot deleteDetect resources present in commercetools and absent from the source during reconciliation, then unpublish or deactivate them