# Shopify Store Products Scraper & Price Change Monitor (`owlabee/shopify-catalog-monitor`) Actor

Scrape any public Shopify store's full catalog (every variant with price, compare-at price, availability, SKU, images) from its products.json, or monitor it and get price, stock and new/removed product change events. $0.001 per product/variant, $0.0005 per change.

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

## Pricing

from $1.00 / 1,000 product or variant scrapeds

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

## Shopify Store Products Scraper & Price Change Monitor

### What it does

This Actor reads the public catalog of any Shopify store straight from the storefront JSON that every Shopify shop exposes (`/products.json`, optionally `/collections.json`) and turns it into clean, flat records: one per **variant** by default (price, compare-at price, availability, SKU, barcode, options, image, weight) or one per **product**. Point it at one store or a hundred; it paginates each store 250 products at a time, normalizes prices to numbers, builds canonical product URLs and stamps every record with `fetchedAt`.

Switch it to **changes** mode and it becomes a price and stock monitor: it keeps the previous snapshot of each store in a key-value store you name, diffs the new catalog against it and writes only what changed, with `before`/`after` values and the percentage price move. Typical users are competitor price trackers, dropshippers and resellers watching supplier stock, brand teams auditing MAP compliance, affiliate and deal sites, and analysts building catalog datasets. There are no browsers, proxies or API keys involved, so it is cheap and fast, and the output is available as JSON, CSV, Excel, through the Apify API, webhooks, or the Apify MCP server for AI agents.

### How to use

1. Enter one or more **Shopify store URLs** (bare domains like `allbirds.com` or any URL on the store).
2. Pick a **Mode**: `snapshot` for the full catalog, `changes` to receive only price, stock, title and new/removed product events compared with the previous run. Optionally set **Granularity**, **Max products per store** and a **Webhook URL**.
3. Click **Start** (or schedule it daily). Records appear in the dataset as they are saved; download them or consume them via API, integrations or MCP.

### Input

| Field | Type | Required | Description |
|---|---|---|---|
| `storeUrls` | string\[] | yes | Stores to scrape; domains or any store URL, reduced to the origin |
| `mode` | `snapshot` | `changes` | no | Full catalog (default) or only changes since the last run |
| `granularity` | `variant` | `product` | no | One record per variant (default) or per product |
| `maxProductsPerStore` | integer | no | Pagination cap per store (default 5000) |
| `includeCollections` | boolean | no | Add a `collections` array of collection handles to each record (extra requests) |
| `stateStoreName` | string | no | Key-value store holding snapshots for `changes` mode (default `shopify-catalog-monitor-state`) |
| `webhookUrl` | string | no | POST target for the run summary and change sample |

### Output example

Variant record (`recordType` is `snapshot`, or `baseline` on the first `changes` run):

```json
{
  "recordType": "snapshot",
  "storeDomain": "www.allbirds.com",
  "productId": "7000000000001",
  "productHandle": "wool-runner",
  "productTitle": "Wool Runner",
  "vendor": "Allbirds",
  "productType": "Shoes",
  "tags": ["sneakers", "wool", "men"],
  "productUrl": "https://www.allbirds.com/products/wool-runner",
  "createdAt": "2024-02-20T09:00:00-07:00",
  "updatedAt": "2026-10-10T08:30:00-07:00",
  "publishedAt": "2024-03-01T10:00:00-07:00",
  "variantId": "41000000000001",
  "variantTitle": "Natural Grey / 9",
  "sku": "WR-GREY-9",
  "barcode": null,
  "price": 98,
  "compareAtPrice": 120,
  "available": true,
  "inventoryQuantity": null,
  "option1": "Natural Grey",
  "option2": "9",
  "option3": null,
  "imageUrl": "https://cdn.shopify.com/s/files/1/.../wr-grey.jpg",
  "grams": 600,
  "requiresShipping": true,
  "fetchedAt": "2026-10-16T12:00:00.000Z"
}
```

