# PAL Shopify Catalog & Variant Change Tracker (`practical_ai_lab_jp/pal-shopify-catalog-change-tracker`) Actor

Recheck bounded public product feeds for newly listed, removed, repriced or changed variants. For Shopify operations and migration teams. Check the live Pricing tab and input limits.

- **URL**: https://apify.com/practical_ai_lab_jp/pal-shopify-catalog-change-tracker.md
- **Developed by:** [Practical AI Lab](https://apify.com/practical_ai_lab_jp) (community)
- **Categories:** E-commerce, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 product variant exporteds

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

## PAL Shopify Catalog & Variant Change Tracker

Practical AI Lab (PAL) Actor for pricing and market-research teams. It reads the **public** product feed of up to three Shopify storefronts, produces a clean **variant-level** dataset, and – when you hand it the previous snapshot – tells you exactly what changed:

- variants **added** or **removed**
- **price** changes (absolute and % delta)
- **compare-at price** changes
- **availability** flips (in stock ⇄ sold out)

> Store version: production-ready. Pay-per-event pricing is configured; the active plan is listed in the Pricing tab.

**Try a real public storefront:** the input form is pre-filled with `https://www.allbirds.com` and a low-cost starter limit of two products, one page. This public feed was reachable during a read-only test on October 24, 2026; availability can change. For a bigger snapshot, increase the product/page limits (up to 200 products per store). This tool is not affiliated with Allbirds or Shopify.

### How it works (buyer steps)

1. **Baseline run** – enter 1–3 store URLs (`https://shop.example.com`) and run. Every row has `changeType: "baseline"`.
2. Open the run's **Key-value store** and copy the `SNAPSHOT` record (JSON).
3. **Comparison run** (days or weeks later) – same store URLs, paste that JSON into **Previous snapshot**.
4. Download the dataset as **JSON or CSV**. Filter on `changeType` (`added`, `removed`, `changed`, `unchanged`) and `changeTypes` (`price_changed|compare_at_price_changed|availability_changed`). Tick off *Output unchanged variants* to get only the differences.
5. Save the new `SNAPSHOT` as the baseline for the next comparison.

`RUN_SUMMARY` in the same key-value store lists a status per store.

#### Example input

```json
{
  "storeUrls": ["https://shop.example.com"],
  "previousSnapshot": { "schemaVersion": 1, "variants": [ ... ] },
  "maxProductsPerStore": 200,
  "emitUnchanged": false
}
```

#### Example output row (changed)

```json
{
  "variantKey": "shop.example.com:7001:42001",
  "productKey": "shop.example.com:7001",
  "sourceStore": "shop.example.com",
  "productTitle": "Trail Bottle", "variantTitle": "750 ml", "sku": "TB-750",
  "price": "24.00", "previousPrice": "28.00", "priceDelta": "-4.00", "priceDeltaPercent": -14.29,
  "compareAtPrice": "28.00", "available": true, "previousAvailable": false,
  "changeType": "changed", "changeTypes": "price_changed|compare_at_price_changed|availability_changed",
  "currency": null,
  "productUrl": "https://shop.example.com/products/trail-bottle",
  "variantUrl": "https://shop.example.com/products/trail-bottle?variant=42001"
}
```

### Stable keys

`variantKey = <store host>:<Shopify product id>:<Shopify variant id>`. Keys use Shopify's numeric ids, so renamed titles or handles do not break comparisons. If a merchant deletes and recreates a product, Shopify issues new ids and it will show as removed + added.

### Safety behaviour

- Only `https://` store roots; reads only `/robots.txt` and `/products.json` (GET, no cookies, no login, no checkout, no customer data, no CAPTCHA handling).
- `robots.txt` is honoured; a Disallow covering `/products.json` ends that store's run. An unreadable robots file (403/429/5xx) also stops it.
- HTTP 403, 429, non-JSON or non-Shopify responses stop the store immediately (no retries, no workarounds).
- SSRF protection: hostnames must be public domains; every DNS answer must be a public IP (loopback, private, link-local, reserved, multicast and IPv4-embedding IPv6 ranges are refused); the connection is pinned to the validated IP; redirects are followed only over HTTPS to the *same host* and re-validated.
- Caps: 3 stores, 200 products/store, 8 pages/store, 5 MB/response, 15 s per request, 90 s and 20 requests per store.
- A store that fails emits **no rows** and is never charged. Failed stores are never treated as "everything removed".

### Limitations – please read

- **Availability is a boolean** (`available`) from the public feed. It is **not** an inventory count.
- **Currency is `null`.** Public `/products.json` does not reliably expose it; prices are the storefront's numeric amounts (formatted to 2 decimals). Market-specific or discount-code pricing is not reflected.
- Max 200 products per store. For larger catalogs the store is `partial_limit`, and removed-variant detection is skipped (otherwise unseen products would be reported as removed).
- Stores that disable or hide `/products.json`, password-protect the storefront, or disallow it in robots.txt cannot be read. Shopify may also return a subset of products on some themes/apps.
- Only data published by the store itself is returned; no customer, order or checkout information.
- Comparisons only cover stores present in the supplied snapshot (others are `not_compared`). Baseline rows for variants of a store that could not be read are simply left untouched in your own saved copy.
- The Actor is not affiliated with Shopify. Use it only on stores and for purposes your legal advice and the store's terms allow.

### Output views & schema

Dataset fields are listed in the **Variant snapshot & changes** view. Per-store statuses: `ok`, `partial_limit`, `robots_denied`, `blocked`, `no_public_feed`, `non_json`, `http_error`, `timeout`, `network_error`, `response_too_large`, `redirect_blocked`, `blocked_destination`, `robots_unavailable`, `request_limit`, `invalid_url`, `duplicate`, `stopped_charge_limit`, `skipped_charge_limit`.

### Pricing

Pay-per-event pricing is configured. The Actor charges the event `variant-emitted` once per successfully emitted current variant row (removed rows, failures and skipped stores are free) and stops cleanly when your run's charge limit is reached. It does nothing at all if that event is not configured.

### Development

```
python3 -m unittest discover -s tests   # offline, standard library only
```

# Actor input Schema

## `storeUrls` (type: `array`):

Public HTTPS storefront roots, e.g. https://shop.example.com. Only the store's public /products.json feed is read, and only if its robots.txt allows it.

## `previousSnapshot` (type: `object`):

Paste the SNAPSHOT JSON saved by an earlier run (key-value store record 'SNAPSHOT'), or leave empty to create a baseline. Max 20,000 rows.

## `maxProductsPerStore` (type: `integer`):

Starter sample loads 2 products. Increase to 200 for a broader snapshot; large catalogs are marked partial.

## `maxPagesPerStore` (type: `integer`):

Starter sample loads one feed page. Increase up to 8 for a larger catalog.

## `emitUnchanged` (type: `boolean`):

Turn off to output only added, removed and changed variants when comparing to a previous snapshot.

## Actor input object example

```json
{
  "storeUrls": [
    "https://www.allbirds.com"
  ],
  "maxProductsPerStore": 2,
  "maxPagesPerStore": 1,
  "emitUnchanged": true
}
```

# Actor output Schema

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

Structured records from this Actor run; downloadable as JSON or CSV.

# 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 = {
    "storeUrls": [
        "https://www.allbirds.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("practical_ai_lab_jp/pal-shopify-catalog-change-tracker").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 = { "storeUrls": ["https://www.allbirds.com"] }

# Run the Actor and wait for it to finish
run = client.actor("practical_ai_lab_jp/pal-shopify-catalog-change-tracker").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 '{
  "storeUrls": [
    "https://www.allbirds.com"
  ]
}' |
apify call practical_ai_lab_jp/pal-shopify-catalog-change-tracker --silent --output-dataset

```

## MCP server setup

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

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/NmCegM6X7jrrrvXFA/builds/44T6ApOh25hGtX7S3/openapi.json
