# Shopify Store Monitor – Price, Stock & New Product Tracker (`skuang/shopify-store-monitor`) Actor

Track competitors' public Shopify catalogs: new and removed products, price changes, and stock changes, plus clean normalized product data.

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

## Pricing

from $1.00 / 1,000 product 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 Monitor – Price, Stock & New Product Tracker

Watch any public Shopify store and get told what changed since the last run: new products, removed products, price changes, items going out of stock or coming back, renamed products, and added or removed variants. Every run also gives you the store's full catalog as clean, flat product rows.

Run it once to get a baseline, then schedule it (for example daily). Each later run compares the store with the previous run and outputs only what changed.

### Who it is for

- **DTC brands** keeping an eye on competitors' launches, discounts and stock-outs.
- **E-commerce agencies** reporting on clients' markets.
- **Resellers and price watchers** who want to know when a product drops in price or comes back in stock.

### What it does

1. Reads each store's public product feed (`https://<store>/products.json`), page by page, politely (one request per second per store, at most 3 stores in parallel).
2. Writes one `product` row per product: prices, sale status, stock, vendor, type, tags, image and (optionally) every variant.
3. Compares the catalog with the snapshot saved by the previous run for that store and writes one `change` row per change.
4. Saves the new snapshot for next time, but only if the store was read completely without errors.
5. Writes a run summary (per store: ok / partial / failed / skipped, counts, changes by type, errors) to the `OUTPUT` record.

### Input

