# Migrate to Modular Catalog Learn how to migrate an existing Project from the Classic catalog model to the Modular catalog model. The [`productCatalogModel`](/api/projects/project.md#productcatalogmodel) setting of a Project determines how [Product Variants](/search.md?urn=ctp:api:type:ProductVariant) are stored: - **`Classic`**: Variants are embedded within the Product. A Product can have up to 100 Product Variants. - **`Modular`**: Variants are standalone [Variant](/search.md?urn=ctp:api:type:Variant) resources linked to a Product. A Product can have up to 10,000 Variants, each with its own lifecycle. Pricing is handled exclusively through [Standalone Prices](/api/projects/standalone-prices.md). ## How the migration works Moving from `Classic` to `Modular` means changing two things: 1. **Your data**: your Variants must be written as standalone [Variant](/api/projects/variants.md#variant) resources, not only as embedded data inside your Products. 2. **Your integration**: your code must create, update, and read Variants through the [Variant APIs](/api/projects/variants.md). If both changed at the same time, you would need a single cutover in which your storefront, your PIM, your ERP, your import pipelines, and every other integration all switched together. The migration path exists so that you can change them one at a time, and reverse any step until the very end. This is made possible by the per-request override and the `InMigration` state. For a new Project without existing embedded Variants, see [Model and sell products with Modular Catalog](/guides/variant-modeling.md). ### Per-request override The `x-catalog-model` header forces a single request to resolve against a specific catalog model, regardless of the Project setting. You control it from your own application, so you decide which traffic uses it: one endpoint, one service, or a percentage of requests. Removing the header instantly restores the Project default behavior, with no Project-level change and no coordination required. Use it to validate your integration against Modular resolution before committing any traffic to it. ### The InMigration state A header only affects requests sent by your code. It cannot reach commercetools background processes, the Merchant Center, or third-party integrations that you do not control. Those switch only when the Project's default switches. The `InMigration` state changes that default to `Modular` for everything, while keeping the `Classic` path open: - Variants resolve from standalone Variants by default, with no header needed. This affects resources that reference a Variant, including [Cart](/api/projects/carts.md) and [Order](/api/projects/orders.md) Line Items, discount matching, and [Product Selections](/api/projects/product-selections.md). - `/products` and `/product-projections` continue to return embedded Variant data, so integrations that read them keep working. - [Product Search](/api/projects/product-search.md) continues to index embedded Variants. - Variant update actions on the [Product API](/api/projects/products.md) are still accepted, so dual-write continues to work. This lets you migrate reads and writes on different schedules. Systems that write catalog data can keep their existing write path and add the standalone Variant write alongside it. - In the `InMigration` state, a Variant written only as embedded data is not visible to Carts, Orders, or discount matching. Every Variant write must reach both representations for as long as your Project is in this state. - Throughout the migration, Product Search continues to index embedded Variant data. ### What each stage provides | Stage | Default resolution | Embedded reads | Embedded Variant writes | Reversible | | --- | --- | --- | --- | --- | | `Classic` | `Classic` | Yes | Yes | No | | `Classic` with header | `Modular` for selected requests | Yes | Yes | Yes, remove the header | | `InMigration` | `Modular` | Yes | Yes | Yes, set `Classic` | | `Modular` | `Modular` | Yes | No | Yes, set `InMigration`; resynchronize embedded Variants before setting `Classic` | | After cleanup | `Modular` | No | No | No | Embedded Variant data is preserved until the cleanup job runs. Cleanup is the point of no return. After cleanup, returning to `Classic` requires a separate reverse migration, which is not supported. ### The Classic catalog model limit While you are dual-writing, every Variant you create must also be created as an embedded Variant. Embedded Variants remain subject to the `Classic` limit of 100 Product Variants per Product. The `Modular` limit of 10,000 Variants per Product becomes available once you stop writing embedded Variants. ## Migrate in stages To complete the migration successfully, follow these stages: 1. [Meet all the prerequisites](/guides/migration-guides/modular-catalog-migration.md#prerequisites). 2. [Start dual-writing Variants](/guides/migration-guides/modular-catalog-migration.md#start-dual-writing-variants). 3. [Request the Variant copy job and consistency report](/guides/migration-guides/modular-catalog-migration.md#request-the-variant-copy-job-and-consistency-report). 4. [Roll out Modular reads](/guides/migration-guides/modular-catalog-migration.md#roll-out-modular-reads). 5. [Switch the Project to `InMigration`](/guides/migration-guides/modular-catalog-migration.md#switch-the-project-to-inmigration). 6. [Stop writing Variant updates through the Product API](/guides/migration-guides/modular-catalog-migration.md#stop-writing-variant-updates-through-the-product-api). 7. [Switch the Project to Modular](/guides/migration-guides/modular-catalog-migration.md#switch-the-project-to-modular). 8. [Request the cleanup job](/guides/migration-guides/modular-catalog-migration.md#request-the-cleanup-job). ## Prerequisites Before you begin, your Project must meet the following conditions. ### All Products use Standalone Prices The `Modular` model does not support Embedded Prices. Migrate your prices from Embedded to [Standalone Prices](/api/projects/standalone-prices.md) before you begin this migration. To find Products that still have Embedded Prices set, query the `prices` field on `masterVariant` and `variants`, for example, `masterData.current.masterVariant.prices` on `/products`, or `masterVariant.prices` on `/product-projections`. The following example fetches matching Product IDs with `limit=500`. If your catalog has more results than that, paginate through using the `offset` field to find them all. ```bash title="Find Products that use Embedded Prices" curl --get "https://api.{region}.commercetools.com/{projectKey}/products" --data-urlencode 'where=masterData(current(masterVariant(prices is not empty))) or masterData(current(variants(prices is not empty))) or masterData(staged(masterVariant(prices is not empty))) or masterData(staged(variants(prices is not empty)))' --data-urlencode 'limit=500' -H "Authorization: Bearer {access_token}" | jq -r '.results[] | .id' ``` If the query returns Product IDs, set their price mode after migrating their prices: ```bash title="Set the Product price mode to Standalone" curl -X POST "https://api.{region}.commercetools.com/{projectKey}/products/{id}" -H "Authorization: Bearer {access_token}" -H "Content-Type: application/json" -d '{ "version": {version_number}, "actions": [ { "action": "setPriceMode", "priceMode": "Standalone" } ] }' ``` ### API Client scopes - `manage_products:{projectKey}`: to create and update Products and Variants. - `manage_project_settings:{projectKey}`: to change the catalog model of the Project. - `manage_orders:{projectKey}`: to create and update Carts and Orders. - `view_products:{projectKey}`: to query standalone Variants and staged Variant Projections. - `view_published_products:{projectKey}`: to query Variant Projections. ### Test on a non-production Project first Run the full sequence on a sandbox or staging Project with representative data before migrating your production Project. ## Start dual-writing Variants Dual-writing means applying the same Variant changes to both the embedded Variant in the Product and its standalone Variant resource. You must write every Variant creation and update to both representations: the [Product API](/api/projects/products.md) for embedded Variants and the [Variants API](/api/projects/variants.md) for standalone Variants. The copy job copies only the initial Variant data. You are responsible for keeping the embedded and standalone representations in sync throughout the migration. Apply every Variant operation, including create, update, and delete, to both representations. ```bash title="Add an embedded Variant" curl -X POST "https://api.{region}.commercetools.com/{projectKey}/products/{product_id}" -H "Authorization: Bearer {access_token}" -H "Content-Type: application/json" -d '{ "version": {version_number}, "actions": [ { "action": "addVariant", "key": "outdoor-jacket-red-m", "sku": "JACKET-RED-M" } ] }' ``` ```bash title="Create a standalone Variant" curl -X POST "https://api.{region}.commercetools.com/{projectKey}/variants" -H "Authorization: Bearer {access_token}" -H "Content-Type: application/json" -d '{ "product": { "typeId": "product", "id": "{product_id}" }, "key": "outdoor-jacket-red-m", "sku": "JACKET-RED-M" }' ``` Updating a Variant through the Variants API only works once that Variant exists as a standalone Variant. Before the copy job runs, queue updates for embedded-only Variants and replay them after the copy job completes. ## Request the Variant copy job and consistency report Contact the [commercetools support team](https://support.commercetools.com/) to request the Variant copy job. Include the Region, Project key, preferred time window, and confirmation that all Products use Standalone Prices. The job creates standalone Variants from embedded Variant data for the current and staged representations. It preserves `product`, `variantId`, `sku`, and `key`, sets `masterVariant` as the default Variant for the Product, and sets the Product Variant counter to the highest `variantId`. The job fails if there is any Embedded Price left on any Variant. It also drops existing standalone Variants before copying, so any Variant that exists only as a standalone Variant is permanently lost. Create Variants in both representations during dual-write. Only one job runs per Project at a time. When it completes, the commercetools support team shares a consistency report that identifies Products whose embedded and standalone Variant counts differ. ## Roll out Modular reads While your Project is still in Classic mode, force Modular resolution on individual requests with the `x-catalog-model` header. Implement the header as a feature toggle so you can turn it off immediately if you see a problem. ```bash title="Test Modular resolution for a request" curl -X POST "https://api.{region}.commercetools.com/{projectKey}/carts/{cart_id}" -H "Authorization: ******" -H "Content-Type: application/json" -H "x-catalog-model: Modular" -d '{ "version": {version_number}, "actions": [ { "action": "addLineItem", "sku": "JACKET-RED-M", "quantity": 1 } ] }' ``` The line item's Variant is resolved from standalone Variants, and its price is selected from the Standalone Price for that SKU. Send the same request without the header to compare against Classic resolution. The header is supported on requests for Shopping Lists, Carts, Product Tailoring, and Orders, both through the HTTP API and the GraphQL API. It is not supported for Product Discounts matching, Inventory, Standalone Prices, or Product Selections. The header applies to the entire HTTP request. A GraphQL document that mixes a Cart query with a Product query resolves everything under one catalog model. You cannot scope it to an individual GraphQL selection. When adding a line item in the `Modular` model, specify an `sku`, or both `productId` and `variantId`. A `productId` alone is not sufficient. ## Switch the Project to InMigration Once your integration works correctly against Modular reads, switch the Project to the intermediate `InMigration` state: ```bash title="Switch a Project to InMigration" curl -X POST "https://api.{region}.commercetools.com/{projectKey}" -H "Authorization: Bearer {access_token}" -H "Content-Type: application/json" -d '{ "version": {version_number}, "actions": [ { "action": "setProductCatalogModel", "productCatalogModel": "InMigration" } ] }' ``` In `InMigration`, Variants resolve from standalone Variants by default, while `/products` and `/product-projections` continue to return embedded Variant data. Variant update actions on the Product API remain accepted, and Product Search continues to index embedded Variants. To roll back, set `productCatalogModel` to `Classic`. Treat `InMigration` as a transition window with a defined end date. Dual-write carries maintenance overhead and creates a risk of drift between the two representations. ## Stop writing Variant updates through the Product API Once your reads are fully served from the `Modular` model and consistency checks are clean, stop the embedded half of your dual-write. From this point, manage Variants only through the Variants API. This also removes the `Classic` limit of 100 Variants per Product. ## Switch the Project to Modular ```bash title="Switch a Project to Modular" curl -X POST "https://api.{region}.commercetools.com/{projectKey}" -H "Authorization: Bearer {access_token}" -H "Content-Type: application/json" -d '{ "version": {version_number}, "actions": [ { "action": "setProductCatalogModel", "productCatalogModel": "Modular" } ] }' ``` In `Modular` mode, Variant update actions on the Product API are rejected. Use the `/variants` endpoint instead. Embedded Variant data still exists at this point but is no longer maintained. You can return to `InMigration` directly because it resolves Variants from standalone resources. Before returning to `Classic`, synchronize all changes made after embedded writes stopped. ## Request the cleanup job The cleanup job permanently removes embedded Variant data. Do not request it until your Project has run on the Modular model in production long enough for you to be confident in the migration. After a grace period of at least 30 days from when you stop writing embedded Variants, contact the [commercetools support team](https://support.commercetools.com/) to request the cleanup job. The Project must be in `Modular` mode. After cleanup: - `/products` and `/product-projections` no longer return embedded Variant data. - The `variants` array is empty on the current and staged representations. - `masterVariant` is reset to a bare shape, with no SKU, attributes, or prices. Your migration is complete. Variant data is managed through the Variants API, read through the Variant Projections and Variant Attributes APIs, and priced through Standalone Prices. ## Related pages - [Area overview page with navigation](/guides.md) - [Previous page: Migrate to Product Search](/guides/migration-guides/product-search-migration-guide.md) - [Search documentation and API specs](/search.md)