# Shopify Store Monitor — Price, Stock & Catalog Changes (`johndowdev/shopify-store-monitor`) Actor

Monitors Shopify stores on a schedule and returns what changed since the last run: price moves, sales starting and ending, new and removed products, out-of-stock and back-in-stock variants. Pay per change, not per crawl.

- **URL**: https://apify.com/johndowdev/shopify-store-monitor.md
- **Developed by:** [John Dow](https://apify.com/johndowdev) (community)
- **Categories:** E-commerce, Lead generation, Open source
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 variant rows

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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 Store Monitor — Price, Stock & Catalog Changes

Give this Actor a list of Shopify store domains and it returns their catalogs **together with what changed since the previous run** — price moves, sales starting and ending, products appearing and disappearing, variants selling out and coming back. It is built to sit on a daily schedule: the first run records a baseline, and every run after that hands you a short list of changes instead of another full catalog dump.

***

### Copy this input and press Start

The default input works as-is — no editing required.

```json
{
  "storeUrls": ["https://www.deathwishcoffee.com"],
  "mode": "changes_only",
  "stateStoreName": "shopify-monitor-state",
  "maxProductsPerStore": 25,
  "includeUnavailable": true,
  "requestDelayMs": 400,
  "maxConcurrentDomains": 3,
  "proxyConfiguration": { "useApifyProxy": true }
}
```

**This default is a small sample — one store, 25 products — so trying the Actor costs a few cents. Raise `maxProductsPerStore` and add your own domains for real monitoring.** Rows are billed individually, so a full catalog is a deliberate choice rather than something the Start button drops you into; see [What it costs](#what-it-costs).

The first run writes every variant with `change_status: "baseline"` — that is the snapshot the next run compares against. It is a full catalog export and is priced like one. Run it again tomorrow and you get only what moved, at a fraction of the cost.

#### Input reference

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `storeUrls` | array of string | yes | — | Store domains or any URL from them. `shop.com`, `www.shop.com`, `https://shop.com/products/x` and `https://shop.com/collections/all` all work. Paths, query strings and redirects are resolved; duplicates are removed. |
| `mode` | enum | no | `changes_only` | `changes_only`, `changes_and_new` or `full_catalog` — see [Output modes](#output-modes). |
| `stateStoreName` | string | no | `shopify-monitor-state` | Named Key-Value Store holding one state record per domain between runs. |
| `compareDatasetId` | string | no | `null` | Build the baseline from a previous run's dataset instead of the state store. |
| `maxProductsPerStore` | integer | no | `5000` | Per-store safety cap. |
| `includeUnavailable` | boolean | no | `true` | Write rows for out-of-stock variants. |
| `productHandleFilter` | array of string | no | `[]` | Only output these product handles. |
| `vendorFilter` | array of string | no | `[]` | Only output these vendors (case-insensitive). |
| `minPrice` / `maxPrice` | integer | no | `null` | Price range filter, in the store's own currency. |
| `requestDelayMs` | integer | no | `400` | Pause between requests to the same store. |
| `maxConcurrentDomains` | integer | no | `3` | How many stores to process at once. |
| `proxyConfiguration` | object | no | Apify Proxy | Standard Apify proxy object. Datacenter by default. |

Filters apply to the **output only**, never to the stored state, so changing a filter between runs cannot corrupt change detection.

#### Output modes

| Mode | What lands in the dataset |
|---|---|
| `changes_only` | Price, sale and stock changes on variants you already saw, plus removals. New products and variants are **not** included. |
| `changes_and_new` | The above, plus `new_product` and `new_variant` rows. |
| `full_catalog` | Every variant on every run, changed or not. Use this for one-off catalog exports. |

In every mode, the first run for a domain writes the full catalog as `baseline` rows — there has to be something to compare against.

***

### Output

Two record types share the dataset, told apart by `record_type`. Both are completely flat, so CSV export keeps every field and adds no nested structures.

#### `record_type: "variant"` — one row per variant

A product with three sizes produces three rows, because price and availability live on the variant.

| Field | Type | Description |
|---|---|---|
| `domain` | string | Normalised host |
| `record_type` | string | Always `variant` for these rows |
| `change_status` | string | Exactly one of the statuses below |
| `product_id` | number | null | Shopify product ID |
| `product_handle` | string | null | Product slug |
| `product_title` | string | null | Product name |
| `vendor` | string | null | Brand |
| `product_type` | string | null | Product type |
| `tags` | string | Tags, comma-separated |
| `variant_id` | number | null | Shopify variant ID |
| `variant_title` | string | null | Variant name |
| `sku` | string | null | SKU |
| `option1` / `option2` / `option3` | string | null | Option values (size, colour, …) |
| `price` | number | null | Current price |
| `compare_at_price` | number | null | Struck-through price (`"0.00"` is normalised to `null`) |
| `currency` | string | null | Read from `/cart.js`; `null` when unavailable |
| `on_sale` | boolean | `compare_at_price > price` |
| `discount_pct` | number | null | Discount off the compare-at price, one decimal |
| `available` | boolean | In stock |
| `previous_price` | number | null | Set when the price changed |
| `price_change_pct` | number | null | Price change in percent, two decimals |
| `previous_available` | boolean | null | Set when availability changed |
| `product_url` | string | Direct link to the product |
| `image_url` | string | null | First product image |
| `published_at` | string | null | ISO timestamp |
| `scraped_at` | string | ISO timestamp of this run |

#### Change statuses

| Status | Meaning |
|---|---|
| `baseline` | First run for this domain — nothing to compare against yet |
| `new_product` | The product was not in the previous state |
| `new_variant` | The product was, this variant was not |
| `removed` | Present in the previous state, absent from the current catalog |
| `price_up` / `price_down` | The price moved |
| `sale_started` | `compare_at_price` went from empty or ≤ price to > price |
| `sale_ended` | The reverse |
| `back_in_stock` / `sold_out` | Availability flipped |
| `unchanged` | None of the above |

When several things change at once, one status wins, in this order:
`new_product` › `new_variant` › `removed` › `sold_out` › `back_in_stock` › `price_down` › `price_up` › `sale_started` › `sale_ended` › `unchanged`.

The losing changes are not lost — a variant that both sold out and dropped in price is reported as `sold_out` with `previous_price` and `price_change_pct` filled in.

#### `record_type: "store_status"` — one row per unreadable store

| Field | Description |
|---|---|
| `domain` | Normalised host |
| `record_type` | `store_status` |
| `store_status` | `catalog_restricted`, `password_protected`, `not_shopify` or `unreachable` |
| `detection_method` | Which check identified the store, e.g. `meta.json` |
| `message` | Plain-English explanation and what to do about it |
| `scraped_at` | ISO timestamp |

#### `RUN_SUMMARY`

A machine-readable report is written to the run's default Key-Value Store under the key `RUN_SUMMARY` — not into the dataset, so it never pollutes your export. It is written on every outcome, including aborts and failures.

```json
{
  "started_at": "2026-08-16T06:00:00.000Z",
  "finished_at": "2026-08-16T06:04:12.000Z",
  "domains_input": 12,
  "domains_ok": 9,
  "domains_catalog_restricted": 1,
  "domains_password_protected": 1,
  "domains_not_shopify": 0,
  "domains_unreachable": 1,
  "products_seen": 4210,
  "variants_seen": 9877,
  "rows_written": 143,
  "changes_by_status": { "price_down": 31, "new_product": 12, "sold_out": 4 },
  "truncated_domains": [],
  "truncation_suppressed_rows": {},
  "invalid_inputs": [],
  "duplicate_inputs": [],
  "errors": [{ "domain": "x.com", "reason": "timeout after 3 attempts" }],
  "mode": "changes_only",
  "status": "SUCCEEDED"
}
```

`status` is `SUCCEEDED` when every domain was resolved, `PARTIAL` when some failed but results exist, and `FAILED` when nothing could be processed.

***

### If a catalog is closed

`catalog_restricted` is a **result, not a bug**. It means the domain is definitely a Shopify store, but the owner has switched off the public product feed. There is no public catalog left to read, and no scraper can produce one without the owner turning it back on.

The Actor never fails silently on this. Every domain runs through a seven-step detection chain, and the first check that succeeds decides the answer:

| # | Check | Success signal |
|---|---|---|
| 1 | `GET /products.json?limit=250` | HTTP 200, JSON content type, body has a `products` array |
| 2 | `GET /collections/all/products.json?limit=250` | Same — some stores close the root path only |
| 3 | `GET /meta.json` | HTTP 200, JSON with `id` and `name` |
| 4 | `GET /cart.js` | HTTP 200, JSON with `token` or `currency` |
| 5 | Homepage response headers | `x-shopid`, `x-shopify-stage`, `x-sorting-hat-shopid` or `powered-by: Shopify` |
| 6 | Homepage cookies | `_shopify_y`, `_shopify_s` or `secure_customer_sig` |
| 7 | Homepage HTML | Contains `cdn.shopify.com`, `Shopify.shop` or `shopify-features` |

Which produces one of five honest outcomes:

| Status | Meaning | Charged? |
|---|---|---|
| `ok` | Check 1 or 2 succeeded — the catalog is extracted | yes |
| `catalog_restricted` | A Shopify store, but the public feed is off | no |
| `password_protected` | The storefront sits behind a Shopify password page | no |
| `not_shopify` | None of the seven checks matched | no |
| `unreachable` | DNS failure, timeout or connection error after retries | no |

Checks 1 and 2 are tried on the domain you supplied and on wherever its homepage redirects, because plenty of real stores redirect the homepage to a `www` or regional host while only the original host serves `/products.json`.

***

### Running it on a schedule

1. Run the Actor once and check the results.
2. Open the Actor → **Schedules** → create a schedule, e.g. `0 6 * * *` for 06:00 daily.
3. Keep `stateStoreName` the same across runs. That named Key-Value Store is what makes change detection work.

**How the state works.** After each successful run the Actor stores one compact record per domain, keyed `state-<host>`, mapping every `variant_id` to its price, compare-at price, availability, title and product handle. The next run reads that record, compares, and overwrites it. Storing a compact map rather than re-reading the previous dataset keeps runs cheap and fast.

A few properties worth knowing:

- **State is only written after a successful extraction.** If a store times out mid-crawl, its previous state is left untouched, so the next run does not report the whole catalog as removed.
- **Deleting the state store never breaks a run.** The next run simply treats every domain as a first run — which means a fresh baseline, priced as a full catalog export. Keep the state store to keep the runs cheap.
- **Different lists can run side by side** by pointing them at different `stateStoreName` values. Each name carries its own baseline, so each one pays for its own first run.
- **Prefer `compareDatasetId` for explicit control.** Set it to a previous run's dataset ID and the baseline is rebuilt from that dataset instead; the state store is then ignored and not updated. It works best against a dataset produced in `full_catalog` mode, since a `changes_only` dataset only lists rows that changed.

***

### What it costs

The Actor is priced per event, so on a schedule you pay for the changes, not for the crawl:

| Event | Price | When it is charged |
|---|---|---|
| Actor Start | $0.01 | Once per run, per GB of run memory (minimum one) |
| Store detected | $0.005 | Once per domain confirmed to be a Shopify store with a readable catalog |
| Variant row | $0.001 | Per variant row written to the dataset |

Nothing is charged for `not_shopify`, `unreachable`, `password_protected` or `catalog_restricted` domains — a domain that yields no catalog yields no bill, only a `store_status` row explaining why. `unchanged` variants cost nothing in `changes_only` mode because they are never written.

Rows on the first run for a domain are charged like any other row. A first run is a full catalog export, and that is what it is priced as; the saving comes from every run after it.

**Worked example — 10 competitor stores, ~2,000 variants each, daily for a month**, at 512 MB (one Actor Start event per run):

- First run, the baseline: $0.01 + 10 × $0.005 + 20,000 rows × $0.001 = **$20.06**.
- Each following run, with say 60 changed variants across the 10 stores: $0.01 + $0.05 + 60 × $0.001 = **$0.12**.
- 1 baseline run + 29 daily runs ≈ **$23.54 for the month**.

Exporting the same 10 catalogs in full every day instead — `full_catalog` mode, 20,000 rows per run — costs about $20.06 per run, roughly **$600 a month**. You pay the export price once, then about twelve cents a day. That gap is the whole point of the delta engine.

**If you set a spend limit, the run respects it.** When `maxTotalChargeUsd` is reached the Actor stops instead of crawling on: remaining stores are skipped, `RUN_SUMMARY` reports `charge_limit_reached: true` with `rows_written` reflecting what was really stored, and the run ends `PARTIAL`. Crucially, **no state is saved for a store that was cut off** — otherwise you would have paid for a partial catalog while the baseline recorded the whole one, making every row you never received look `unchanged` forever. Raise the limit, re-run, and the store is picked up from scratch.

Two things follow from this, worth planning around:

- **The baseline scales with catalog size, nothing else does.** A store with 30,000 variants costs $30 to baseline and then cents a day to monitor. Use `maxProductsPerStore` to put a ceiling on that first run, and keep the state store so you only pay it once.
- **A one-off export (scenario C) costs the export price, once.** Run `full_catalog`, take the data, walk away. There is no cheaper path — a fresh `stateStoreName` just buys another baseline at the same price.

***

### Known limitations

- **No exact stock counts.** `/products.json` exposes a boolean `available` flag only. There is no way to get units-on-hand from the public endpoint, so the Actor reports availability, not inventory.
- **Base currency only.** Multi-currency stores (Shopify Markets) serve their base currency on `/products.json`. Prices are the store's base prices, and `currency` comes from `/cart.js`.
- **Public catalogs only.** No anti-bot circumvention, no headless browser, no JS rendering, no logging in. If the owner closed the feed, the answer is `catalog_restricted`.
- **Recreated products look like a removal plus an addition.** When a store deletes and re-creates a product, Shopify issues new variant IDs, so you will see `removed` rows alongside `new_product` / `new_variant` rows. That is accurate — the identifiers really did change.
- **Truncated catalogs cannot prove absence.** If a store exceeds `maxProductsPerStore`, the run keeps what it read, lists the domain in `truncated_domains`, and skips `removed` detection. If the *previous* run was truncated, variants with no prior state are also withheld instead of being reported as `new_product`, since a partial sample cannot tell "new" from "not sampled last time". The count is reported per domain in `truncation_suppressed_rows`; raise `maxProductsPerStore` to fix it.
- **Store identity follows the host that serves the catalog.** Homepage redirects are often geo- or locale-dependent, so keying state on the redirect target would silently reset change detection whenever the proxy region moved. The domain you supply is used when it serves the catalog; the redirect target is still used to collapse `shop.com` and `www.shop.com` into a single store.
- **A locked storefront reports `password_protected`, not `catalog_restricted`.** Shopify password pages carry the same markers as any Shopify page, so this status takes precedence when checks 1 and 2 fail — it is the more useful of the two answers.

***

### Behaviour under load and failure

- **Politeness.** Requests to a single store are serialised with `requestDelayMs` between them; parallelism only happens across stores. `Retry-After` is honoured on HTTP 429.
- **Retries.** Up to 3 attempts per request with 1 s / 4 s backoff. 4xx other than 429 is not retried; a definitive DNS failure is not retried either.
- **Proxies.** Apify Proxy datacenter by default. A store answering HTTP 403 is retried once through a residential proxy, then left alone.
- **Failure isolation.** An exception on one domain never ends the run; it lands in `RUN_SUMMARY.errors` and the other domains carry on.
- **Memory.** Catalogs stream page by page rather than accumulating. Measured peak: 348 MB RSS while reading three 5,000-product stores concurrently (102,713 rows). 512 MB is a comfortable run size; the default 1-hour timeout suits lists of a few dozen stores.
- **Logging.** One summary line per domain — domain, status, products, variants, changes, duration. No raw JSON dumps.

***

### Development

```bash
npm install
npm run build
npm test           # 78 unit and integration tests
npm run test:e2e   # drives the built Actor across 5 runs against fake storefronts
npm run test:all   # both
npm start          # runs against local storage in ./storage
```

Neither suite touches the network. The unit tests spin up local fake storefronts covering every branch of the detection chain, compression handling, pagination and truncation. The end-to-end script runs the real entry point five times against a mutating fake catalog and asserts every change status, the store-status rows, `RUN_SUMMARY`, filters, idempotency and the empty-feed guard.

| Path | Contents |
|---|---|
| `src/normalize.ts` | Input URL normalisation |
| `src/detect.ts` | Seven-check detection chain and status resolution |
| `src/http.ts` | Per-domain client: politeness, retries, proxy escalation, decompression |
| `src/catalog.ts` | Streaming pagination |
| `src/delta.ts` | Change classification and priority |
| `src/rows.ts` | Flattening products into dataset rows |
| `src/state.ts` | Key-Value Store and dataset state backends |
| `src/main.ts` | Orchestration, run summary, charging |

Pay-per-event prices live in Apify Console under **Publication → Monetization**; `.actor/pay_per_event.json` is a reference copy, and the event names there must match the ones passed to `Actor.charge()`.

# Actor input Schema

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

Shopify store domains to monitor. Any format works — 'shop.com', 'www.shop.com', 'https://shop.com/collections/all' or a link to a single product. Paths, query strings and redirects are resolved automatically, and duplicates are removed. The default is a one-store sample so the first run costs cents; add your competitors here for real monitoring.

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

What ends up in the dataset. 'changes\_only' — price, sale and stock changes on variants you already saw, plus removals. 'changes\_and\_new' — the same plus newly added products and variants. 'full\_catalog' — every variant on every run, changed or not. The first run for a domain always writes the whole catalog as 'baseline' rows so there is something to compare against next time.

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

Name of the named Key-Value Store that holds one compact state record per domain between runs. Keep the same name across scheduled runs — that is what makes change detection work. Use different names to run independent monitoring sets side by side.

## `compareDatasetId` (type: `string`):

Optional. Build the comparison baseline from a specific previous run's dataset instead of the Key-Value Store. Works best with a dataset produced in 'full\_catalog' mode — a 'changes\_only' dataset only lists changed rows, so everything else would look new. When set, the state store is ignored and not updated.

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

Safety cap on how many products to read from a single store. This default is a small sample so you can try the Actor for cents — raise it for real monitoring, and remember that the first run for a store writes its whole catalog and is billed per row. When a catalog is larger than the cap, the run stops there, flags the domain in RUN\_SUMMARY.truncated\_domains, and skips 'removed' detection for it (a partial catalog cannot prove a product is gone).

## `includeUnavailable` (type: `boolean`):

Write rows for variants that are currently out of stock. Turning this off only hides them from the dataset — they are still tracked internally, so 'back\_in\_stock' still fires when they return.

## `productHandleFilter` (type: `array`):

Optional. Only output products whose handle (the slug in /products/<handle>) is on this list. Leave empty to monitor the whole catalog.

## `vendorFilter` (type: `array`):

Optional. Only output products from these vendors / brands (case-insensitive, exact match on the Shopify 'vendor' field). Leave empty for all vendors.

## `minPrice` (type: `integer`):

Optional. Only output variants priced at or above this value, in the store's own currency. Left empty by default (no lower bound); prices are whole numbers in the Console editor.

## `maxPrice` (type: `integer`):

Optional. Only output variants priced at or below this value, in the store's own currency. Left empty by default (no upper bound); prices are whole numbers in the Console editor.

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

Pause between consecutive requests to the same store. Requests to one store are never sent in parallel; this delay keeps the crawl polite. Raise it if a store starts returning HTTP 429.

## `maxConcurrentDomains` (type: `integer`):

How many different stores to process at the same time. Parallelism happens between stores only. Raise it for long domain lists if the run has enough memory.

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

Apify Proxy settings. Datacenter proxies are used by default; if a store answers HTTP 403, that one store is retried once through a residential proxy and then left alone.

## Actor input object example

```json
{
  "storeUrls": [
    "competitor-one.com",
    "https://competitor-two.myshopify.com"
  ],
  "mode": "changes_and_new",
  "stateStoreName": "shopify-monitor-state",
  "compareDatasetId": "aBcDeFgHiJkLmNoPq",
  "maxProductsPerStore": 5000,
  "includeUnavailable": true,
  "productHandleFilter": [
    "wool-runner-mizzle",
    "tree-dasher-2"
  ],
  "vendorFilter": [
    "Allbirds",
    "Gymshark"
  ],
  "minPrice": 25,
  "maxPrice": 250,
  "requestDelayMs": 400,
  "maxConcurrentDomains": 3,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

One row per product variant. Each row carries a change\_status (baseline, price\_up, price\_down, sale\_started, sale\_ended, new\_product, new\_variant, removed, back\_in\_stock, sold\_out or unchanged) plus price, compare-at price, availability and, where relevant, the previous values. Domains whose catalog could not be read appear as store\_status rows explaining why.

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

The RUN\_SUMMARY record: per-status domain counts, products and variants seen, rows written, a breakdown of changes by status, truncated domains, invalid and duplicate inputs, per-domain errors, and an overall SUCCEEDED / PARTIAL / FAILED status. Written on every outcome, including aborted and failed runs.

# 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.deathwishcoffee.com"
    ],
    "mode": "changes_only",
    "stateStoreName": "shopify-monitor-state",
    "compareDatasetId": "",
    "maxProductsPerStore": 25,
    "includeUnavailable": true,
    "productHandleFilter": [],
    "vendorFilter": [],
    "requestDelayMs": 400,
    "maxConcurrentDomains": 3,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("johndowdev/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.deathwishcoffee.com"],
    "mode": "changes_only",
    "stateStoreName": "shopify-monitor-state",
    "compareDatasetId": "",
    "maxProductsPerStore": 25,
    "includeUnavailable": True,
    "productHandleFilter": [],
    "vendorFilter": [],
    "requestDelayMs": 400,
    "maxConcurrentDomains": 3,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("johndowdev/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.deathwishcoffee.com"
  ],
  "mode": "changes_only",
  "stateStoreName": "shopify-monitor-state",
  "compareDatasetId": "",
  "maxProductsPerStore": 25,
  "includeUnavailable": true,
  "productHandleFilter": [],
  "vendorFilter": [],
  "requestDelayMs": 400,
  "maxConcurrentDomains": 3,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call johndowdev/shopify-store-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,johndowdev/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/9KeavAiArfoRWaLqE/builds/cfEaqXCRKGupeDWjn/openapi.json
