# Any Store Product Monitor: Price & Stock Tracker (`cleanpull/any-store-product-monitor`) Actor

Track price, stock and variants on any online shop's product pages: Shopify, WooCommerce, Magento, BigCommerce, Wix, Squarespace and more. Changes-only monitoring for competitor price watchlists, from $2 per 1,000 products; compact JSON for AI agents.

- **URL**: https://apify.com/cleanpull/any-store-product-monitor.md
- **Developed by:** [CleanPull](https://apify.com/cleanpull) (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 $1.20 / 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

## Any Store Product Monitor: Price & Stock Tracker

Give it **product page URLs from any online shop**, or just a store's address, and get each product's **price, list price, stock state, every variant with its own price, stock, SKU and barcode**, brand, images, rating and review count. It reads **Shopify, WooCommerce, Magento, BigCommerce, Wix, Squarespace, PrestaShop, Shopware, Salesforce Commerce Cloud** and custom stores. Schedule it and every row says **what changed since your last run**: price, stock, a sale starting or ending, variants, title, image, or the page being taken down. From **$1.50 per 1,000 products**.

### What you can do with it

- **"Tell me when a competitor changes a price or sells out."** Put their product URLs in a watchlist, turn on Changes only and schedule it daily: only products that moved come back, with the old and new value.
- **"Watch a competitor's whole store."** Give the store's address; the actor finds its product sitemap and reads up to the number of products you choose, filtered by a word or pattern in the URL.
- **"Which sizes are out of stock?"** Every variant comes with its own stock state, and units left where the store publishes them.
- **"Match competitor products to mine."** Barcodes (GTIN, EAN, UPC), SKUs and MPNs are returned wherever the store publishes them.
- **"Feed product data to an AI agent or a spreadsheet."** Compact JSON with the same field names for every store, a field picker, and a dataset view per job.
- **"Check the prices in another actor's results."** Point it at a dataset whose rows hold product URLs.

### How it reads a store

Each product is read the cheapest complete way, and every row says where its price came from (`priceSource`):

1. **Shopify stores:** the store's own product data (`/products/<handle>.js`), with every variant, its stock and barcode. No page load at all.
2. **Every other store:** the product page's structured data: JSON-LD (including variant groups), microdata, Magento's price box, the analytics product event, and Open Graph tags, merged with the most trusted first.
3. **WooCommerce:** when the page lacks a price, stock or variant prices, the store's public Store API fills them in, every variation included.

In tests on 51 product pages from 30 stores on 10 platforms, read from Apify's servers, it returned price and stock for **44**. The other 7 were Target and Best Buy (pointed to the actor that reads them), three pages that publish no product data, a removed IKEA product and a store that blocks automated visitors. A watchlist of 30 of those products read twice, minutes apart, reported **no false changes**.

### What makes the data right

- **A block is never an empty result.** Each store gets its own proxy ladder (direct, then datacenter, then residential only for stores that block the others), so one strict store never makes the rest of the run slower or pricier.
- **Partial pages are read again.** Some shops occasionally serve a page without its product data; the actor reads it once more on a fresh session before giving up.
- **Values from different sources are never mixed.** A price keeps its own source's currency and list price; a product that cannot be bought has no price rather than a price of 0.
- **Changes are real changes.** A price or stock state missing from one read carries over instead of reading as a change, prices in different currencies are not compared, and a re-uploaded image with a new cache key is not a new image.
- **Moved is not removed.** A product sent to the same brand's site for another country (brand.com → brand.co.uk) is reported as moved, with the new address, and never as removed.
- **Only public addresses are read.** A URL, sitemap or redirect pointing into a private network is never fetched.
- **Only delivered products are billed.** A page that could not be read is listed in the run summary with the reason, never charged. Sitemap pages that are not products are skipped for free.
- **Runs do not fail for one bad entry.** An invalid URL is skipped with a warning; an empty input runs the form's example.
- **Sites with their own CleanPull actor are routed, not guessed:** Amazon URLs point to Amazon Product Scraper, and Target and Best Buy to Barcode Price Checker & MAP Monitor. Neither they nor Walmart are fetched or charged.

### Input

| Field                                                            | What it does                                                                                                  |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `productUrls`                                                    | Product page URLs from any shop, one per line                                                                 |
| `sitemaps`, `maxProductsPerSitemap`, `urlFilter`                 | Store addresses or sitemap URLs: read the store's products, up to a limit, optionally only URLs matching text |
| `datasetId`, `datasetField`                                      | Another dataset whose rows hold product URLs                                                                  |
| `includeVariants`                                                | Every variant with its own price, stock, SKU and barcode (on by default)                                      |
| `readFullPage`                                                   | Shopify: also read the product page, for the review rating its markup adds                                    |
| `changesOnly`, `monitorName`, `changeTypes`                      | Return only products that changed since the last run with the same monitor name                               |
| `minPriceChangePercent`                                          | Ignore small price moves                                                                                      |
| `maxItems`, `fields`, `maxConcurrency`, `maxConcurrencyPerStore` | Limits, field selection and pace                                                                              |
| `proxyConfiguration`                                             | Your own proxy, tried after the direct attempt                                                                |
| `startUrls`                                                      | Accepted for compatibility with other actors (same as `productUrls`)                                          |

#### Example inputs

A competitor watchlist, checked daily for changes:

```json
{
  "productUrls": [
    "https://www.allbirds.com/products/mens-tree-runners",
    "https://www.zwilling.com/us/miyabi-evolution-5-inch-prep-knife-fine-edge--34025-133/34025-133-0.html"
  ],
  "changesOnly": true,
  "monitorName": "competitors"
}
```

The first 200 products of a store, only those with "bottle" in the URL:

```json
{ "sitemaps": ["nalgene.com"], "maxProductsPerSitemap": 200, "urlFilter": "bottle" }
```

Price drops of 5% or more and stock changes only:

```json
{
  "productUrls": ["https://www.monbento.com/catalog/product/view/id/5348"],
  "changesOnly": true,
  "changeTypes": ["price", "stock"],
  "minPriceChangePercent": 5
}
```

### Output

One row per product. A real row:

```json
{
  "recordType": "product",
  "url": "https://www.zwilling.com/us/miyabi-evolution-5-inch-prep-knife-fine-edge--34025-133/34025-133-0.html",
  "domain": "zwilling.com",
  "platform": "salesforce-commerce-cloud",
  "source": "url",
  "title": "MIYABI Evolution 5-inch Prep Knife, Fine Edge",
  "brand": "MIYABI",
  "gtin": "4009839334917",
  "mpn": "34025-133-0",
  "price": 129.99,
  "currency": "USD",
  "listPrice": null,
  "onSale": false,
  "availability": "InStock",
  "inStock": true,
  "priceMin": 129.99,
  "priceMax": 129.99,
  "variantCount": 0,
  "image": "https://www.zwilling.com/on/demandware.static/-/Sites-zwilling-master-catalog/default/dw35140085/images/large/34025-133-0_1.jpg",
  "rating": 5,
  "reviewCount": 70,
  "dataSources": ["json-ld", "open-graph"],
  "priceSource": "json-ld",
  "readThroughResidentialProxy": false,
  "changes": [],
  "firstSeen": true,
  "scrapedAt": "2026-10-13T22:10:01.035Z"
}
```

A variant (in `variants`) looks like this:

```json
{
  "id": "41884546990160",
  "sku": "A12498M080",
  "gtin": "196942291030",
  "title": "8",
  "options": { "Size": "8" },
  "price": 100,
  "listPrice": null,
  "currency": "USD",
  "availability": "OutOfStock",
  "inStock": false,
  "stockQuantity": null,
  "url": "https://allbirds.com/products/mens-canvas-runner-nz?variant=41884546990160"
}
```

A change looks like `{ "type": "price", "field": "price", "from": 129.99, "to": 109.99, "percent": -15.4 }`; variant changes name the variant. A product gone since the last run (its page answers 404, or redirects to the home page, a category or another site on two runs in a row; the first redirect is only a warning, not billed) gives one row with `"removed": true`, `removedHow` (`not-found` or `redirected`) and `redirectedTo`.

Also: `canonicalUrl`, `finalUrl` (after redirects), `sku`, `productId`, `category`, `discountPercent`, `stockQuantity` (where the store publishes it), `variantsInStock`, `images`, `description`, `condition`, `seller`, `priceValidUntil`, `sitemap` (for products found through one), `previous` (last run's values, on changed rows).

Dataset views: **Products**, **Variants** (one row per variant) and **Changes**. `RUN_SUMMARY` in the key-value store gives the outcome (`COMPLETE`, `PARTIAL`, `NO_CHANGES`, `EMPTY`, `FAILED`), the platforms seen, charges, and every product that could not be read, with the reason.

### Pricing

| Event                                                                                  | Free and Bronze | Silver | Gold and above |
| -------------------------------------------------------------------------------------- | --------------- | ------ | -------------- |
| Product (a delivered row)                                                              | $1.50           | $1.35  | $1.20          |
| Product checked, unchanged (Changes only)                                              | $0.50           | $0.45  | $0.40          |
| Product from a protected store (add-on, only for stores that block standard addresses) | $4.00           | $3.60  | $3.20          |

Per 1,000 products. Each run also has a $0.00005 start fee. The add-on applies once per product read, only when the store's product page had to be read through residential addresses; optional store API calls never trigger it. Pages read that way are capped at 600 KB transferred, compressed as sent (a larger one is not read or charged); Shopify product data and WooCommerce Store API answers (a few KB) never add it. A daily check of 100 competitor products with Changes only is about 3,000 checks a month: $1.50, plus the products that changed.

### Limits

- **Stores that publish no product data** in their pages (no structured data, analytics event or platform API) cannot be read; the run summary names them.
- **Large retailers** are not read here: Amazon, Target and Best Buy have their own CleanPull actors, and Walmart blocks automated visitors.
- **Prices are the store's public prices** for a visitor from the proxy's country, without a sign-in, coupons or cart discounts.
- **Gzipped sitemaps** (`.xml.gz`) are skipped; give the plain sitemap or product URLs.
- **robots.txt** is read only to find a store's sitemaps; its Disallow rules are not applied. Read a store at a pace and scale its terms allow (Products at a time per store is 2 by default).
- **Another dataset** (`datasetId`) is read only when you pick it in the form, which gives this actor read access to that one dataset. The actor runs with limited permissions and cannot see your other storages.
- **WooCommerce variations:** up to 100 per product.

### Changelog

- **0.1** (October 2026): products from any shop by URL, sitemap or dataset; Shopify product data, structured data, Magento, analytics and Open Graph reads; WooCommerce Store API with variations; per-store proxy ladder; changes since the last run, including removed pages.

# Actor input Schema

## `productUrls` (type: `array`):

Product page addresses from any online shop, one per line: Shopify, WooCommerce, Magento, BigCommerce, Wix, Squarespace, PrestaShop, Shopware, Salesforce Commerce Cloud and custom stores. Each row gives price, stock, variants and more.

## `sitemaps` (type: `array`):

Optional. A store address (brand.com) or a sitemap URL, one per line. The actor finds the store's product sitemaps (from robots.txt) and reads its product pages, up to the limit below.

## `urlFilter` (type: `string`):

Optional. For sitemaps: keep only product URLs containing this text (case-insensitive), or matching a /regular expression/.

## `maxProductsPerSitemap` (type: `integer`):

How many product URLs to read from each store's sitemaps.

## `datasetId` (type: `string`):

Optional. Another dataset (a previous run, or another actor's results) whose rows hold product URLs. Pick it here so this actor is given read access to it; it never sees your other storages.

## `datasetField` (type: `string`):

The field holding the product URL in that dataset. "url", "productUrl" and "link" are also tried.

## `includeVariants` (type: `boolean`):

List every variant (size, colour…) with its own price, stock, SKU and barcode where the store publishes them. On WooCommerce this can add one small request per product.

## `readFullPage` (type: `boolean`):

Shopify products are read from the store's product data alone (fast, small). On: also read the product page, for review ratings and anything else its markup adds.

## `changesOnly` (type: `boolean`):

Return only products that changed since the last run with the same monitor name (price, stock, sale, variants, title, image, page removed). Unchanged products cost $0.50 per 1,000. Every row always lists its changes; the first run is the baseline. Schedule it daily or hourly.

## `monitorName` (type: `string`):

Runs with the same name compare against each other. Use one name per watchlist.

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

Which changes count. A product whose only changes are left out counts as unchanged.

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

Ignore price moves smaller than this percentage. 0 reports every change.

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

Optional. Stop after this many product rows.

## `fields` (type: `array`):

Optional. Keep only these fields, for example \["url", "title", "price", "currency", "inStock"].

## `maxConcurrency` (type: `integer`):

Products read in parallel across all stores.

## `maxConcurrencyPerStore` (type: `integer`):

Products read in parallel from one store. Low values are polite and avoid blocks.

## `proxyConfiguration` (type: `object`):

Optional. The actor picks proxies itself per store: direct, then datacenter, then residential only for stores that block the others.

## `startUrls` (type: `array`):

Optional. Accepted for compatibility with other actors: product URLs, same as Product URLs.

## Actor input object example

```json
{
  "productUrls": [
    "https://www.allbirds.com/products/mens-tree-runners",
    "https://www.zwilling.com/us/miyabi-evolution-5-inch-prep-knife-fine-edge--34025-133/34025-133-0.html"
  ],
  "maxProductsPerSitemap": 100,
  "datasetField": "url",
  "includeVariants": true,
  "readFullPage": false,
  "changesOnly": false,
  "monitorName": "default",
  "changeTypes": [
    "price",
    "stock",
    "sale",
    "variants",
    "title",
    "image",
    "removed"
  ],
  "minPriceChangePercent": 0,
  "maxConcurrency": 5,
  "maxConcurrencyPerStore": 2
}
```

# Actor output Schema

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

One row per product, with variants and changes.

## `products` (type: `string`):

No description

## `variants` (type: `string`):

No description

## `changes` (type: `string`):

No description

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

Outcome (COMPLETE, PARTIAL, NO_CHANGES, EMPTY, FAILED), counts, charges and every product that could not be read, with the reason.

# 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 = {
    "productUrls": [
        "https://www.allbirds.com/products/mens-tree-runners",
        "https://www.zwilling.com/us/miyabi-evolution-5-inch-prep-knife-fine-edge--34025-133/34025-133-0.html"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("cleanpull/any-store-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 = { "productUrls": [
        "https://www.allbirds.com/products/mens-tree-runners",
        "https://www.zwilling.com/us/miyabi-evolution-5-inch-prep-knife-fine-edge--34025-133/34025-133-0.html",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("cleanpull/any-store-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 '{
  "productUrls": [
    "https://www.allbirds.com/products/mens-tree-runners",
    "https://www.zwilling.com/us/miyabi-evolution-5-inch-prep-knife-fine-edge--34025-133/34025-133-0.html"
  ]
}' |
apify call cleanpull/any-store-product-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cleanpull/any-store-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/87aqCiJ8E39BtwRc1/builds/Wn8EyKanPweVhNf0U/openapi.json