| Field | Default | What it does |
|---|---|---|
| `storeUrls` (required) | – | Up to 50 store addresses, e.g. `https://www.allbirds.com` or just `allbirds.com`. Paths are ignored; only the standard ports (80/443) are accepted. |
| `maxProductsPerStore` | 2000 | Stop after this many products per store (max 25,000, which is also Shopify's own limit for this feed). |
| `trackChanges` | `true` | Compare with the previous run and output changes. |
| `outputMode` | `products_and_changes` | `products_and_changes`, `changes_only` (cheapest for daily monitoring) or `products_only`. |
| `includeVariants` | `true` | Add the variant list to every product row. |

Example:

```json
{
    "storeUrls": ["https://www.allbirds.com", "colourpop.com"],
    "maxProductsPerStore": 2000,
    "trackChanges": true,
    "outputMode": "changes_only"
}
```

### Output

All rows go to the run's dataset. The `type` field tells products and changes apart. The dataset has two views: **Products** and **Changes**.

#### Product row (`type: "product"`)

```json
{
    "type": "product",
    "store": "https://colourpop.com",
    "productId": 7567362162770,
    "handle": "neutral-swipe-shadow-stix-set",
    "url": "https://colourpop.com/products/neutral-swipe-shadow-stix-set",
    "title": "Neutral Swipe",
    "vendor": "ColourPop",
    "productType": "Shadow Stix",
    "tags": ["Birthday2026", "brand:Colourpop", "category:Bundles", "category:Set"],
    "createdAt": "2026-04-02T09:00:13-07:00",
    "updatedAt": "2026-10-11T13:07:09-07:00",
    "publishedAt": "2026-04-17T09:23:47-07:00",
    "minPrice": 77,
    "maxPrice": 77,
    "currency": "USD",
    "compareAtMin": 90,
    "compareAtMax": 90,
    "onSale": true,
    "variantsCount": 1,
    "availableVariants": 1,
    "inStock": true,
    "imageUrl": "https://cdn.shopify.com/s/files/1/1338/0845/files/BYOB_Shadow-Stix-Neutral_PDP-01_2000x2000_89956604-1d00-4be4-a8aa-c8cbe7c5a92d.jpg?v=1775252232",
    "scrapedAt": "2026-10-11T20:07:11.021Z",
    "variants": [
        {
            "id": 42358844227666,
            "title": "Default Title",
            "sku": "Bundle-NeutralSwipe",
            "price": 77,
            "compareAtPrice": 90,
            "available": true,
            "option1": "Default Title",
            "option2": null,
            "option3": null
        }
    ]
}
```

| Field | Meaning |
|---|---|
| `minPrice`, `maxPrice` | Lowest and highest variant price. |
| `currency` | The store's currency as reported by the store's `/meta.json`. `null` if the store does not report it; it is never guessed. |
| `compareAtMin`, `compareAtMax` | Lowest and highest "was" price, counting only variants whose compare-at price is above their current price. `null` when no variant is marked down. |
| `onSale` | `true` when at least one variant has a compare-at price above its price. |
| `availableVariants`, `inStock` | Number of variants that can be bought, and whether at least one can. `null` if the store hides availability. |
| `variants` | Only when `includeVariants` is on. |

#### Change row (`type: "change"`)

```json
{
    "type": "change",
    "store": "https://www.allbirds.com",
    "productId": 7340901859408,
    "handle": "womens-allbirds-flip-flop-dusty-pink",
    "url": "https://www.allbirds.com/products/womens-allbirds-flip-flop-dusty-pink",
    "title": "Women's Allbirds Flip Flop - Dusty Pink",
    "changeType": "price_changed",
    "variantId": null,
    "sku": null,
    "variantTitle": null,
    "before": 35,
    "after": 25,
    "currency": "USD",
    "detectedAt": "2026-10-11T20:10:28.576Z"
}
```

| `changeType` | Level | `before` → `after` |
|---|---|---|
| `new_product` | product | `null` → `{ title, minPrice, maxPrice, inStock, variantsCount }` |
| `removed_product` | product | `{ title, minPrice, maxPrice, inStock, variantsCount }` → `null` |
| `price_changed` | product or variant | old price → new price |
| `out_of_stock` | product or variant | `true` → `false` |
| `back_in_stock` | product or variant | `false` → `true` |
| `title_changed` | product | old title → new title |
| `variant_added` | variant | `null` → `{ title, sku, price, available }` |
| `variant_removed` | variant | `{ title, sku, price, available }` → `null` |

Product-level rows have `variantId`, `sku` and `variantTitle` set to `null`. How the level is chosen:

- **Price:** if every variant moved from the same old price to the same new price, you get one product-level row. Otherwise you get one row per variant whose price changed.
- **Stock:** if the product as a whole sold out (no variant available any more) or came back (at least one variant available again), you get one product-level row. Otherwise, while the product stays in stock, you get one row per variant (size, color, ...) that sold out or came back.
- **Removed products** are double-checked on the product's own page before they are reported, so a listing that shifted while it was being read does not produce false removals. Up to 20 products per store are checked per run; if all of them are confirmed gone, the rest are reported as removed too, otherwise the rest are kept and checked again next run.

#### Run summary (`OUTPUT` record)

The default key-value store holds an `OUTPUT` record with `totals` and one entry per store:

```json
{
    "input": "https://www.allbirds.com",
    "store": "https://www.allbirds.com",
    "status": "ok",
    "productsFetched": 692,
    "productsPushed": 692,
    "changesDetected": 3,
    "changesPushed": 3,
    "changesByType": { "new_product": 0, "removed_product": 1, "price_changed": 1, "back_in_stock": 0, "out_of_stock": 0, "title_changed": 1, "variant_added": 0, "variant_removed": 0 },
    "catalogComplete": true,
    "stopReason": null,
    "baseline": "compared",
    "currency": "USD",
    "storeReportedProductCount": 692,
    "shopDomain": "weareallbirds.myshopify.com",
    "errorType": null,
    "error": null
}
```

- `status`: `ok`, `partial` (some rows were delivered, then an error or the charge limit stopped it), `failed` (nothing could be read, for example not a Shopify store) or `skipped` (not processed: the charge limit was already reached, or the address leads to a store that is already in the list, by redirect or by the same Shopify shop domain).
- `errorType`: `fetch` (reading the store failed), `output` (writing rows to the dataset failed), `charge-limit`, `aborted`, `duplicate`, `input` or `internal`; `null` when everything worked. `productsPushed` and `changesPushed` always count the rows that were actually delivered, also when an error happened.
- `baseline`: `created` (first run for this store, no changes reported), `compared`, `kept` (the catalog could not be read to the end, so the previous snapshot was left untouched) or `not-tracked`.
- `stopReason`: `max-products` or `shopify-page-limit` when the catalog was larger than what was read.

The run fails only if every store failed. A store that fails is listed in `OUTPUT` and in the run's status message.

### How change tracking works

- **First run for a store = baseline.** It saves a snapshot and reports no changes. The status message says so.
- **Every later run** compares with the latest saved snapshot for that store, then replaces it.
- **Schedule it.** Use an Apify schedule (daily is typical). Changes are measured between consecutive runs.
- **Snapshots live in your account**, in a named key-value store called `shopify-store-monitor-state`, one record per store. They hold only what is needed for comparison (product title, handle, variant prices, availability and SKUs). Delete a store's record to start that store over with a new baseline.
- **Partial runs never overwrite the baseline with a partial catalog.** If a store could not be read to the end (network error, blocking, charge limit), its previous snapshot is kept. Otherwise every product that was not read would look "removed" next time.
- **Undelivered changes come back, delivered ones do not.** If the charge limit, an abort or a dataset error stops a run while change rows are being written, the snapshot records only the changes that were delivered. The next run reports the changes that were not delivered and none of the delivered ones. A product's change rows are delivered together: when the remaining budget cannot cover all of the next product's rows, the run stops before that product. Two cases can stop in the middle of one product's rows: a dataset error, and a product with more change rows than a whole run's budget allows (it is then delivered over several runs). In those cases, a product-level price or stock change of that product that was not delivered can come back as one row per variant, or, when a delivered variant removal already made the product sell out, not at all.
- **Large catalogs.** If a store has more products than `maxProductsPerStore` (or more than Shopify's 25,000 limit), removed products cannot be detected for it, and only products created or published after the previous run count as new.
- All runs of this actor in your account share these snapshots. Two schedules watching the same store will each see only the changes since the other one ran.

### Pricing

Pay per event. You pay only for rows actually delivered to your dataset:

| Event | When | Price |
|---|---|---|
| `product-scraped` | One product row written | $0.001 ($1.00 per 1,000 products) |
| `change-detected` | One change row written | $0.003 ($3.00 per 1,000 changes) |

Tip: for daily monitoring, use `outputMode: "changes_only"`. You then pay only for changes; product rows are not written.

If you set a maximum cost per run, the actor stops cleanly when it is reached and says so in the status message and in `OUTPUT`. Stores that were cut short are marked `partial`, stores that were not started are marked `skipped`, and changes that could not be delivered are reported on the next run (changes that were delivered are not reported or charged again).

### Limitations

- Works only with Shopify stores that expose the public `/products.json` feed. Some stores disable it, put the store behind a password, or block automated traffic; those stores are reported as `failed` with the reason.
- Reads only public catalog data. No access to sales numbers, inventory quantities, customers or anything behind a login.
- Only public internet addresses are contacted. Addresses that resolve to private, local or cloud-internal networks, and non-standard ports, are refused (also when a store redirects to them).
- Shopify's feed stops at 25,000 products per store.
- Prices are the ones the public feed returns. For stores that sell in several currencies or countries, a shopper elsewhere may see different prices; discounts applied at checkout are not included.
- Stock is the store's "available for sale" flag per variant; Shopify does not expose quantities in this feed.

### FAQ

**How do I know a store works?** Open `https://<store>/products.json` in your browser. If you see product data, the actor can read it.

**Why did the first run report no changes?** There was nothing to compare with yet. The first run saves the baseline; changes appear from the second run on.

**Can I track the same store with different settings?** Yes, but runs share one snapshot per store, so each run reports changes since the last run of any of them.

**How fast is it?** About one page of 250 products per second per store, with up to 3 stores in parallel. A 2,000-product store takes roughly 10 seconds.

**Do I need proxies?** No. The actor reads public feeds directly, with a clear user agent and at most one request per second per store.

# Actor input Schema

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

Storefront addresses of the Shopify stores to monitor, one per line. A full URL (https://www.allbirds.com) or a bare domain (allbirds.com) both work; any path is ignored. Each store is tracked separately. Up to 50 stores per run.

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

Stop reading a store's catalog after this many products. Shopify's public feed itself stops at 25,000 products. When a store has more products than this limit, removed products cannot be detected for it (the rest of the catalog was not read).

## `trackChanges` (type: `boolean`):

Compare each store with the snapshot saved by this actor's previous run and output change records (new or removed products, price changes, stock changes, title changes, added or removed variants). The first run for a store only saves the baseline. Schedule the actor (for example daily) to get changes.

## `outputMode` (type: `string`):

What to write to the dataset. "Products and changes": the full product list plus change records. "Changes only": only change records (cheapest for daily monitoring; needs change tracking on). "Products only": only the product list; no changes are computed and the saved snapshot is not touched.

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

Add a variants list (id, title, SKU, price, compare-at price, availability, options) to every product item. Turn off for smaller, flatter product rows. Change tracking always works at variant level either way.

## Actor input object example

```json
{
  "storeUrls": [
    "https://www.allbirds.com",
    "https://colourpop.com"
  ],
  "maxProductsPerStore": 2000,
  "trackChanges": true,
  "outputMode": "products_and_changes",
  "includeVariants": true
}
```

# Actor output Schema

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

No description

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

No description

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

// Run the Actor and wait for it to finish
const run = await client.actor("skuang/shopify-store-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": [
        "https://www.allbirds.com",
        "https://colourpop.com",
    ] }

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

```

## MCP server setup

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