# Shopify Price & Stock Change Monitor (`duffy1990/shopify-price-change-monitor`) Actor

Watches Shopify stores and returns only what changed since the last run: price drops, price rises, out-of-stock, back-in-stock, new and removed products.

- **URL**: https://apify.com/duffy1990/shopify-price-change-monitor.md
- **Developed by:** [Duffy Radmilović](https://apify.com/duffy1990) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.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?

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 Price & Stock Change Monitor

Watches Shopify stores and returns **only what changed since your last run** — not another
full catalog dump you have to diff yourself.

Point it at one or more Shopify storefronts, run it on a schedule, and each run tells you
what dropped in price, what went out of stock, what came back, what is new and what is gone.

### What makes this different

Other Shopify scrapers hand you a snapshot of a catalog. To spot a price drop you have to
store yesterday's export, store today's, and compare them yourself.

This Actor keeps the previous catalog for you and does the comparison. A typical run on a
store with 2,000 products returns a handful of rows, not 2,000 — and you pay for those
few rows instead of the whole catalog, every single day.

### How it works

Only public, documented Shopify endpoints are used:

| endpoint | used for |
|---|---|
| `/meta.json` | store name, currency, and confirming the site really is Shopify |
| `/products.json?limit=250&page=N` | the public product catalog, page by page |

No login, no cookies, no captcha solving, no proxies, no AI. Plain HTTP and JSON.

The first run has nothing to compare against, so it saves a baseline and returns a one-row summary of the
store: products, in stock, sold out, on sale, price range and the newest products. From the second run on
you get the actual changes.

### Get the changes in a Google Sheet every morning

Pair this Actor with [Dataset to Google Sheets — No OAuth](https://apify.com/duffy1990/dataset-to-google-sheets)
and every change lands in a spreadsheet you already use. No Google login, no token that expires.

1. Create a task of this Actor with your competitors' stores and give it a daily **Schedule**.
2. Share your Google Sheet (Editor) with the e-mail shown in the Sheets Actor's README.
3. In this task → **Integrations** → connect an Actor → **Dataset to Google Sheets — No OAuth**, with input:

   ```json
   {
     "spreadsheetUrl": "https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit",
     "sheetName": "Price changes",
     "mode": "append",
     "datasetId": "{{resource.defaultDatasetId}}",
     "columns": ["detectedAt", "shopName", "changeType", "title", "variantTitle",
                 "priceBefore", "priceAfter", "priceChangePct", "availableAfter", "productUrl", "message"]
   }
   ```

That's it: each morning the tab gets one row per change (price drop, restock, new product...), or a single
`NO_CHANGES` row, so you can see the check ran. Tested end to end on 25 Sep 2026: this Actor's run started
the Sheets Actor on its own, and 200 real change rows landed in the sheet in the column order above.

**Cost for one store checked daily:** about $0.01 for the check + $0.003 per change here, plus $0.025 for
the sheet update, so roughly **$1–1.50 a month**.

### Input

```json
{
  "shopUrls": [
    "https://www.deathwishcoffee.com",
    "https://allbirds.com"
  ],
  "mode": "changes",
  "watch": ["price", "availability", "new", "removed"],
  "minPriceChangePct": 5,
  "direction": "down",
  "stateStoreName": "shopify-watch-state",
  "requestDelayMs": 300
}
```

| field | default | meaning |
|---|---|---|
| `shopUrls` | *(required)* | Store URLs or bare domains. |
| `mode` | `changes` | `changes` returns differences; `snapshot` returns the full catalog, one row per variant. |
| `watch` | all | Which change types to report. Reporting fewer types costs less. |
| `minPriceChangePct` | `0` | Ignore price moves smaller than this, to cut noise. |
| `direction` | `any` | `down` for price drops only, `up` for rises only. |
| `maxProductsPerShop` | `0` | Safety brake for huge catalogs. `0` means no limit. |
| `maxChangesPerShop` | `0` | Caps how many change rows one store can produce in a single run, so a site-wide sale cannot produce a surprise bill. `0` means no limit. |
| `stateStoreName` | `shopify-watch-state` | Named key-value store holding the previous catalog. **Runs that should be compared must use the same name.** |
| `resetState` | `false` | Forget the baseline and start over. |
| `requestDelayMs` | `300` | Pause between requests to the same store. |

### Output

One row per change. The row below uses real product data from
`deathwishcoffee.com` as captured by the Actor; `priceBefore` is the value the previous
run had stored, which is what makes it a change:

```json
{
  "shopDomain": "www.deathwishcoffee.com",
  "shopName": "Death Wish Coffee",
  "currency": "USD",
  "detectedAt": "2026-09-22T11:46:07.316Z",
  "changeType": "PRICE_DROP",
  "productId": "171104582",
  "variantId": "392715452",
  "handle": "valhalla-java-single-serve-pods",
  "productUrl": "https://www.deathwishcoffee.com/products/valhalla-java-single-serve-pods",
  "title": "Valhalla Java Single-Serve Pods",
  "variantTitle": "10 count",
  "sku": "5094SSC",
  "vendor": "Death Wish Coffee Company",
  "productType": "Coffee",
  "tags": ["Intro Offer", "Single Serve", "Subscription", "Valhalla Java"],
  "priceBefore": "16.99",
  "priceAfter": "14.99",
  "priceChange": -2,
  "priceChangePct": -11.77,
  "compareAtPrice": null,
  "availableBefore": true,
  "availableAfter": true,
  "imageUrl": "https://cdn.shopify.com/s/files/1/0271/7209/files/Valhalla_Java_10ct_Angle_Vertical_Pods_A1_2.jpg?v=1733925456",
  "publishedAt": "2021-02-12T12:07:04-05:00",
  "productUpdatedAt": "2026-09-22T04:55:13-04:00",
  "isFirstRun": false
}
```

`changeType` is one of:

| value | meaning |
|---|---|
| `PRICE_DROP` / `PRICE_RISE` | a variant's price moved |
| `OUT_OF_STOCK` / `BACK_IN_STOCK` | a variant's availability flipped |
| `NEW_PRODUCT` / `REMOVED_PRODUCT` | a product appeared or disappeared |
| `TITLE_CHANGED` | a product was renamed |
| `BASELINE_SAVED` | first run for this store; nothing to compare yet |
| `NO_CHANGES` | the store was checked and nothing changed (free row, so an empty day is never mistaken for a failed run) |
| `SNAPSHOT` | a row from `snapshot` mode |

#### Errors are rows, not just log lines

A store that cannot be read does not fail the whole run and does not hide in the log.
It produces a row you can filter on:

```json
{ "shopDomain": "shop.tesla.com", "error": "BLOCKED",
  "message": "Store blocked the request (HTTP 403) at https://shop.tesla.com/meta.json",
  "detectedAt": "2026-09-22T11:45:29.318Z" }
```

Error codes: `BAD_INPUT`, `NOT_SHOPIFY`, `BLOCKED`, `RATE_LIMITED`, `TIMEOUT`,
`HTTP_ERROR`, `NETWORK_ERROR`, `TOO_LARGE`, `UNEXPECTED_ERROR`, `CHARGE_LIMIT_REACHED`.

### Pricing

Pay per event:

| event | price |
|---|---|
| Actor start | $0.00005 |
| Store checked | $0.01 |
| Change detected | $0.003 |
| Snapshot item | $0.0003 |

Scanning a catalog costs the same whether anything changed or not, which is why a checked
store is billed separately from the changes found. A quiet day is cheap; it is not free.

**Example:** 20 stores, checked daily, roughly 60 changes a day
→ `20 × $0.01 + 60 × $0.003 = $0.38 per day`, about **$11.40 per month**.

Doing the same job with a catalog-dump scraper means exporting 20 × ~1,500 products at
around $0.001 per product — roughly **$30 for a single run**.

If a store runs a site-wide sale, one run can legitimately produce thousands of changes.
Use `maxChangesPerShop`, or Apify's own `maxTotalChargeUsd`, to put a ceiling on that.

### Limitations — please read before buying

- **Your maximum cost per run is respected.** When the next store would push the run over
  the limit you set, the Actor stops, bills nothing more, and adds a `CHARGE_LIMIT_REACHED`
  row listing the stores it did not check. If the limit is hit in the middle of one store's
  changes, the rest of that store's changes are held back and its baseline is not advanced,
  so they are reported on the next run instead of being lost.

These are real and measured, not boilerplate:

- **Roughly 10–20% of Shopify stores block automated requests.** Tested on 10 random
  stores: 7 worked, 1 rate-limited, 1 returned HTTP 403, 1 was not Shopify. Blocked stores
  return a `BLOCKED` row. No proxies are used, so there is no way around this.
- **Exact stock levels are not available.** Shopify's public `products.json` returns
  `inventory_quantity: null`. You get `available: true/false` only. Anyone promising
  public stock counts from this endpoint is guessing.
- **The first run reports no changes.** It saves the baseline and returns a store summary. Changes start with run two.
- **Only Shopify stores.** Other platforms return `NOT_SHOPIFY`.
- **About 100,000 variants per store** is the ceiling, set by the 9 MB key-value record
  limit at roughly 90 bytes of stored state per variant. The Actor runs at 512 MB and
  processes the catalog page by page, so catalogs in the tens of thousands of variants are
  fine — a 22,777-variant store peaks at well under half the limit.
- **If `maxProductsPerShop` is set**, products beyond the limit are invisible and are
  deliberately *not* reported as removed.
- **A partly downloaded catalog never overwrites the baseline**, so a network hiccup
  cannot turn into a fake "everything was removed" report.
- **No personal data.** Products, prices, and company information only.

### FAQ

**How much does it cost to monitor one store?**
One check is $0.01 plus $0.003 per change found. A store checked once a day with a few
changes costs roughly $0.30–$0.50 a month. See the pricing table above.

**How often should I run it?**
Once a day suits most competitor price tracking. Every few hours makes sense for restock
alerts on fast-selling items. Running more often than hourly rarely finds more and costs more.
Use an Apify schedule and keep the same `stateStoreName` so every run compares against the
previous one.

**Can I get a price drop or restock alert by email or Slack?**
Yes. Set `watch` to `["price"]` with `direction: "down"`, or `["availability"]` for
back-in-stock, then attach Apify's email, Slack or webhook integration to the Actor.
It only fires when there is a row, and rows only exist when something changed.

**What happens if a store blocks the Actor?**
You get a `BLOCKED` or `RATE_LIMITED` row for that store, the other stores still run, and
the baseline for the blocked store is left untouched. You pay for the store check, not for
changes that were never fetched.

**How do I make sure a big sale doesn't produce a big bill?**
Set `maxChangesPerShop`, or set a maximum cost per run in the run options. The Actor stops
cleanly at the limit and reports the rest on the next run.

**Can it track a competitor's whole catalog, not just changes?**
Yes, `mode: "snapshot"` returns one row per variant. For ongoing competitor price tracking,
`changes` mode is much cheaper.

**Does it work on non-Shopify stores?**
No. Other platforms return a `NOT_SHOPIFY` row.

### Legal

Uses public endpoints that Shopify serves to anyone. `/products.json` is not disallowed by
the default Shopify `robots.txt` (verified). Respect the terms of the stores you monitor,
and keep `requestDelayMs` at a polite value.

# Actor input Schema

## `shopUrls` (type: `array`):

Stores to watch. Paste the storefront URL or just the domain, e.g. https://www.deathwishcoffee.com or allbirds.com. Only real Shopify stores work — the Actor verifies this and tells you if a URL is not Shopify.

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

changes = return only what changed since the previous run (needs at least two runs). snapshot = return the full current catalog, one row per variant.

## `watch` (type: `array`):

Change types to report. Unchecking types you do not need reduces your bill, because you are charged per reported change.

## `minPriceChangePct` (type: `integer`):

Ignore price moves smaller than this, to filter out noise. 0 reports every change.

## `direction` (type: `string`):

Report only drops, only rises, or both.

## `maxProductsPerShop` (type: `integer`):

Safety brake for very large catalogs. 0 means no limit. Note: if you set a limit, products beyond it are invisible to the Actor and are NOT reported as removed.

## `maxChangesPerShop` (type: `integer`):

Safety brake against bill shock. A site-wide sale can change thousands of prices at once; this caps how many change rows one store can produce in one run. Extra changes are dropped and a note is added to the run log. 0 means no limit.

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

Named key-value store that remembers the previous catalog. Runs that should be compared to each other must use the SAME name. Use different names to watch the same store on independent schedules.

## `resetState` (type: `boolean`):

Forget everything and start over. The next run after this one will be the first one again, so it will report no changes.

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

Pause between page requests to the same store. Polite crawling; lower values risk HTTP 429.

## Actor input object example

```json
{
  "shopUrls": [
    "https://www.deathwishcoffee.com"
  ],
  "mode": "changes",
  "watch": [
    "price",
    "availability",
    "new",
    "removed",
    "title"
  ],
  "minPriceChangePct": 0,
  "direction": "any",
  "maxProductsPerShop": 0,
  "maxChangesPerShop": 0,
  "stateStoreName": "shopify-watch-state",
  "resetState": false,
  "requestDelayMs": 300
}
```

# Actor output Schema

## `changes` (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 = {
    "shopUrls": [
        "https://www.deathwishcoffee.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("duffy1990/shopify-price-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 = { "shopUrls": ["https://www.deathwishcoffee.com"] }

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

```

## MCP server setup

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