# Shopify Products Scraper & Price Monitor - Stock, New Products (`neverempty/shopify-products-price-monitor`) Actor

For e-commerce teams tracking competitor stores: every product of any Shopify store from its public product list - variants, SKU, price, compare-at price, in stock or not, vendor, type, tags, image. Monitor mode returns only new products, price changes, sold-out and back-in-stock items.

- **URL**: https://apify.com/neverempty/shopify-products-price-monitor.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (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.85 / 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 Products Scraper & Price Monitor - Stock, New Products

Get **every product of any Shopify store** from the store's own public product list: **title, vendor, product type, tags, variants with SKU, price, compare-at price and whether each variant is in stock**, plus product URL, image and the published, created and updated times.

Turn on **monitor mode** and each run returns **only what changed since the last run: new products, price changes, sold-out and back-in-stock items, and new variants** - each with the previous price and stock state. Unchanged products are not returned and not charged as rows.

Unofficial. Reads only the standard public product list that Shopify stores serve at `/products.json`, after reading the store's `robots.txt`. No login, no Shopify account, no API key, no cart, no checkout.

### What you get

One row per product.

| Column | Example |
|---|---|
| `store`, `productId`, `handle`, `productUrl` | allbirds.com, 7340901859408, womens-allbirds-flip-flop-dusty-pink, https://allbirds.com/products/womens-allbirds-flip-flop-dusty-pink |
| `title`, `vendor`, `productType`, `tags` | Women's Allbirds Flip Flop - Dusty Pink, Allbirds, Shoes, the store's own tags |
| `price`, `priceMax` | 25, 25 - the lowest and the highest variant price |
| `compareAtPrice`, `onSale` | 50, true - the highest compare-at ("was") price among the variants, `null` when the store sets none; `onSale` is true when a variant's compare-at price is above its price |
| `available` | true when at least one variant is in stock; `null` when the store does not say |
| `variantCount`, `variants` | 7, `[{ "variantId": "42146889039952", "title": "5", "sku": "A12513W050", "price": 25, "compareAtPrice": 50, "available": false }, ...]` |
| `publishedAt`, `createdAt`, `updatedAt` | 2026-09-25T16:58:13-07:00 - as the store gives them, with its time zone offset |
| `imageUrl`, `imageCount` | the first product image, 5 |
| `changeType`, `changeTypes`, `changes`, `previousPrice`, `previousAvailable`, `previousRecordedAt` | monitor mode only, see below |
| `storeInput`, `watchName`, `checkedAt` | what you typed for this store, the watch name, when this run read the product |

Prices are numbers exactly as the store lists them. **The list carries no currency**, so no currency name is added: the numbers are in the currency the store answers with to a visitor from the United States without cookies. Stock is in stock / not in stock per variant; Shopify does not publish stock quantities in this list. A value the store does not give is `null`, never 0.

### Input

| Field | What it does |
|---|---|
| `stores` | Shopify stores, one per line: a domain (`allbirds.com`) or any URL of the store (only the domain is used). Up to 200 per run. Empty (with monitor mode off) = the first 50 products of one example store. |
| `maxProductsPerStore` | Stop after this many products of each store. Empty = the whole list. Not used in monitor mode. |
| `onlyChanges` | Monitor mode: return only new products, price changes, stock changes and new variants. |
| `watchName` | Monitor mode: name of the remembered state, so different store lists can be watched on their own schedules. |
| `resetMonitoringState` | Monitor mode: forget what the watch remembered for these stores and record a new starting point. |

Example - the full product list of two stores:

```json
{ "stores": ["allbirds.com", "colourpop.com"] }
```

Example - a daily price and stock monitor for a set of competitor stores:

```json
{ "stores": ["allbirds.com", "colourpop.com", "kyliecosmetics.com"], "onlyChanges": true, "watchName": "competitors" }
```

### Monitor mode: what counts as a change

Schedule the same input (same `stores`, same `watchName`) as often as you like. Each run compares every variant of every product with what the watch remembered:

| `changeType` | Meaning |
|---|---|
| `new` | a product the store did not list at the earlier checks |
| `price-changed` | the price of at least one variant changed |
| `back-in-stock` | at least one variant went from not in stock to in stock |
| `sold-out` | at least one variant went from in stock to not in stock |
| `new-variant` | an existing product got a variant it did not have |

A product with several kinds of change gets the first that applies in the order price-changed, back-in-stock, sold-out, new-variant; `changeTypes` lists all of them. `changes` lists each changed variant: `{ "type": "price-changed", "variantId": "...", "variantTitle": "5", "previousPrice": 100, "price": 79.99, "previousAvailable": true, "available": true }`. `previousPrice` and `previousAvailable` on the row are the product's lowest remembered price and whether any variant was in stock; `previousRecordedAt` is when those remembered values were recorded.

- **The first run of a watch for a store returns no product rows.** It remembers the product list as the starting point, counts one `store-checked` event, and adds a free `baseline-recorded` row that says how many products were remembered. Run without monitor mode if you want the full list as rows.
- If the starting point could not be finished (the list was not read to its end), products met for the first time are remembered without being returned as `new` until one run has read the whole list; products remembered earlier are compared and returned when they changed. A store that never lets the whole list be read (for example one that only answers through residential IP addresses and has more pages than the allowance of 6 residential requests per run covers) therefore never reports `new` products.
- For a store with more than 25,000 products, only the first 25,000 of its list are watched.
- With monitor mode on, the key-value store that holds the remembered state must be available; if it cannot be opened, the run requests nothing and charges nothing.
- A run with no change returns no product rows, only a free `no-change` row per store.
- **Products that disappear from the list are not reported.** A product that is missing from one answer is not called removed.
- A change is remembered only after its row was delivered. If the run reaches its maximum total charge first, the changes that were not delivered stay un-remembered and are returned by a run with a higher limit.
- Compare-at price, title, tags and images are not watched; a change only there does not return the product.
- Do not put the same store and `watchName` into two schedules that overlap in time. The remembered state lives in a key-value store named `shopify-products-price-monitor-state` in your account, and Apify key-value stores have no atomic update, so two runs finishing at the same moment can overwrite each other.

