# Product Catalog Reconciliation — SKU, Margin & Stock QA (`elfajad/product-catalog-reconciliation`) Actor

Compare supplier and store CSV/JSON exports: duplicate SKUs, exact or explicit matches, optional GTIN matching, cost-versus-retail margin and stock differences. Up to 5,000 rows per catalog. Printable HTML, JSON and a CSV review queue. Fictional demo. No store writes.

- **URL**: https://apify.com/elfajad/product-catalog-reconciliation.md
- **Developed by:** [El Fajad](https://apify.com/elfajad) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $19.00 / catalog reconciliation report

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Product Catalog Reconciliation

Compare a **supplier export with a store export** and get a prioritized review queue: duplicate SKU identities, matched product rows, supplier-only/store-only records, unit-cost versus retail arithmetic, reference-price differences and quantity conflicts. Process **up to 5,000 rows per catalog** with **printable HTML, full JSON and CSV**.

Built for merchants and agencies preparing a catalog review. Supply CSV text or JSON rows; there are no store writes, automatic price changes, upstream paid Actors or live website requests. No AI key or VPS is needed.

### Try the fictional demo

```json
{ "mode": "demo" }
```

Fixed synthetic catalogs illustrate leading-zero SKUs, duplicate identities, a below-cost listing, a zero-supplier-stock review, a unique GTIN fallback and an unmatched case difference. Custom catalog inputs are ignored in demo mode. **No $19 report event** is charged; the $0.00005 startup event still applies.

Open **Catalog brief → Printable HTML**. Print or save as PDF, or download the CSV review queue and full JSON.

### Reconcile your exports

```json
{
  "mode": "reconcile",
  "reportLabel": "Supplier / store catalog review",
  "minimumGrossMarginPercent": 30,
  "matchByBarcode": false,
  "supplierCatalog": {
    "defaultCurrency": "USD",
    "rows": [
      { "sku": "001", "title": "Ceramic mug", "cost": "8.00", "price": "14.00", "quantity": "0" }
    ]
  },
  "storeCatalog": {
    "defaultCurrency": "USD",
    "rows": [
      { "sku": "001", "title": "Ceramic coffee mug", "price": "7.00", "quantity": "10" }
    ]
  }
}
```

1. Export the supplier and store catalog for the **same scope, unit basis and currency**. Inventory exports with multiple locations should be filtered to one comparable location first; duplicate SKUs are not automatically summed.
2. Provide `rows` **or** `csvText` on each side. Map nonstandard headers with `columns`.
3. Keep identifiers and money as **strings**. Set currencies explicitly, per row or using `defaultCurrency`. Neither defaults to USD.
4. Set maximum charge to **at least $19.01**. Your account's remaining budget and spending limits must allow it. The small saved default is for the demo.
5. Review findings and their row IDs before making changes. Download results before your Apify retention period expires.

An entirely disjoint pair of valid catalogs still produces a billable report. Reports with no findings or only duplicate-identity issues are also billable when each side has at least one usable SKU. This Actor does not verify the exports are complete, recent or correctly assigned to products.

### CSV input and field mapping

```json
{
  "mode": "reconcile",
  "supplierCatalog": {
    "csvText": "Item Code,Unit Cost,Recommended Retail,Units\n001,8.00,14.00,0\n",
    "defaultCurrency": "USD",
    "columns": { "sku": "Item Code", "cost": "Unit Cost", "price": "Recommended Retail", "quantity": "Units" }
  },
  "storeCatalog": {
    "csvText": "Variant SKU,Title,Variant Price,Variant Inventory Qty\n001,Ceramic mug,7.00,10\n",
    "defaultCurrency": "USD",
    "columns": { "sku": "Variant SKU", "title": "Title", "price": "Variant Price", "quantity": "Variant Inventory Qty" }
  }
}
```

Map your **actual headers**; Shopify exports and product/inventory CSV formats vary. This is not a Shopify CSV validator or an import-ready patch generator. You can paste UTF-8 text from your exported file into `csvText` through the JSON input editor; this MVP does not download file URLs or support XLSX directly.

Canonical fields: `sku`, `title`, `barcode`, `cost`, `price`, `quantity`, `currency`. Omitted optional fields remain unknown. Explicitly mapped absent columns fail clearly rather than silently using another field. Set a mapping to `null` to ignore that field. Unmapped extra columns are not used in analysis.

Each catalog can use `delimiter: "comma"` (default), `"semicolon"` or `"tab"`. CSV quoting, embedded newlines, BOM and CRLF are supported. Duplicate/empty headers, malformed quoting or inconsistent row widths are rejected; no malformed rows are silently skipped. `sourceRow` means the one-based **data record number**, excluding the header, not the physical CSV line number.

### Identity matching

| Match | Behavior |
|---|---|
| Exact SKU | Default. Case, whitespace, punctuation and leading zeros remain significant. |
| Explicit `skuMap` | One-to-one `{ "supplierSku": "A", "storeSku": "B" }` entries, applied before exact matches. Conflicting/repeated mappings fail. Unresolved mapped rows are reserved and not matched by fallback. |
| Optional GTIN fallback | Enabled by `matchByBarcode: true`. Unique valid GTIN-8/12/13/14 values are compared in zero-padded 14-digit form. Does not bypass duplicate SKUs or existing exact-SKU conflicts. |
| Case/space suggestions | Unique unmatched SKUs differing only by case, surrounding spaces or Unicode normalization are flagged for manual review. They are **never automatic matches**. |
| Title similarity | No fuzzy title or AI identity matching. |

Duplicate SKUs on either side exclude those rows from automatic matching, even if duplicate fields are identical. Quantities are not summed. A valid GTIN check digit does not prove GS1 allocation, ownership, product identity or that the item represents the same pack/unit. Exact SKU matches with different valid GTINs get a high-priority review.

Missing/numeric/control-character SKUs are unusable. JSON identifiers must be strings: `"001"`, not `1`. Export completeness is supplied context, not independently verified. Supplier-only/store-only findings describe the supplied exports, not discontinued or newly launched products.

### Money and stock checks

- `cost` = supplied supplier **unit cost**. `supplierCatalog.price` = reference price, which may be recommended retail or another price you chose. `storeCatalog.price` = store retail price. A reference-price difference is a review item, not proof of an incorrect listing.
- Money must be a nonnegative **decimal string**, e.g. `"12.50"`, at most four decimal places and at most `999999999.9999`. Currency symbols, thousands separators, decimal commas, scientific notation, surrounding spaces and numeric JSON money are invalid; they are not guessed or converted.
- Currencies must be explicit uppercase three-letter codes on each row or via that catalog's default. Codes and their real-world assignment are not independently verified. Different or missing currencies prevent price/margin arithmetic. No exchange-rate conversion occurs.
- Gross margin = `(retail price − supplied unit cost) / retail price × 100`, before tax, shipping, fees, discounts or other costs. Displayed percentages are truncated to two decimal places. The threshold defaults to 30% and is a configurable review rule, not a recommended price.
- A zero retail price makes margin undefined. Retail below supplied unit cost is high priority. Missing/invalid cost or price remains unknown.
- Quantities are nonnegative whole units, up to 999,999,999. Different stock figures are review items; zero supplier units and positive store units get high priority. Your own inventory, reservations, warehouse scope or capture times can explain differences. No automatic sync or inferred lost-revenue estimate.

### Pricing

Launch price: **$19 per delivered non-demo catalog report**, for up to 5,000 rows on each side. **The Pricing tab is authoritative.**

- `catalog-report`: one event per usable report, including no-match/no-finding reports and duplicate-identity reviews.
- Fictional demo: no report event.
- Invalid input, parsing failure, insufficient report budget or a catalog with zero usable string SKUs: no report event. Unusable collections save data-quality details before failing.
- Startup event: **$0.00005** at supported memory sizes, including demos and failures.
- No separate dataset-row charge or customer platform-usage pass-through.

The report budget is checked before parsing real catalogs. HTML, CSV and full reconciliation are saved before the report row is billed. Already-delivered rows are skipped if a run is resurrected. An effective budget below $19 prevents a real report; account limits may override saved run settings. Owner validation runs show owner resource usage separately.

### Outputs and limits

The default dataset contains **one report summary**, HTML/CSV links, counts, coverage, **the first 100 findings and first 25 matches**. `datasetPreview` explicitly describes these display limits. **All rows are processed** and the full result is available from the key-value store:

| Record | Content |
|---|---|
| `catalog-<analysisId>.html` | Printable full checklist and match table |
| `REVIEW-QUEUE.csv` | Every finding and source row reference; review-only, not an import file |
| `RECONCILIATION` | Full JSON report, every finding and match |
| `NORMALIZED-ROWS` | Every interpreted source record, row ID, field state and data-quality issue |
| `RUN-SUMMARY` | Demo/delivery/counts/budget state |

CSV fields beginning with formula characters are prefixed with an apostrophe. Spreadsheet software can still convert numeric-looking identifiers or strip leading zeros; import SKU columns as text or use JSON for exact identifiers. Do not feed this review CSV into a platform import workflow.

Input limit: **8 MiB total**, at most **4 MiB CSV text per side**, **5,000 data rows per side**, 60 columns per row and 10,000 characters per cell. SKU limit 160 characters; displayed titles up to 1,000. Oversized catalogs fail instead of sampling. Optional unused fields are not included in normalized outputs. No network collection, automatic file downloads, dataset reads or external model calls.

### API, MCP and development

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('elfajad/product-catalog-reconciliation').call({
  mode: 'reconcile',
  supplierCatalog: { defaultCurrency: 'USD', rows: [{ sku: '001', cost: '8.00' }] },
  storeCatalog: { defaultCurrency: 'USD', rows: [{ sku: '001', price: '7.00' }] }
}, { memory: 256, timeout: 120, maxTotalChargeUsd: 19.01 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const full = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord('RECONCILIATION');
```

Available through Apify API and MCP after publication. Limited permissions; only the current run's output storage is needed. Treat supplier costs and private exports as confidential: do not publish them in Issues or public task examples.

Node.js 22+, Apify SDK (Apache-2.0) and csv-parse (MIT). Dependencies pinned. Run `npm ci`, `npm test`, `npm run sample`.

For workflow context, see [Shopify SKU guidance](https://help.shopify.com/en/manual/products/details/sku) and [Shopify CSV import/export guidance](https://help.shopify.com/en/manual/products/import-export/using-csv). Use Issues for problems with the run ID and non-sensitive field names; omit credentials and private catalog contents.

# Actor input Schema

## `mode` (type: `string`):

Fictional demo has no $19 report event. Reconcile uses your supplied exports and is billable.

## `supplierCatalog` (type: `object`):

Provide rows OR csvText, optional columns and defaultCurrency. 1–5,000 rows. Canonical fields: sku, title, barcode, cost, price, quantity, currency. Prices/cost and identifiers must be strings.

## `storeCatalog` (type: `object`):

Provide rows OR csvText. Map export headers with columns. Prices are decimal strings; quantity is a nonnegative integer. No URLs are fetched.

## `minimumGrossMarginPercent` (type: `number`):

Flags (retail price minus supplied unit cost) / retail price below this threshold, only with matching explicit currencies. Excludes other costs.

## `matchByBarcode` (type: `boolean`):

Opt-in fallback when no exact SKU conflict exists. GTIN checksum and uniqueness do not independently establish product identity.

## `skuMap` (type: `array`):

Optional one-to-one array of {supplierSku,storeSku}. Reserved unresolved rows are not matched by fallback.

## `reportLabel` (type: `string`):

Label for printable HTML and JSON.

## Actor input object example

```json
{
  "mode": "demo",
  "supplierCatalog": {
    "defaultCurrency": "USD",
    "rows": [
      {
        "sku": "001",
        "cost": "8.00",
        "price": "14.00",
        "quantity": "0"
      }
    ]
  },
  "storeCatalog": {
    "defaultCurrency": "USD",
    "rows": [
      {
        "sku": "001",
        "price": "7.00",
        "quantity": "10"
      }
    ]
  },
  "minimumGrossMarginPercent": 30,
  "matchByBarcode": false,
  "reportLabel": "Supplier and store catalog review"
}
```

# Actor output Schema

## `report` (type: `string`):

Report summary with HTML/CSV links and bounded evidence preview.

## `fullReconciliation` (type: `string`):

Every match and finding; no dataset preview truncation.

## `reviewQueue` (type: `string`):

Review-only CSV, not a platform import file.

## `normalizedRows` (type: `string`):

Source data row IDs, interpreted fields and field states.

## `runSummary` (type: `string`):

Delivery, demo and spending-limit outcome.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "demo"
};

// Run the Actor and wait for it to finish
const run = await client.actor("elfajad/product-catalog-reconciliation").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "mode": "demo" }

# Run the Actor and wait for it to finish
run = client.actor("elfajad/product-catalog-reconciliation").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "demo"
}' |
apify call elfajad/product-catalog-reconciliation --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,elfajad/product-catalog-reconciliation"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/VaQBZqbdTRoURc5lg/builds/apc7YtOT5my2VYgCq/openapi.json
