# Catalog Feed Diff: Price, Stock and SKU Change Detector (`althorlabs/catalog-feed-diff`) Actor

Compare two product feeds or price lists (CSV, JSON, XML, Google Merchant) and get every added, removed, repriced and out-of-stock product. Schedule it to monitor a supplier feed.

- **URL**: https://apify.com/althorlabs/catalog-feed-diff.md
- **Developed by:** [Althor Labs](https://apify.com/althorlabs) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 changed catalog rows

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 Feed Diff: Price, Stock and SKU Change Detector

Compare two versions of a product feed, supplier price list or catalog export
and get one row per product that changed: added, removed, repriced, out of
stock, back in stock, or edited (title, brand, any field you choose).

Works with CSV, TSV, JSON, JSON Lines and XML, including Google Merchant
(Google Shopping) feeds. Run it once on two files, or schedule it to monitor a
supplier feed and get only what changed since the last run.

### What you can use it for

- **Supplier price list changes.** See which wholesale prices went up or down
  before you update your store.
- **Stock monitoring.** Catch products that went out of stock or came back.
- **Feed QA before upload.** Check what a new Google Merchant or marketplace
  feed changes compared with the one currently live.
- **Catalog sync checks.** Find SKUs missing from one system after an import.

### How to use it

1. Give the **current feed** and the **previous feed**, as URLs or pasted text.
2. Leave the field settings empty. The Actor detects the key (SKU, id, GTIN,
   handle), price, availability and title fields automatically.
3. Run it. Each changed product is a row in the dataset. Export as CSV, Excel
   or JSON, or send it to Google Sheets, Slack or email with Apify
   integrations.

#### Monitoring mode (scheduled)

Turn on **Compare against the last saved run** and give only the current feed
URL. The first run saves a baseline. Every later run compares the feed with the
saved baseline, reports the changes, and saves the new version. Use a
different **Baseline name** for each supplier or store. Add an Apify schedule
(daily or hourly) and an integration to get the changes delivered.

For a restricted Apify token that can access selected storage resources by ID,
provide both **Baseline key-value store ID** and **Monitoring lock request
queue ID**. The store must be catalog-feed-reconciler-baselines; the queue must match
the selected baseline name. The Actor checks each resource name before
reading or writing the baseline. Leave both IDs empty for normal name-based
access.

In monitoring mode, the stored baseline takes precedence over the previous-feed
example in the prefilled input. A baseline advances only after the complete
comparison and all dataset rows finish within the run's charge limit. If a run
stops at a charge limit, its stored baseline stays unchanged so the next run
compares against the last complete snapshot. Rows written before the limit can
appear again on that next run.

### n8n workflow

Use the [Catalog Feed Monitor workflow JSON](https://raw.githubusercontent.com/althorlabs/n8n-workflows/main/catalog-feed-monitor.json)
to seed a baseline, check daily changes, and email changed products. See the
[template setup notes](https://github.com/althorlabs/n8n-workflows/blob/main/catalog-feed-monitor-README.md)
for import and credential setup. Its n8n template submission is pending
review.

### Input

- **Current feed / previous feed:** public HTTP or HTTPS URL (up to 50 MB, gzip
  supported) or pasted text. Redirects, URL credentials, and feeds behind
  authentication are not supported. Resolved private or local-network targets
  are rejected; URL paths and query strings are omitted from run metadata.
- **Feed format:** detected automatically; can be forced.
- **Items path:** for JSON documents where the products sit inside a key, such
  as `products` or `data.items`. Detected automatically.
- **Key, price, availability and title fields:** optional overrides. Dot paths
  work for nested JSON, for example `variants.0.price`.
- **Other fields to compare:** extra columns to watch, such as `brand` or
  `image_link`.
- **Minimum price change (%):** ignore small price moves.
- **Include unchanged items** and **include full item data:** optional.

### Output

One dataset row per changed product:

```json
{
  "changeType": "price_changed",
  "changeTypes": ["price_changed", "availability_changed"],
  "key": "G-1",
  "title": "Trail Running Shoe",
  "oldPrice": 89.0,
  "newPrice": 99.0,
  "priceChange": 10.0,
  "priceChangePct": 11.24,
  "currency": "USD",
  "oldAvailability": "in_stock",
  "newAvailability": "out_of_stock",
  "changedFields": []
}
```

`changeType` is one of `added`, `removed`, `price_changed`,
`availability_changed`, `fields_changed` (or `unchanged` when requested).
`changeTypes` lists every change on the item. `changedFields` lists other
field edits with old and new values.

The run's `OUTPUT` record in the key-value store has a summary: item counts,
the fields used, counts per change type (including price increases and
decreases, items that went out of stock, items back in stock) and any
warnings.

### How values are normalized

- **Prices:** `$1,299.00`, `1.299,95 EUR`, `24,50`, `USD 12` and plain numbers
  are parsed. Currency is read from symbols or ISO codes. A currency change
  counts as a price change.
- **Availability:** text such as `in stock`, `InStock`, `sold out`,
  `preorder`, `backorder`, schema.org URLs, booleans and stock quantities are
  mapped to `in_stock`, `out_of_stock`, `preorder` or `backorder`.
- **Keys:** compared without surrounding spaces and without case differences.
  Spreadsheet artifacts like `123.0` are treated as `123`.

### Limits

- Up to 500,000 items per feed (default 100,000). Larger feeds are rejected
  instead of being truncated, which could report false additions or removals.
- Feed URLs must be publicly reachable; authenticated feeds are not supported
  yet.
- URL redirects are rejected. Each URL fetch has a 60-second total deadline.
- Monitoring runs for the same named baseline are serialized with an Apify
  request-queue lock. A run that exceeds its lock window will leave the stored
  baseline unchanged.
- If no key field can be detected, the run stops and lists the columns it saw
  so you can set **Key field**.

### Pricing

This Actor uses pay-per-event pricing on Apify:

- **$0.02 per started 1,000 product items compared across both feeds.** The
  current feed and previous feed both count, including a saved baseline in
  monitoring mode. This charge applies even when the compared items produce no
  changed rows.
- **$0.0005 per changed product row** written to the default dataset.

There is no Actor-start fee. Standard platform usage costs are included in
these event prices. Check the [Apify listing](https://apify.com/althorlabs/catalog-feed-diff)
for current terms.

### Support

Open an issue on this Actor's Issues tab with an example of your feed
structure (a few rows are enough). We reply within one business day.

# Changelog

This Actor's version history is a separate document: https://apify.com/althorlabs/catalog-feed-diff/changelog.md

# Actor input Schema

## `currentFeedUrl` (type: `string`):

Public HTTP or HTTPS URL of the newer feed. It must resolve to a public IP. Redirects and URL credentials are rejected. Takes priority over pasted text.

## `currentFeedText` (type: `string`):

Paste the newer feed here if it has no URL.

## `previousFeedUrl` (type: `string`):

Public HTTP or HTTPS URL of the older feed. Leave empty when using a stored baseline.

## `previousFeedText` (type: `string`):

Paste the older feed here if it has no URL.

## `useStoredBaseline` (type: `boolean`):

Compare with the feed saved under the baseline name, then replace it only after a complete run. Previous-feed inputs are ignored in this mode.

## `baselineName` (type: `string`):

Use a different name per supplier or store so baselines stay separate. Runs for the same baseline are serialized.

## `baselineStoreId` (type: `string`):

Optional. Use with lockQueueId to grant a limited-permission run read/write access to the existing monitoring storage. Must identify the catalog-feed-reconciler-baselines store.

## `lockQueueId` (type: `string`):

Optional. Use with baselineStoreId in monitoring mode. Must identify the lock queue derived from this Baseline name. Both IDs are required together.

## `format` (type: `string`):

Leave on automatic detection unless parsing fails.

## `itemsPath` (type: `string`):

Dot path to the product list inside a JSON document, for example products or data.items. Detected automatically if empty.

## `keyField` (type: `string`):

Field that identifies a product in both feeds, such as SKU, id or gtin. Detected automatically if empty.

## `priceField` (type: `string`):

Detected automatically if empty. Dot paths work, for example variants.0.price.

## `availabilityField` (type: `string`):

Text such as in stock, booleans and quantities are all understood. Detected automatically if empty.

## `titleField` (type: `string`):

Detected automatically if empty. Title changes are reported.

## `compareFields` (type: `array`):

Extra fields to report when they change, such as brand, image_link or description.

## `minPriceChangePct` (type: `number`):

Ignore price changes smaller than this percentage.

## `includeUnchanged` (type: `boolean`):

Include unchanged products in the dataset output.

## `includeItemData` (type: `boolean`):

Attach the complete previous and current item to each row.

## `maxItems` (type: `integer`):

Feeds above this item count are rejected without truncation.

## Actor input object example

```json
{
  "currentFeedText": "SKU,Title,Price,Availability,Brand\nA-100,Stainless Water Bottle 750ml,$22.99,in stock,Hydra\nA-101,Insulated Lunch Box,$32.00,out of stock,Hydra\nA-102,Bamboo Cutlery Set,$12.50,in stock,GreenKit\nA-103,Collapsible Coffee Cup - Sage,$18.00,in stock,GreenKit\nA-105,Glass Meal Prep Containers,\"$1,099.00\",in stock,Prepco\nA-106,Silicone Storage Bags,$21.00,in stock,Prepco\nA-107,Reusable Produce Bags,$9.99,preorder,Wrapsy\n",
  "previousFeedText": "SKU,Title,Price,Availability,Brand\nA-100,Stainless Water Bottle 750ml,$24.99,in stock,Hydra\nA-101,Insulated Lunch Box,$32.00,in stock,Hydra\nA-102,Bamboo Cutlery Set,$12.50,out of stock,GreenKit\nA-103,Collapsible Coffee Cup,$18.00,in stock,GreenKit\nA-104,Beeswax Food Wraps (3 pack),$15.00,in stock,Wrapsy\nA-105,Glass Meal Prep Containers,\"$1,049.00\",in stock,Prepco\nA-106,Silicone Storage Bags,$21.00,in stock,Prepco\n",
  "useStoredBaseline": false,
  "baselineName": "default",
  "format": "auto",
  "compareFields": [
    "Brand"
  ],
  "minPriceChangePct": 0,
  "includeUnchanged": false,
  "includeItemData": false,
  "maxItems": 100000
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset rows for added, removed, repriced, availability-changed, or otherwise changed products.

## `summary` (type: `string`):

Structured run counts and source metadata from the OUTPUT key-value record.

# 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 = {
    "currentFeedText": `SKU,Title,Price,Availability,Brand
A-100,Stainless Water Bottle 750ml,$22.99,in stock,Hydra
A-101,Insulated Lunch Box,$32.00,out of stock,Hydra
A-102,Bamboo Cutlery Set,$12.50,in stock,GreenKit
A-103,Collapsible Coffee Cup - Sage,$18.00,in stock,GreenKit
A-105,Glass Meal Prep Containers,"$1,099.00",in stock,Prepco
A-106,Silicone Storage Bags,$21.00,in stock,Prepco
A-107,Reusable Produce Bags,$9.99,preorder,Wrapsy`,
    "previousFeedText": `SKU,Title,Price,Availability,Brand
A-100,Stainless Water Bottle 750ml,$24.99,in stock,Hydra
A-101,Insulated Lunch Box,$32.00,in stock,Hydra
A-102,Bamboo Cutlery Set,$12.50,out of stock,GreenKit
A-103,Collapsible Coffee Cup,$18.00,in stock,GreenKit
A-104,Beeswax Food Wraps (3 pack),$15.00,in stock,Wrapsy
A-105,Glass Meal Prep Containers,"$1,049.00",in stock,Prepco
A-106,Silicone Storage Bags,$21.00,in stock,Prepco`,
    "compareFields": [
        "Brand"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("althorlabs/catalog-feed-diff").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 = {
    "currentFeedText": """SKU,Title,Price,Availability,Brand
A-100,Stainless Water Bottle 750ml,$22.99,in stock,Hydra
A-101,Insulated Lunch Box,$32.00,out of stock,Hydra
A-102,Bamboo Cutlery Set,$12.50,in stock,GreenKit
A-103,Collapsible Coffee Cup - Sage,$18.00,in stock,GreenKit
A-105,Glass Meal Prep Containers,\"$1,099.00\",in stock,Prepco
A-106,Silicone Storage Bags,$21.00,in stock,Prepco
A-107,Reusable Produce Bags,$9.99,preorder,Wrapsy
""",
    "previousFeedText": """SKU,Title,Price,Availability,Brand
A-100,Stainless Water Bottle 750ml,$24.99,in stock,Hydra
A-101,Insulated Lunch Box,$32.00,in stock,Hydra
A-102,Bamboo Cutlery Set,$12.50,out of stock,GreenKit
A-103,Collapsible Coffee Cup,$18.00,in stock,GreenKit
A-104,Beeswax Food Wraps (3 pack),$15.00,in stock,Wrapsy
A-105,Glass Meal Prep Containers,\"$1,049.00\",in stock,Prepco
A-106,Silicone Storage Bags,$21.00,in stock,Prepco
""",
    "compareFields": ["Brand"],
}

# Run the Actor and wait for it to finish
run = client.actor("althorlabs/catalog-feed-diff").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 '{
  "currentFeedText": "SKU,Title,Price,Availability,Brand\\nA-100,Stainless Water Bottle 750ml,$22.99,in stock,Hydra\\nA-101,Insulated Lunch Box,$32.00,out of stock,Hydra\\nA-102,Bamboo Cutlery Set,$12.50,in stock,GreenKit\\nA-103,Collapsible Coffee Cup - Sage,$18.00,in stock,GreenKit\\nA-105,Glass Meal Prep Containers,\\"$1,099.00\\",in stock,Prepco\\nA-106,Silicone Storage Bags,$21.00,in stock,Prepco\\nA-107,Reusable Produce Bags,$9.99,preorder,Wrapsy\\n",
  "previousFeedText": "SKU,Title,Price,Availability,Brand\\nA-100,Stainless Water Bottle 750ml,$24.99,in stock,Hydra\\nA-101,Insulated Lunch Box,$32.00,in stock,Hydra\\nA-102,Bamboo Cutlery Set,$12.50,out of stock,GreenKit\\nA-103,Collapsible Coffee Cup,$18.00,in stock,GreenKit\\nA-104,Beeswax Food Wraps (3 pack),$15.00,in stock,Wrapsy\\nA-105,Glass Meal Prep Containers,\\"$1,049.00\\",in stock,Prepco\\nA-106,Silicone Storage Bags,$21.00,in stock,Prepco\\n",
  "compareFields": [
    "Brand"
  ]
}' |
apify call althorlabs/catalog-feed-diff --silent --output-dataset

```

## MCP server setup

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

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/hPQawsVIn5gAIhk8c/builds/oy6PmPagZt8M3mU0u/openapi.json