### Stores that cannot be read

Not every store serves this list, and the Actor does not work around the ones that do not. Each of these comes back as one free row with the reason in `note`:

| `status` | Meaning |
|---|---|
| `no-products-json` | the address answered 404 for `/products.json`: not a Shopify store, or the store switched the list off |
| `robots-disallowed` | the store's `robots.txt` disallows the product list request for all crawlers; it was not sent |
| `password-protected` | the store is behind a store password |
| `blocked` | the store refused the request (HTTP 403, 429 or 430) also when asked once more through a residential IP address, or showed a bot check page. Check pages are not solved or bypassed. At most 6 requests per run go through residential IP addresses |
| `redirected`, `unreadable` | the store sent the request somewhere else, or the answer was not a product list |
| `catalog-limit` | the store lists more than 25,000 products; Shopify does not serve this list beyond that |
| `no-products` | the store answered with an empty list |
| `duplicate-store` | two inputs led to the same store; it was read once |
| `budget-reached` | the run reached its maximum total charge; the row says what was not returned |
| `bad-input` | the input could not be used; nothing was requested |

On 2026-10-04, from Apify's servers, allbirds.com (692 products), colourpop.com (1,048) and kyliecosmetics.com (250) answered with their product lists, www.gymshark.com answered 403 also through a residential IP address, fashionnova.com's robots.txt disallowed the request and example.com answered 404. Stores change this over time.

### Pricing

Pay per event:

| Event | Price | When |
|---|---|---|
| `actor-start` | $0.001 | once per run, when the first product list was read |
| `product-returned` | $1.00 per 1,000 product rows (less on higher Apify plans) | each product row delivered |
| `store-checked` | $0.002 | monitor mode only: once for each store, when the first page of its product list was read and compared - whether or not anything changed, and also when a later page of that store could not be read (the row that explains it says so) |

Rows that explain why something was not returned are free. A run in which no store could be read is not charged. The Actor never delivers more rows than the maximum total charge you set for the run allows, and it does not start reading if that maximum has no room for the start fee together with one row (in monitor mode: one store check and one row).

Example: watching 10 stores once a day costs 10 x $0.002 + $0.001 = $0.021 per run when nothing changed, plus $0.001 for each changed product.

### Notes

- The Actor waits at least half a second between requests to a store and honours a `Crawl-delay` set for all crawlers in `robots.txt` (up to 10 seconds).
- If a domain redirects to another domain (for example to `www.`), the `robots.txt` of the new domain is read before the product list, and `store` shows the domain that answered.
- The same store given twice (with and without `www.`, or through a redirecting domain) is read once.
- Product descriptions (`body_html`) are not returned.

# Actor input Schema

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

Shopify stores, one per line: a domain such as allbirds.com or any URL of the store such as https://www.allbirds.com/collections/mens (only the domain is used). Up to 200 stores per run. The Actor reads the store's robots.txt first and then its public product list (/products.json). A store that is not on Shopify, has that list switched off, disallows it in robots.txt or refuses the request comes back as a free row that says why. If empty (and monitor mode is off), the first 50 products of one example store are returned.

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

Stop after this many products of each store (1-25000). If empty, the whole product list is returned (Shopify serves at most 25,000 products per store through this list). Not used in monitor mode: there the whole list is always compared, so that a change further down the list is not missed.

## `onlyChanges` (type: `boolean`):

Compare each store with the last run of the same watch name and return only products that are new, whose price changed, that sold out or came back in stock, or that got a new variant. Each row lists the changes with the previous price and stock state. The first run of a watch for a store only remembers the product list as the starting point and returns no product rows. Each store compared is charged one store-checked event; unchanged products are not returned and not charged as rows.

## `watchName` (type: `string`):

Monitor mode only: name of the remembered state used to compare runs (letters, digits, dot, dash, underscore; up to 40). Use a different name for each list of stores you track on its own schedule. If empty, the name "default" is used.

## `resetMonitoringState` (type: `boolean`):

Monitor mode only: forget the remembered products of the stores in this run before it starts, so this run records a new starting point and returns no product rows.

## Actor input object example

```json
{
  "stores": [
    "allbirds.com",
    "colourpop.com"
  ],
  "maxProductsPerStore": 100,
  "onlyChanges": false,
  "resetMonitoringState": false
}
```

# Actor output Schema

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

One row per Shopify product: store, product ID, handle, title, vendor, product type, tags, lowest and highest variant price, compare-at price, in stock or not, variants (ID, title, SKU, price, compare-at price, available), published, created and updated times, product URL and image. In monitor mode: changeType (new, price-changed, back-in-stock, sold-out, new-variant), the list of changes per variant and the previous price and stock state. A store that could not be read, is not on Shopify, disallows the list in robots.txt, an unusable input, a run with no change or a run that hit its maximum charge comes back as a free row that says why.

# 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",
        "colourpop.com"
    ],
    "maxProductsPerStore": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/shopify-products-price-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",
        "colourpop.com",
    ],
    "maxProductsPerStore": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/shopify-products-price-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",
    "colourpop.com"
  ],
  "maxProductsPerStore": 100
}' |
apify call neverempty/shopify-products-price-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/shopify-products-price-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/uO3WxvgUFpxTehoH3/builds/xiHEk9MriQHZhjLAy/openapi.json
