# Shopify Price, Restock & New Product Monitor (`muhzuhaib/shopify-change-monitor`) Actor

Watch any list of Shopify stores and get only what changed: price moves, sale starts and ends, back in stock, out of stock, new and removed products, and variant changes. Webhook alerts to Discord, Slack or your own endpoint.

- **URL**: https://apify.com/muhzuhaib/shopify-change-monitor.md
- **Developed by:** [Muhammad Zuhaib Zahid](https://apify.com/muhzuhaib) (community)
- **Categories:** E-commerce, Automation, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 change detecteds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Shopify Price, Restock & New Product Monitor

Watch any list of Shopify stores on a schedule and get **only what changed**: price moves, sale
starts and ends, back in stock, out of stock, new and removed products, variant changes and
merchandising edits.

Most Shopify actors re-read the whole catalogue every run and bill you for every product. If you
watch a 5,000 product store daily, that is 150,000 rows a month to find the few hundred things that
actually moved. This one remembers the catalogue between runs and writes only the difference, so you
pay for signal instead of paying to re-read a shelf that did not move.

![Run summary](https://api.apify.com/v2/key-value-stores/fzPqfSUYe9NdjD4vQ/records/shopify-01-run-summary)

![Catalogue changes in the dataset](https://api.apify.com/v2/key-value-stores/fzPqfSUYe9NdjD4vQ/records/shopify-02-dataset-changes)

![Per store summary](https://api.apify.com/v2/key-value-stores/fzPqfSUYe9NdjD4vQ/records/shopify-03-store-summary)

![Discord alert](https://api.apify.com/v2/key-value-stores/fzPqfSUYe9NdjD4vQ/records/shopify-04-webhook-alert)

### What it detects

**Price and promotion**

| Change | What it means |
|---|---|
| `price_decreased` / `price_increased` | The variant price moved, with the absolute and percentage move |
| `sale_started` | A compare-at price appeared above the price, with the discount percentage |
| `sale_ended` | The compare-at price was removed |
| `discount_deepened` / `discount_reduced` | Still on sale, but the gap changed |

**Stock**

| Change | What it means |
|---|---|
| `back_in_stock` | That exact variant went from unavailable to available |
| `out_of_stock` | That exact variant sold out |
| `partial_sellout` | The product was fully available and some sizes or colours have now gone |

**Catalogue**

| Change | What it means |
|---|---|
| `product_added` / `product_removed` | A listing appeared or disappeared |
| `variant_added` / `variant_removed` | A size, colour or option was added or dropped |
| `published` / `unpublished` | The product was hidden or brought back |

**Merchandising and SEO**

`title_changed`, `description_changed`, `tags_changed`, `vendor_changed`, `product_type_changed`,
`handle_changed`, `sku_changed`, `images_changed`. A handle change rewrites the public product URL,
so it is worth knowing about if you link to competitor pages.

### Who uses it

- **Direct to consumer brands** watching competitor pricing and promotions.
- **Resellers and dropshippers** hunting restocks, price drops and new arrivals.
- **Buyers and retailers** watching supplier catalogues for availability.
- **Agencies** reporting on a client's competitive set.
- **Analysts** tracking discount depth and out of stock rates across a category.

### How it works

1. Give it one or more Shopify store domains. A bare domain, a full URL or a myshopify handle all
   work.
2. The first run records a baseline of the catalogue and reports no changes. This is deliberate: a
   brand new watch list is not news, and you are not billed change events for it.
3. Every run after that compares the live catalogue with what it remembers and writes only the
   difference.
4. Set a schedule, daily or hourly, and it becomes a standing watch.

Only public storefront JSON is read. No API key, no login, no proxy and no browser. It is fast and
cheap to run, and it does not touch any path that Shopify's robots.txt disallows.

### Output

Three kinds of row, each tagged with `recordType`.

**`change`**, one per change, flat so it exports cleanly to CSV and Excel:

```json
{
  "recordType": "change",
  "changeType": "price_decreased",
  "severity": "high",
  "detectedAt": "2026-09-10T09:14:02.113Z",
  "storeDomain": "examplestore.com",
  "storeName": "Example Store",
  "currency": "USD",
  "productId": "7369944137808",
  "productTitle": "Merino Runner",
  "productHandle": "merino-runner",
  "productUrl": "https://examplestore.com/products/merino-runner",
  "vendor": "Example",
  "productType": "Shoes",
  "variantId": "41358994178128",
  "variantTitle": "10",
  "variantSku": "MR-10",
  "priceBefore": 125,
  "priceAfter": 98,
  "deltaAbsolute": -27,
  "deltaPercent": -21.6,
  "compareAtBefore": null,
  "compareAtAfter": 125,
  "discountPercent": 21.6,
  "inStockBefore": null,
  "inStockAfter": null
}
```

**`store_stats`**, one per store per run: catalogue size, variant count, out of stock share, on sale
share, price range, pages read, and the reason for any skip or guard.

**`digest`**, one per store per run when digest mode is on, carrying the change list inside it.

### Trust and safety

Change detection can be dangerously wrong in ways that cost you money, so three rules are built in.

- **A partial read is never treated as a deletion.** If a page of the catalogue fails, the run keeps
  the previous memory, writes no change events and records the error on the store row. Without this,
  one bad network moment on a 5,000 product store would report 3,250 deleted products and bill you
  for every one of them.
- **A collapsed catalogue trips a shrink guard.** If a store answers with far fewer products than
  last time, the comparison is refused rather than inventing thousands of removals. A genuine mass
  delisting is picked up on the next run once the smaller catalogue is stable.
- **Caps stop a surprise bill.** A storewide promotion can flip every product at once.
  `maxChangesPerStore` and `maxChangesPerRun` bound what is reported and charged, keeping the most
  actionable changes and counting the rest on the store row.

Prices are compared as integer amounts, so `10.00` and `10.0` are the same price. A missing
compare-at price is read the same whether the store writes `null`, an empty value or zero, so sale
flags do not flap on a product nobody touched.

A store that is not Shopify, is password protected or is rate limited is skipped with a plain
reason on its own row. One bad entry in your list never fails the run.

### Alerts

Set `webhookUrl` and every batch of changes is POSTed as JSON. Choose `generic` for a full envelope,
or `discord` or `slack` for a short readable message their incoming webhooks accept directly. Works
with n8n, Make, Zapier or your own endpoint. Changes to the same product are grouped into one line,
so a shoe restocking ten sizes reads as one item rather than ten.

Delivery failures are recorded on the store row and never fail the data run.

### Controlling cost and noise

- `changeTypes` reports only the types you care about. This is the main lever: only what you keep is
  charged.
- `minPricePercent` and `minPriceAbsolute` ignore small price moves.
- `includeKeywords`, `excludeKeywords`, `productTypes`, `vendors` and `tags` narrow to part of a
  catalogue.
- `maxChangesPerStore` and `maxChangesPerRun` are the hard ceilings.
- `digest` writes one row per store instead of one per change. It reduces rows and alerts, not the
  bill: detection is the work, so changes are still charged.

### Pricing

| Event | Price | When |
|---|---|---|
| Actor start | $0.00005 | Once per run, the platform default, so frequent scheduling stays cheap |
| Store scanned | $0.002 | Per store per run, only after the catalogue was read cleanly |
| Baseline product | $0.0002 | First run only, to record the starting point. A 5,000 product store costs $1.00 once |
| Change detected | $0.01 | Per change reported. The main cost, and the only one that scales with news |

A quiet day costs almost nothing. Nothing is charged for a store that was skipped, guarded or read
incompletely.

### Notes and limits

- Identity is the Shopify id. If a merchant deletes and recreates a product or variant it comes back
  with a new id, so it is reported as added rather than as a restock.
- If a store changes its currency, price and sale events are held for that run because the numbers
  are not comparable, and the store is rebaselined.
- `maxProductsPerStore` defaults to 10,000. Raise it for very large catalogues.
- Catalogue timestamps are not compared. Shopify updates them for reasons that never reach the
  storefront, so comparing them would report constant changes that are not real.

# Actor input Schema

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

One store per line. A bare domain, a full URL or a myshopify handle all work, for example allbirds.com, https://www.gymshark.com/collections/mens or acme.myshopify.com.

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

Changes reports what moved since the previous run. Baseline only records the current catalogue as the new reference point and reports no changes.

## `resetBaseline` (type: `boolean`):

Discards the saved fingerprint for every store in this run and starts again from the current catalogue. Use it after changing the store list or when a store has migrated.

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

The named key-value store that holds the catalogue history between runs. Change it to keep separate watch lists apart, for example one per client. Leave it alone unless you need that.

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

Leave empty to report every type. Narrowing this is the main way to control both noise and cost, because only the types you keep are charged.

## `minPricePercent` (type: `integer`):

Ignore price changes smaller than this percentage. Useful for stores that round prices constantly. 0 reports every move.

## `minPriceAbsolute` (type: `integer`):

Ignore price changes smaller than this amount in the store's own currency. 0 reports every move.

## `includeKeywords` (type: `array`):

Case insensitive. Matched against product title, handle, variant title, SKU, vendor, product type and tags.

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

Case insensitive, applied after the include list.

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

Exact match on the Shopify product type, case insensitive.

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

Exact match on the Shopify vendor, case insensitive.

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

Case insensitive. A product matches if it carries any one of the tags.

## `digest` (type: `boolean`):

Write one summary row per store per run, with the change list carried inside it, instead of one row per change. Fewer rows and one alert, same detection.

## `includeStoreStats` (type: `boolean`):

Catalogue size, out of stock share, on sale share, price range, and the reason for any skip or guard. Keep this on: it is where a skipped store explains itself.

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

Optional. Every batch of changes is POSTed here as JSON. Works with Discord and Slack incoming webhooks, n8n, Make, Zapier or your own endpoint.

## `webhookFormat` (type: `string`):

Generic sends a full JSON envelope with every change. Discord and Slack send a short readable message their incoming webhooks accept directly.

## `webhookMode` (type: `string`):

One request per store per run, or one request per change. Per change can be very chatty on a busy store.

## `maxChangesPerStore` (type: `integer`):

A safety cap so a storewide promotion cannot produce an unexpected bill. The most important changes are kept first and the rest are counted in the statistics row.

## `maxChangesPerRun` (type: `integer`):

The same cap across every store in the run.

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

Ceiling on catalogue size. Products beyond it are not read and not compared.

## `shrinkGuardPercent` (type: `integer`):

If a catalogue comes back smaller than this share of the previous run, the comparison is refused rather than reporting thousands of deletions that did not happen. A genuine mass delisting is picked up on the following run.

## `requestTimeoutSecs` (type: `integer`):

Per HTTP request.

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

Lower this if you are watching many stores on one small host.

## Actor input object example

```json
{
  "stores": [
    "www.allbirds.com"
  ],
  "mode": "changes",
  "resetBaseline": false,
  "stateStoreName": "shopify-change-monitor-state",
  "changeTypes": [],
  "minPricePercent": 0,
  "minPriceAbsolute": 0,
  "includeKeywords": [],
  "excludeKeywords": [],
  "productTypes": [],
  "vendors": [],
  "tags": [],
  "digest": false,
  "includeStoreStats": true,
  "webhookFormat": "generic",
  "webhookMode": "batch",
  "maxChangesPerStore": 500,
  "maxChangesPerRun": 5000,
  "maxProductsPerStore": 10000,
  "shrinkGuardPercent": 40,
  "requestTimeoutSecs": 30,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

One row per detected change: price moves, sale starts and ends, stock flips, added and removed products and variants, and merchandising edits.

## `storeSummary` (type: `string`):

One row per store per run: catalogue size, out of stock share, on sale share, price range, and the reason for any skip or guard.

## `runSummary` (type: `string`):

Totals for the whole run: stores compared, baselined, skipped, guarded and errored, plus changes reported and HTTP requests made.

# 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": [
        "www.allbirds.com"
    ]
};

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

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,muhzuhaib/shopify-change-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/633N7qQFJTBv5VQiK/builds/pUlUZ4hbwRDsLZ2EN/openapi.json
