> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omnicommerce.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Merge marketplace listings

> Find duplicate catalog products, choose a valid master, and merge through the CLI or SDK.

This is the same operation as **Products → Combine → Marketplaces**. Exact-SKU
catalog products become one master product. Existing marketplace listings keep
their IDs, content and live/unpublished state. Other OmniCommerce products are
archived, with history retained. The merge itself does not publish changes.

The master keeps its content, images, price and stock. Stock is **not added
together**. Distinct listings in the same connected store block a merge. Listings
in different stores, including stores on the same marketplace, can be retained.
Every supported marketplace uses the same merge service.

## Discover duplicates

Use credentials for the intended organization. Reading requires `catalog:read`;
refreshing a scan and merging require `catalog:write`.

```bash theme={null}
omni products scan-duplicates --json '{}'
omni products get-duplicate-scan --format json
omni products list --status duplicates --duplicate-type sku --detail full --format json
```

The scan runs durably in the background. An already queued/running scan is reused.
Read `data.status` until `completed` with a bounded wait/backoff; stop and inspect
`errorMessage` on `failed`. `idle` means a scan has not been saved. Results are
from the latest saved scan; `requestedAt` and `computedAt` show its freshness.
The shared UI scan currently examines at most the 5,000 most recently updated
non-archived root products. Completion does not establish full coverage of a
larger catalog.

List rows are in `items`. Each row's `duplicateMatches` includes peer `productId`,
name, SKU, score and match reasons. Use the root `id` plus its `exact_sku` peer IDs
as candidate targets, removing repeats. Follow `pagination.nextCursor` and
`nextOffset` until `hasMore` is false for the selected scope. A single page or
representative row is not the entire catalog. `detail=summary` omits duplicate
evidence; use `full`. `duplicateStatus=active` restricts discovery to active products;
omit it to include drafts as in the UI.

`duplicateType=content` and `variants` expose other UI discovery filters, but
matching content/model signals alone do **not** make products eligible for this
merge. The server partitions and validates exact-SKU groups. Send 2–20 distinct
products per request, at most 10 groups. Do not silently truncate larger groups.

## Choose and review the master

```bash theme={null}
omni products preview-duplicate-merge --json '{"productIds":["product-a","product-b"]}' --format json
```

Inspect `data.groups[].products` for names, SKUs, currency-labeled prices, stock
and every per-store listing. Inspect each group's `masterSelections`:

| Result | Action |
| - | - |
| One `allowed: true` | Keep that product as master. Its stock connection must remain on the existing product. |
| Several `allowed: true` | Choose which product's content, price and stock the seller wants to retain. Eligibility does not imply a universal best choice. |
| No allowed master | Keep the products separate. The card `note` explains the stock conflict. |
| Missing eligibility or another group error | Do not merge; inspect the error and obtain a fresh successful preview. |

A domain block returns HTTP 200 with `ok: false`; the preview and eligibility are
still in `data`. In particular, `errorCode: stock_source_requires_master` means the
current master choice cannot preserve stock ownership. Choose an allowed master
and preview again. Other blockers, such as running jobs, variants, bundles,
currency differences and same-store listing conflicts, remain enforced.

```bash theme={null}
omni products preview-duplicate-merge --json '{"productIds":["product-a","product-b"],"masterProductIds":["product-b"]}' --format json
```

This is a real read-only validation. CLI `--dry-run` only prints the request and
does not perform server validation. Review each successful group's `productIds`
(master first), `sku` and preserved listings before approving a merge.

The UI dialog's **Dry run** button opens chat with the selected products and
master. Choose **Run dry run** in the tool card. The server runs the same merge
transaction, validates database constraints and rolls back its writes. Products,
marketplace links, references and audit records remain unchanged; it emits no
product-update events or duplicate scan. A real merge needs a separate approval.

CLI and SDK callers use `dryRun: true` on the merge operation for this server-side
simulation. This differs from the CLI's transport-only `--dry-run` option:

```bash theme={null}
omni products merge-duplicates --json '{"productIds":["product-a","product-b"],"masterProductIds":["product-b"],"expectedGroups":[{"productIds":["product-b","product-a"],"sku":"REVIEWED-SKU"}],"dryRun":true}' --format json
```

Dry-run responses include `dryRun: true`; counts and per-product results describe
validation outcomes. `mergedProductIds` is empty and `wouldMergeProductIds`
contains the sources that passed simulation. Partial failures remain explicit.

## Merge the reviewed groups

Pass `expectedGroups` from the successful preview. This pins group membership,
master and SKU; changed or omitted groups fail rather than being silently merged.