Change record (`changes` mode, from the second run on). `changeType` is one of `new-product`, `removed-product`, `new-variant`, `removed-variant`, `price-changed`, `availability-changed`, `title-changed`; all current variant fields from above are included as well:

```json
{
  "recordType": "change",
  "changeType": "price-changed",
  "changedFields": ["price"],
  "before": { "price": 98, "compareAtPrice": 120 },
  "after": { "price": 88.2, "compareAtPrice": 120 },
  "percentChange": -10,
  "detectedAt": "2026-10-17T12:00:00.000Z",
  "storeDomain": "www.allbirds.com",
  "productTitle": "Wool Runner",
  "variantTitle": "Natural Grey / 9",
  "price": 88.2,
  "available": true,
  "productUrl": "https://www.allbirds.com/products/wool-runner"
}
```

Error items look like `{ "storeDomain": "example.com", "recordType": "error", "error": "PASSWORD_PROTECTED", "errorMessage": "..." }` with codes `NOT_SHOPIFY`, `PASSWORD_PROTECTED`, `BLOCKED` (HTTP 403), `RATE_LIMITED`, `EMPTY_CATALOG`, `FETCH_FAILED` or `INVALID_STORE_URL`. They are charged like one product record, because the work to discover the problem was done.

### Pricing

Pay per event. You only pay for records the Actor writes; set a **maximum cost per run** in the run options to cap spend.

| Event | Price | Notes |
|---|---|---|
| Product or variant scraped | $0.001 | Per record in `snapshot` mode and per `baseline` record on the first `changes` run; error items too |
| Change detected | $0.0005 | Per change record in `changes` mode |
| Actor start | $0.00005 | Apify platform event |

**Examples:** 1,000 variants ≈ **$1.00**. A daily check of a 500-variant store that changes 20 prices ≈ **$0.01** + start fee (the first run writes the 500-record baseline for $0.50).

### Limits and fair use

- Requests to each store are spaced about 500 ms apart with bounded retries on 429/5xx, so a 5,000-product store takes roughly 15–20 s; two stores are processed in parallel.
- Only the public storefront JSON is read. Inventory quantities are not exposed by most stores and are `null` unless the store publishes them.
- Stores that are password-protected, not on Shopify, or that block datacenter traffic with a WAF return an error item; this Actor does not use residential proxies or browsers to get around that.
- `maxProductsPerStore` truncates the listing; in `changes` mode removals are not reported for a truncated store to avoid false "removed" events.
- Snapshots live in the key-value store named by `stateStoreName`, one key per store and granularity. Delete the key (or use a new store name) to start a fresh baseline.

### Related Actors

Other pay-per-event Actors from the same account, all limited-permission and agent-friendly:

