# Catalog Change Guard: CSV Price, Stock & SKU Diff (`jtpalms/catalog-change-guard`) Actor

Compare two retail CSV or TSV feeds before publishing. Detect removed SKUs, large price changes, currency changes and stock loss. Get exact counts, a BLOCK/REVIEW/CLEAR result and up to 500 example findings. $2 per comparison, including platform usage.

- **URL**: https://apify.com/jtpalms/catalog-change-guard.md
- **Developed by:** [JT Palms](https://apify.com/jtpalms) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2,000.00 / 1,000 completed catalog comparisons

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

## Catalog Change Guard

Catch unexpected price, stock and SKU changes before replacing a retail product feed.

Compare a baseline CSV/TSV with a candidate export. Get one JSON report containing exact totals, a `BLOCK`, `REVIEW` or `CLEAR` policy status, and up to 500 example findings. Designed for retail catalog workflows and feed automation. No shop credentials, external scraping, or AI API keys are needed.

Cloud-tested on Apify with valid and invalid inputs, budget limits and same-run restart checks. This is a new product; no uptime or error-detection guarantee is offered.

### Example

Yesterday: two shirt variants at CAD 29.99, both in stock. Today: one variant disappears and the other is CAD 2.99. The report flags the removed SKU and the roughly 90% price drop, with `BLOCK` for the mass removal. The tool does not guess whether either change was intentional.

### Inputs

Paste two complete feeds with these default columns:

```csv
id,price,availability
SHIRT-BLUE-M,29.99 CAD,in_stock
SHIRT-BLUE-L,29.99 CAD,in_stock
```

Use `idColumn`, `priceColumn` and `availabilityColumn` for other header names. Both snapshots must use the same mappings, delimiter, ID rules and scope. A partial export is not comparable to a complete export.

Accepted delimiters: comma, tab or semicolon. UTF-8 text, quoted cells and a leading BOM are supported. IDs are case-sensitive strings; leading zeros are preserved. Surrounding whitespace is stripped. IDs must uniquely identify products or variants. A Shopify product export with repeated handles is not accepted unless you supply a unique per-variant key and a compatible availability column.

Prices must be nonnegative decimal values, with up to 12 integer digits and 6 decimal places; an uppercase three-letter currency suffix is optional. Bare prices use the `currency` input, default `CAD`. Currency codes are checked for shape, not against the ISO register. Mixed currencies are not converted. A currency change on an existing ID is blocking.

Availability accepts `in_stock`, `out_of_stock`, `preorder` or `backorder`; capitalization and spaces are normalized. Duplicate IDs, malformed rows, missing columns and invalid values fail the input before the comparison event is charged. Failure logs do not print feed contents.

Limits: 20,000 product rows and 8 MiB of UTF-8 text per feed. No URLs, XML, Excel workbooks, automatic store connections, or scheduled fetching in this release.

### What the report means

| Status | Meaning |
| --- | --- |
| BLOCK | At least one configured blocking condition: empty candidate, too many removals, too much in-stock loss, a zero candidate price, or currency change. |
| REVIEW | No blocking condition, but a removal, large price change, or availability change away from in-stock needs review. |
| CLEAR | These specific rules found no blocking or review condition. Informational changes may exist. |

The default large-price threshold is **at least 20%** in either direction. Default mass-removal threshold is **over 5%** of baseline products. Default stock-loss threshold is **over 50%** of baseline in-stock products; removed in-stock products count as lost. Thresholds are editable.

Counts cover the complete accepted feeds. Detail examples stop at 500 and `detailsTruncated` explicitly reports truncation. A product can produce multiple findings; findings are not unique product counts. All details are deterministically ordered, starting with aggregate blocks.

**CLEAR is not a guarantee of a safe upload or channel approval.** This checks only mapped IDs, price, currency and availability. It does not verify images, product descriptions, inventory quantities, channel-specific policy, landing pages, tax or profit. The tool never changes a feed or connects to a shop. Integrations must stop on failed runs, missing reports or truncated detail if complete detail is required.

### Automation

Pass `beforeCsv` and `afterCsv` from your existing feed pipeline via Apify's API. The dataset contains exactly one report per completed run. Gate your own next step on `policyStatus`, your business rules, and successful run completion. You supply the baseline and candidate every time; this Actor does not retain a shared baseline across customers.

### Pricing

**USD 2 per completed comparison**, event `catalog-audit`, including platform usage in the event price. This is one event per report, not per row or warning. There is no separate Actor-start or dataset-item fee. Invalid input does not trigger this event. Set the maximum charge to at least USD 2; a lower budget produces no comparison report. Restarting the same completed run does not add another report or comparison event. Starting a new run is a new comparison. Check the Apify Pricing tab for the current price. No subscription or outside checkout.

### Data handling

Only provide retail product data you are authorized to process. Do not include customer records, emails, orders, credentials or personal data. Unused columns are ignored during comparison but are still present in the original input submitted to Apify. Remove them before submission. The processor makes no external network requests; inputs and reports are stored in the customer's Apify run storage and subject to Apify account retention and access settings. Deleting a local copy does not delete Apify storage. Do not share run links containing confidential feeds. No feed data is used for model training by this code.

This product is not offered for real estate or property-related workflows. It is independent of and not endorsed by Shopify, Google or any sales channel. No paid support or consulting upsell.

### Local verification

```sh
python3 -m unittest -v
python3 guard.py examples/risky-change.json --output report.json
```

The core uses only Python's standard library. The cloud entry point uses the pinned Apify SDK. See the operator's deployment record for cloud-test status; local tests do not prove platform billing.

# Actor input Schema

## `beforeCsv` (type: `string`):

Paste the earlier complete retail product feed. Maximum 20,000 rows and 8 MiB. Unique product/variant IDs required.

## `afterCsv` (type: `string`):

Paste the replacement feed, using matching headers and ID conventions. A header-only candidate is flagged as empty.

## `delimiter` (type: `string`):

The same delimiter is used for both feeds.

## `idColumn` (type: `string`):

Exact header for product or variant IDs. For SKU feeds, enter sku. IDs remain strings.

## `priceColumn` (type: `string`):

Exact price header. Values such as 29.99 or 29.99 CAD. No currency symbols or thousands separators.

## `availabilityColumn` (type: `string`):

Values must be in_stock, out_of_stock, preorder or backorder. Spaces and letter case are normalized.

## `currency` (type: `string`):

Three-letter uppercase currency code used where the price has no currency suffix. No conversion is performed.

## `priceChangeThresholdPct` (type: `number`):

Absolute percentage increase or decrease relative to each previous price. Zero-to-nonzero changes always require review.

## `removedThresholdPct` (type: `number`):

Removed IDs divided by baseline row count. Every removal is still reported below this threshold.

## `stockLossThresholdPct` (type: `number`):

Share of previously in-stock IDs that disappear or cease to be in stock. New IDs do not offset these losses.

## Actor input object example

```json
{
  "beforeCsv": "id,price,availability\nSHIRT-BLUE-M,29.99 CAD,in_stock\nSHIRT-BLUE-L,29.99 CAD,in_stock\n",
  "afterCsv": "id,price,availability\nSHIRT-BLUE-M,2.99 CAD,in_stock\n",
  "delimiter": ",",
  "idColumn": "id",
  "priceColumn": "price",
  "availabilityColumn": "availability",
  "currency": "CAD",
  "priceChangeThresholdPct": 20,
  "removedThresholdPct": 5,
  "stockLossThresholdPct": 50
}
```

# Actor output Schema

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

A dataset containing one report with exact summary counts, configured thresholds, up to 500 findings and a truncation flag.

# 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 = {
    "beforeCsv": `id,price,availability
SHIRT-BLUE-M,29.99 CAD,in_stock
SHIRT-BLUE-L,29.99 CAD,in_stock`,
    "afterCsv": `id,price,availability
SHIRT-BLUE-M,2.99 CAD,in_stock`
};

// Run the Actor and wait for it to finish
const run = await client.actor("jtpalms/catalog-change-guard").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 = {
    "beforeCsv": """id,price,availability
SHIRT-BLUE-M,29.99 CAD,in_stock
SHIRT-BLUE-L,29.99 CAD,in_stock
""",
    "afterCsv": """id,price,availability
SHIRT-BLUE-M,2.99 CAD,in_stock
""",
}

# Run the Actor and wait for it to finish
run = client.actor("jtpalms/catalog-change-guard").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 '{
  "beforeCsv": "id,price,availability\\nSHIRT-BLUE-M,29.99 CAD,in_stock\\nSHIRT-BLUE-L,29.99 CAD,in_stock\\n",
  "afterCsv": "id,price,availability\\nSHIRT-BLUE-M,2.99 CAD,in_stock\\n"
}' |
apify call jtpalms/catalog-change-guard --silent --output-dataset

```

## MCP server setup

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

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/aJY67fpI3fiVPkb7M/builds/JXF7BsnbjRSNgrVUI/openapi.json