```bash theme={null}
omni products merge-duplicates --json '{"productIds":["product-a","product-b"],"masterProductIds":["product-b"],"expectedGroups":[{"productIds":["product-b","product-a"],"sku":"REVIEWED-SKU"}]}' --format json
```

Replace example IDs and SKU with the reviewed values. Inspect `data.success`,
`succeededCount`, `failedCount`, `groups` and ordered per-product `items`. Groups
commit independently; report partial failures. Do not treat HTTP 200 as proof that
every product merged. The server rechecks stock connections and other constraints
inside the merge transaction. A separate authorized publish/update is needed to
send content or prices to existing marketplace listings.

## TypeScript SDK

```ts theme={null}
// Refresh once with scanDuplicates({}) if needed, then wait for completion.
const scan = await client.products.getDuplicateScan();
if (scan.data.status !== "completed")
  throw new Error("Wait for a successful duplicate scan");

const page = await client.products.list({
  status: "duplicates",
  duplicateType: "sku",
  detail: "full",
});
// Page all results and select the root + exact_sku peer IDs to review.
const productIds = ["product-a", "product-b"];
const inspected = await client.products.previewDuplicateMerge({ productIds });
if (inspected.data.groups.length !== 1)
  throw new Error("Review one master per group");
const allowed =
  inspected.data.groups[0].masterSelections?.filter((x) => x.allowed) ?? [];
// If several are allowed, obtain the seller's choice instead of guessing.
if (allowed.length !== 1)
  throw new Error("Choose an eligible master before continuing");
const masterProductIds = [allowed[0].productId];
const reviewed = await client.products.previewDuplicateMerge({
  productIds,
  masterProductIds,
});
if (!reviewed.data.success) throw new Error(reviewed.data.error);
const expectedGroups = reviewed.data.groups.map((group) => {
  if (!group.success || !group.sku) throw new Error("A group is not ready");
  return { productIds: group.productIds, sku: group.sku };
});
// Test the actual merge transaction without saving:
const simulation = await client.products.mergeDuplicates({
  productIds,
  masterProductIds,
  expectedGroups,
  dryRun: true,
});
console.log(simulation.data.success, simulation.data.wouldMergeProductIds);
// A real merge is separate, after the seller approves these concrete targets:
const result = await client.products.mergeDuplicates({
  productIds,
  masterProductIds,
  expectedGroups,
});
console.log(result.data.success, result.data.items);
```

Preview and merge responses are typed from the canonical schemas, including master
eligibility and per-target results. The example continues only for a single group
with one eligible master; for multiple groups, choose and review one master per
group and include every approved group in the batch.

## Python SDK

```python theme={null}
# Refresh once with scan_duplicates() if needed, then wait for completion.
scan = client.products.get_duplicate_scan()  # wait for data.status == completed
if scan["data"]["status"] != "completed":
    raise ValueError("Wait for a successful duplicate scan")
page = client.products.list(status="duplicates", duplicate_type="sku", detail="full")
product_ids = ["product-a", "product-b"]  # selected root and exact-SKU peers
inspected = client.products.preview_duplicate_merge(product_ids=product_ids)
if len(inspected["data"]["groups"]) != 1:
    raise ValueError("Review one master per group")
allowed = [x for x in inspected["data"]["groups"][0].get("masterSelections", []) if x["allowed"]]
if len(allowed) != 1:
    raise ValueError("Choose an eligible master before continuing")
masters = [allowed[0]["productId"]]
reviewed = client.products.preview_duplicate_merge(product_ids=product_ids, master_product_ids=masters)
if not reviewed["data"]["success"]:
    raise ValueError(reviewed["data"].get("error", "Merge blocked"))
groups = reviewed["data"]["groups"]
expected = [{"productIds": g["productIds"], "sku": g["sku"]} for g in groups]
# Test the transaction without saving any changes:
simulation = client.products.merge_duplicates(product_ids=product_ids, master_product_ids=masters, expected_groups=expected, dry_run=True)
print(simulation["data"]["success"], simulation["data"]["wouldMergeProductIds"])
# A real merge is separate, after reviewing and approving all targets:
result = client.products.merge_duplicates(product_ids=product_ids, master_product_ids=masters, expected_groups=expected)
print(result["data"]["success"], result["data"]["items"])
```

`AsyncOmni` exposes the same operations with `await`. Python request arguments use
snake\_case; response fields and nested `expectedGroups` members retain camelCase.
CLI `--help` and `--schema` include operation guidance generated from OpenAPI, so
terminal agents can discover these rules without a browser session.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.