- [Website Change Monitor — Sitemap & Page Content Diff](https://apify.com/owlabee/website-change-monitor) — sitemap and page content changes for stores and brands that are not on Shopify.
- [Domain WHOIS, DNS, Email Security & TLS Checker](https://apify.com/owlabee/domain-intelligence) — detect which domains in a list run on Shopify before you monitor them.
- [Bluesky Posts, Profiles & Followers Scraper](https://apify.com/owlabee/bluesky-monitor) — brand and product mentions, profiles and followers on Bluesky.

### FAQ

**Does this need proxies or an API key?** No. It reads the public `products.json` endpoint that Shopify serves for every storefront, using plain HTTP from Apify's standard infrastructure.

**How is cost controlled?** Set `maxProductsPerStore` and the run's maximum total charge. The Actor charges each record as it is written and stops cleanly when the limit is reached; in `changes` mode it then keeps the old snapshot so the next run re-detects anything it could not emit.

**Why does the first `changes` run write so many records?** There is nothing to compare against yet, so the full catalog is stored as `baseline` records (charged at the product rate). From the next run on you only receive change events.

**What exactly counts as a change?** Price, compare-at price, availability, product title and variant title per variant (per product with `granularity: product`), plus products and variants that appear or disappear. A brand-new product is one `new-product` event, not one per variant.

**Can I run it on a schedule or from an AI agent?** Yes. Use Apify Schedules for daily monitoring, call it from the API, or let agents discover it through the Apify MCP server; it is pay-per-event and uses limited permissions.

**Which fields can I filter on?** `recordType` (`snapshot`, `baseline`, `change`, `error`), `changeType`, `storeDomain`, `vendor`, `productType`, `tags`, `available`, `price`.

### Legal

This Actor accesses only data the store publishes to every visitor and respects per-host rate limits. Use the data in compliance with the store's terms and applicable law. It collects no personal data, is not a consumer reporting agency, and its output must not be used for employment, credit, insurance, or tenant screening decisions.

# Actor input Schema

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

Stores to scrape, one per line. Bare domains (allbirds.com) or any URL on the store (https://www.allbirds.com/collections/mens) both work; each is reduced to the store origin. Stores that are not Shopify, are password-protected or block datacenter traffic produce one error item instead of failing the run.

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

`snapshot` writes the current catalog (one record per variant or product). `changes` compares the catalog with the snapshot saved by the previous run in the state store and writes only change records (price, availability, title, new/removed products and variants). The first `changes` run for a store has nothing to compare against and writes the full catalog as `baseline` records.

## `granularity` (type: `string`):

`variant` (default) emits one record per variant with its own price, SKU and availability. `product` emits one record per product with the lowest variant price, price range, variant count and any-variant availability.

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

Stop paginating a store after this many products (250 products per request). Use it with the run's maximum cost to cap spend. When the cap is hit in `changes` mode, removed-product/removed-variant events are suppressed for that store because the listing was incomplete.

## `includeCollections` (type: `boolean`):

Also read /collections.json and each collection's products.json (up to 100 collections) and add a `collections` array of collection handles to every record. Costs one extra request per collection page; off by default.

## `stateStoreName` (type: `string`):

Named key-value store that keeps the last snapshot per store for `changes` mode. Use different names to run independent monitors on the same store; delete a key to re-baseline.

## `webhookUrl` (type: `string`):

If set, the Actor POSTs a JSON summary of the run (per-store counts, change counts by type and up to 200 change records) to this URL when it finishes. Delivery failures are logged, never fatal.

## Actor input object example

```json
{
  "storeUrls": [
    "allbirds.com",
    "www.deathwishcoffee.com"
  ],
  "mode": "snapshot",
  "granularity": "variant",
  "maxProductsPerStore": 100,
  "includeCollections": false,
  "stateStoreName": "shopify-catalog-monitor-state"
}
```

# Actor output Schema

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

One item per product variant or change event in the default dataset (storeDomain, recordType, changeType, productTitle, variantTitle, price, available, productUrl; error items carry error and errorMessage). Fields are described in dataset_schema.json.

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

SUMMARY record in the run's default key-value store: stores processed, products and changes produced, charges and webhook delivery status, written when the run finishes.

# 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": [
        "allbirds.com",
        "www.deathwishcoffee.com"
    ],
    "mode": "snapshot",
    "maxProductsPerStore": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("owlabee/shopify-catalog-monitor").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": [
        "allbirds.com",
        "www.deathwishcoffee.com",
    ],
    "mode": "snapshot",
    "maxProductsPerStore": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("owlabee/shopify-catalog-monitor").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": [
    "allbirds.com",
    "www.deathwishcoffee.com"
  ],
  "mode": "snapshot",
  "maxProductsPerStore": 100
}' |
apify call owlabee/shopify-catalog-monitor --silent --output-dataset

```

## MCP server setup

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

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/8nEQfexwEdTxzxiBL/builds/mQfazpbBXlRA6BgTb/openapi.json
