# Shopify Product & Price Monitor: Catalog + Stock Alerts (`hydrocore/shopify-product-monitor`) Actor

Scrape any Shopify store's catalog (every variant's price, sale price, stock, SKU, images) or monitor it: get only price drops, increases, restocks, sell-outs, new and removed products since the last run. Multi-store, no browser.

- **URL**: https://apify.com/hydrocore/shopify-product-monitor.md
- **Developed by:** [Hydro Core](https://apify.com/hydrocore) (community)
- **Categories:** E-commerce, Automation, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 products

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Product & Price Monitor: Catalog + Stock Alerts

**Scrape any Shopify store's full catalog, then keep watching it.** Add one or many Shopify stores and get every product and variant with price, compare-at (sale) price, discount, stock status, SKU, options, images, vendor, type and tags. Turn on **monitoring** and each run outputs **only what changed** since the last run: price drops, price increases, back in stock, out of stock, new products, new variants and removed products.

One actor covers both the one-off catalog export and the ongoing price/stock watch. Change rows cost the same as product rows, and a run where nothing changed charges only a tiny per-store check fee.

### What can you do with it?

- **Competitor price monitoring:** schedule it every few hours and get a feed of price drops and price increases across competitor stores.
- **Restock & sell-out alerts:** keep only `back_in_stock` / `out_of_stock` changes and connect a webhook, Slack, email or Zapier/Make integration.
- **New product launch tracking:** see each competitor's new products and new variants (sizes, colors, flavors) the run after they appear.
- **Catalog export:** download a store's full catalog to CSV, Excel or JSON for analysis, product research or feeding an AI pipeline.
- **Deal finding:** filter for on-sale, in-stock items under a price, across many stores at once.
- **Assortment & pricing research:** compare vendors, product types, price ranges and discount depth between brands.

### How it works

1. **Give it stores.** Use a domain (`allbirds.com`), a homepage URL or a **collection URL** (`https://colourpop.com/collections/lips`) to fetch only that collection.
2. **It reads the store's public catalog feed.** Shopify stores publish their catalog as JSON at `/products.json`. The actor pages through it (250 products per request), reads `/meta.json` for the store name and currency, and normalizes every product and variant. No browser, no login, no proxy needed for normal stores.
3. **Catalog mode** (default) saves one row per product (variants nested) or one row per variant (flat, spreadsheet-friendly), after your filters.
4. **Monitoring mode** saves a compact snapshot of each store (variant prices, compare-at prices, availability) in a named key-value store. The next run compares against that snapshot and outputs only change rows. The first run of each store only saves the baseline, so there's no output and no row charges.
5. **It's polite and resilient.** Stores are fetched in parallel, but pages of one store are fetched one at a time with a configurable delay (default 500 ms). It honors robots.txt (including Crawl-delay) and backs off on HTTP 429/5xx. Stores that aren't Shopify, are password-protected or keep rate-limiting are listed in `RUN_SUMMARY` with a clear status and aren't charged.

### Why this actor

- **Change detection built in.** Most Shopify scrapers give you the whole catalog every run, and you have to diff it yourself. Here, monitoring mode returns only the changes, already classified (`price_drop`, `price_increase`, `back_in_stock`, `out_of_stock`, `new_product`, `new_variant`, `removed_product`), with old price, new price and change %.
- **Variant-level accuracy.** Changes are tracked per variant (size, color, pack), so "size 9 is back" or "the 10-count dropped 25%" is one row, not a vague product-level flag.
- **Safe, honest monitoring logic.** Removed-product detection only runs when the full catalog was fetched, so a capped or interrupted run never reports false removals. Changes that couldn't be delivered (result limit or max cost reached) stay pending and are reported next run instead of being lost.
- **Many stores per run,** each with its own snapshot. Collection URLs let you watch just one category.
- **Filters that apply before you pay:** keywords, excluded words, vendors, product types, tags, price range, in-stock only, on-sale only, minimum price-change %, and which change types to report.
- **Catalog-scraper pricing for monitoring:** $0.90 per 1,000 rows on the Free plan (down to $0.50 on Gold+), with change rows priced the same as product rows.
- **Fast and lightweight:** plain HTTPS + JSON at 256 MB memory. In our tests, 936 products from 3 stores took about 4 seconds of run time.

What it doesn't do: it doesn't log in or read private or admin data, and it doesn't return exact inventory quantities, barcodes/GTINs or reviews. It uses only what the store publishes in its public catalog feed. Headless storefronts that disable `/products.json` aren't supported and are reported as `not_shopify`.

### Input

| Field | What it does |
|---|---|
| `stores` | Domains, homepage URLs or collection URLs, one per line (required). |
| `monitorChanges` | `false` = catalog mode. `true` = output only changes since the previous run. |
| `changeTypes` | Which change types to report (default: all 7). |
| `minPriceChangePercent` | Ignore price changes smaller than this %. |
| `outputCatalogOnFirstRun` | Also output the catalog on a store's first monitoring run. |
| `monitoringStoreName` | Name of the key-value store holding snapshots. Use one per watchlist/task. |
| `outputLevel` | `product` (variants nested) or `variant` (one flat row per variant). |
| `descriptionFormat` | `text`, `html`, `both` or `none`. |
| `keywords`, `excludeKeywords`, `vendors`, `productTypes`, `tags` | Filters (case-insensitive). |
| `minPrice`, `maxPrice`, `onlyAvailable`, `onlyOnSale` | Price/stock/sale filters (applied per variant in variant mode). |
| `maxProductsPerStore`, `maxResults` | Limits (0 = no limit). |
| `requestDelayMs` | Delay between requests to the same store (default 500 ms). |

**Example: export catalogs**

```json
{
  "stores": ["allbirds.com", "deathwishcoffee.com", "https://colourpop.com/collections/lips"]
}
```

**Example: price & stock watch (schedule it every 6 hours)**

```json
{
  "stores": ["allbirds.com", "deathwishcoffee.com", "brooklinen.com"],
  "monitorChanges": true,
  "changeTypes": ["price_drop", "back_in_stock", "new_product"],
  "minPriceChangePercent": 5,
  "monitoringStoreName": "competitor-watch"
}
```

**Example: on-sale, in-stock variants under $60**

```json
{
  "stores": ["kyliecosmetics.com"],
  "outputLevel": "variant",
  "onlyOnSale": true,
  "onlyAvailable": true,
  "maxPrice": 60,
  "descriptionFormat": "none"
}
```

### Output

**Product row** (catalog mode, real data from allbirds.com, shortened):

```json
{
  "store": "allbirds.com",
  "storeName": "Allbirds",
  "currency": "USD",
  "productId": "6858599596112",
  "handle": "womens-tree-dasher-relay-blizzard-hazy-cobalt",
  "title": "Women's Tree Dasher Relay - Blizzard/Hazy Cobalt (Clarity Blue Sole)",
  "vendor": "Allbirds",
  "productType": "Shoes",
  "tags": ["allbirds::edition => limited", "allbirds::gender => womens", "..."],
  "url": "https://www.allbirds.com/products/womens-tree-dasher-relay-blizzard-hazy-cobalt",
  "priceMin": 67,
  "priceMax": 67,
  "compareAtPriceMax": 135,
  "onSale": true,
  "maxDiscountPercent": 50.4,
  "available": false,
  "variantCount": 13,
  "availableVariantCount": 0,
  "options": [{ "name": "Size", "values": ["5", "5.5", "6", "...", "11"] }],
  "variants": [
    { "variantId": "40234510123088", "variantTitle": "5", "sku": "A10692W050", "price": 67, "compareAtPrice": 135,
      "onSale": true, "discountPercent": 50.4, "available": false, "option1": "5", "option2": null, "option3": null, "grams": 638, "image": null }
  ],
  "featuredImage": "https://cdn.shopify.com/s/files/1/1104/4168/files/A10691_24Q1_Tree-Dasher-Relay-Blizzard-Hazy-Cobalt-Clarity-Blue_PDP_LEFT.png?v=1776183508",
  "imageCount": 5,
  "descriptionText": "Meet The Tree Dasher Relay — our laceless running shoe made from earth-friendly materials. ...",
  "createdAt": "2023-07-15T21:42:13-07:00",
  "publishedAt": "2026-09-25T16:49:33-07:00",
  "updatedAt": "2026-11-02T16:01:58-08:00",
  "sourceInput": "allbirds.com",
  "scrapedAt": "2026-11-03T00:01:58.910Z"
}
```

**Variant row** (`outputLevel: "variant"`): all product fields plus `variantId`, `variantTitle`, `sku`, `price`, `compareAtPrice`, `discountPercent`, `available`, `options` (e.g. `{"Size": "9"}`), `variantUrl` (`.../products/handle?variant=ID`) and `image`.

**Change row** (monitoring mode). This is the row format; the product is real, and the price change was triggered in our test by editing the stored snapshot:

```json
{
  "changeType": "price_drop",
  "store": "deathwishcoffee.com",
  "storeName": "Death Wish Coffee",
  "currency": "USD",
  "productId": "171104582",
  "productTitle": "Valhalla Java Single-Serve Pods",
  "variantId": "392715452",
  "variantTitle": "10 count",
  "sku": "5094SSC",
  "vendor": "Death Wish Coffee Company",
  "productType": "Coffee",
  "oldPrice": 19.99,
  "newPrice": 14.99,
  "priceChange": -5,
  "priceChangePercent": -25,
  "compareAtPrice": null,
  "wasAvailable": true,
  "available": true,
  "url": "https://www.deathwishcoffee.com/products/valhalla-java-single-serve-pods?variant=392715452",
  "image": "https://cdn.shopify.com/s/files/1/0271/7209/files/Valhalla_Java_10ct_Angle_Vertical_Pods_A1_2.jpg?v=1733925456",
  "previousCheckAt": "2026-11-03T00:09:32.436Z",
  "detectedAt": "2026-11-03T00:11:18.124Z",
  "sourceInput": "deathwishcoffee.com"
}
```

Change types: `price_drop`, `price_increase`, `back_in_stock`, `out_of_stock`, `new_product`, `new_variant`, `removed_product`.

The dataset has three ready-made table views: **Products**, **Variants** and **Changes**. Each run also writes a `RUN_SUMMARY` record to the default key-value store, with per-store status (`ok`, `baseline_saved`, `not_shopify`, `blocked`, `rate_limited`, `error`), products fetched, matches, rows saved and change counts.

### Pricing

Pay per event. You pay only for results:

| Event | Free | Bronze | Silver | Gold / Platinum / Diamond |
|---|---|---|---|---|
| **Product** (each saved product, variant or change row) | $0.0009 | $0.0008 | $0.0007 | $0.0005 |
| **Store check** (monitoring mode only, per store successfully checked) | $0.0005 | $0.0005 | $0.0005 | $0.0005 |
| Actor start | $0.00005 | $0.00005 | $0.00005 | $0.00005 |

Examples on the Free plan:

- Export 936 products from 3 stores ≈ 936 × $0.0009 = **$0.84**.
- Monitor 3 stores every 6 hours (120 runs/month) with no changes = 120 × (3 × $0.0005 + $0.00005) ≈ **$0.19/month**, plus $0.0009 per change found.

Stores that fail (not Shopify, blocked, rate-limited) are never charged. Your **Max cost per run** setting is respected: the actor stops saving rows when it's reached.

### Tips

- **Scheduling:** create a Task with `monitorChanges: true`, then add an Apify Schedule (hourly, every 6 hours, daily). Add a webhook or integration to get changes in Slack, email, Google Sheets, Zapier or Make.
- **One snapshot per watchlist:** use a different `monitoringStoreName` for each task so watchlists don't overwrite each other's snapshots.
- **Watch a category:** put a collection URL in `stores` instead of the whole store.
- **Noise control:** set `minPriceChangePercent` (e.g. 5) and keep only the change types you care about.

### FAQ

**Does it work with every Shopify store?**
It works with stores that publish the standard public `/products.json` catalog, which is most Shopify storefronts. Headless storefronts or stores that disable the endpoint return `not_shopify` in `RUN_SUMMARY` and aren't charged. In our tests, Allbirds, Death Wish Coffee, ColourPop, Brooklinen, Tentree and Kylie Cosmetics worked.

**How does monitoring know what changed?**
After each run it saves a compact snapshot per store (each variant's price, compare-at price and availability) in the key-value store named in `monitoringStoreName`. The next run compares the live catalog with that snapshot.

**Why did my first monitoring run return nothing?**
The first check of a store only saves the baseline. Changes appear from the second run. Set `outputCatalogOnFirstRun: true` if you also want the full catalog on that first run.

**Will I get false "removed product" alerts?**
Removal detection only runs when the store's whole catalog (or whole collection) was fetched. If you cap `maxProductsPerStore` or a run is interrupted, removals aren't reported for that run.

**What happens if I hit my result limit or max cost mid-run?**
Changes that couldn't be saved stay pending in the snapshot and are reported on the next run, so they aren't lost.

**Do I need proxies or a login?**
No. It reads public JSON over HTTPS. It doesn't log in and doesn't read anything that isn't publicly published by the store.

**Does it return inventory quantities, barcodes or reviews?**
No. It returns availability (in stock / out of stock) per variant, plus prices, SKUs, options and images. Exact quantities, barcodes and reviews aren't in the public catalog feed this actor uses.

**Which currency are prices in?**
The store's default currency, taken from the store's `/meta.json` and shown in the `currency` field. Prices are numbers, not strings.

**How fast is it, and how much memory does it need?**
256 MB is enough. In our Apify tests, 3 stores and 936 products finished in about 4 seconds of run time. Large catalogs take one request per 250 products, plus the politeness delay.

**Is scraping public Shopify catalogs allowed?**
The actor only reads publicly available catalog data, honors robots.txt and rate limits itself. You're responsible for how you use the data and for following each website's terms and applicable law.

# Actor input Schema

## `stores` (type: `array`):

One store per line. Accepts a domain (allbirds.com), a homepage URL (https://www.allbirds.com) or a collection URL (https://colourpop.com/collections/lips) to fetch only that collection. Works with any Shopify store that serves its public /products.json catalog; stores that don't are reported in RUN_SUMMARY and never charged.

## `monitorChanges` (type: `boolean`):

Off: output the catalog. On: compare with the previous run and output only change rows (price drop/increase, back in stock, out of stock, new product, new variant, removed product). The first run of each store saves a baseline silently. Schedule it (e.g. every 6 hours) to get a change feed. You pay only for changes.

## `changeTypes` (type: `array`):

Which changes to output in monitoring mode. Leave all selected for a full change feed, or keep only price_drop for deal alerts, back_in_stock for restock alerts, etc.

## `minPriceChangePercent` (type: `integer`):

Ignore price changes smaller than this percentage (0 = report every change).

## `outputCatalogOnFirstRun` (type: `boolean`):

By default the first monitoring run of a store only saves a baseline (nothing is output or charged). Turn on to also get the full catalog in that first run (charged per row).

## `monitoringStoreName` (type: `string`):

Name of the key-value store that remembers the last snapshot of each store. Use a different name for each saved task or watchlist.

## `outputLevel` (type: `string`):

One row per product (variants nested in a 'variants' array), or one row per variant (size/color) with product fields flattened, which is best for spreadsheets and price comparison. Each row is one charged result.

## `descriptionFormat` (type: `string`):

Plain text is best for spreadsheets and AI pipelines; HTML keeps the original formatting; None makes rows smaller.

## `keywords` (type: `array`):

Keep products whose title, product type, vendor or tags contain ANY of these words (case-insensitive). Empty = all products.

## `excludeKeywords` (type: `array`):

Drop products whose title contains any of these words, e.g. gift card, sample.

## `vendors` (type: `array`):

Keep only these vendors (exact match, case-insensitive). Useful for multi-brand retailers.

## `productTypes` (type: `array`):

Keep only these product types (exact match, case-insensitive), e.g. Shoes, Coffee.

## `tags` (type: `array`):

Keep products that have ANY of these tags (exact tag, case-insensitive).

## `minPrice` (type: `integer`):

Keep products with at least one variant priced at or above this (store currency).

## `maxPrice` (type: `integer`):

Keep products with at least one variant priced at or below this (store currency).

## `onlyAvailable` (type: `boolean`):

Catalog mode: keep only products with at least one variant in stock. (Ignored in monitoring mode so sell-outs are still reported.)

## `onlyOnSale` (type: `boolean`):

Catalog mode: keep only products where a variant has a compare-at price above its price. (Ignored in monitoring mode.)

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

Fetch at most this many products per store (0 = whole catalog). In monitoring mode a partial catalog disables removed-product detection.

## `maxResults` (type: `integer`):

Stop after saving this many rows across all stores. 0 = no limit (your 'Max cost per run' still applies).

## `requestDelayMs` (type: `integer`):

Politeness delay between page requests to the same store (robots.txt Crawl-delay is honored if larger). Stores are fetched in parallel, pages of one store one at a time.

## Actor input object example

```json
{
  "stores": [
    "allbirds.com",
    "deathwishcoffee.com",
    "https://colourpop.com/collections/lips"
  ],
  "monitorChanges": false,
  "changeTypes": [
    "new_product",
    "removed_product",
    "new_variant",
    "price_drop",
    "price_increase",
    "back_in_stock",
    "out_of_stock"
  ],
  "minPriceChangePercent": 0,
  "outputCatalogOnFirstRun": false,
  "monitoringStoreName": "shopify-product-monitor",
  "outputLevel": "product",
  "descriptionFormat": "text",
  "keywords": [],
  "excludeKeywords": [],
  "vendors": [],
  "productTypes": [],
  "tags": [],
  "onlyAvailable": false,
  "onlyOnSale": false,
  "maxProductsPerStore": 0,
  "maxResults": 0,
  "requestDelayMs": 500
}
```

# Actor output Schema

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

No description

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

No description

# 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 = {
    "stores": [
        "allbirds.com",
        "deathwishcoffee.com",
        "https://colourpop.com/collections/lips"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("hydrocore/shopify-product-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 = { "stores": [
        "allbirds.com",
        "deathwishcoffee.com",
        "https://colourpop.com/collections/lips",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("hydrocore/shopify-product-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 '{
  "stores": [
    "allbirds.com",
    "deathwishcoffee.com",
    "https://colourpop.com/collections/lips"
  ]
}' |
apify call hydrocore/shopify-product-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,hydrocore/shopify-product-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/XiG4RCqPsNKHkjkCe/builds/UrfI4pWxw4nw5UjCa/openapi.json
