# Shopify Catalogue & Price Change Monitor (`dottti/shopify-catalog-monitor`) Actor

Track any Shopify store's full catalogue and get only what changed since the last run: price drops, price rises, new products, stock-outs, restocks and removals.

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

## Pricing

from $10.00 / 1,000 product 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 Catalogue & Price Change Monitor

Watch any Shopify store and get **only what changed since your last run**: price drops, price rises, new products, stock-outs, restocks and removals.

Point it at your competitors, schedule it daily, and each run returns a short list of what moved instead of a fresh dump of the whole catalogue.

### Why this one is reliable

Every Shopify store serves its catalogue at `/products.json` without authentication. That endpoint is part of the Shopify platform, not a page template, so it does not break when a store restyles its theme or swaps its front end. No browser, no proxies, no login.

Verified against live storefronts on 13 September 2026: page size is capped at 250 regardless of what is requested, `page` paginates cleanly with no overlap, and storefronts answer bursts with HTTP 429. This Actor paginates correctly, backs off on 429 honouring `Retry-After`, and refuses to mistake an HTML challenge page for an empty catalogue.

### What a change row looks like

```json
{
  "domain": "gymshark.com",
  "changeType": "price_drop",
  "reasons": ["price_drop"],
  "title": "Gymshark Diffuse Sweat Headwrap - Black",
  "previousMinPrice": 51,
  "minPrice": 26,
  "maxPrice": 28,
  "previousInStock": true,
  "inStock": true,
  "onSale": true,
  "vendor": "Gymshark",
  "variantCount": 2,
  "availableVariantCount": 1,
  "url": "https://gymshark.com/products/diffuse-sweat-headwrap-black",
  "detectedAt": "2026-09-13T12:31:00.000Z"
}
```

Change types: `price_drop`, `price_rise`, `price_change`, `back_in_stock`, `out_of_stock`, `variant_availability_change`, `title_change`, `new_product`, `removed_product`, `first_seen`.

Two details that matter in practice:

- **The first run reports `first_seen`, not a flood of `new_product`.** A store's entire catalogue appearing at once is a baseline, not news. Real additions after that are `new_product`.
- **Every row has the same fields**, including `removed_product` rows for items pulled from the storefront. Exports to CSV stay rectangular instead of going ragged.

### Input

| Field | What it does |
| --- | --- |
| `domains` | Storefronts to read. Full URLs, bare domains and `www.` all work. |
| `mode` | `changes` returns only what moved since last run. `catalog` returns everything each time. |
| `stateKey` | Names the saved baseline. Use one key per watchlist so separate schedules don't overwrite each other. |
| `inStockOnly` | Skip products with no available variant. |
| `onSaleOnly` | Keep only products discounted below their compare-at price. |
| `maxProductsPerStore` | How deep to read each catalogue. |

#### Daily competitor price watch

```json
{
  "domains": ["gymshark.com", "allbirds.com"],
  "mode": "changes",
  "stateKey": "competitors",
  "maxProductsPerStore": 2000
}
```

Schedule that daily. The first run records the baseline; every run after returns just the moves.

#### Full catalogue export

```json
{
  "domains": ["gymshark.com"],
  "mode": "catalog",
  "maxProductsPerStore": 5000
}
```

### Notes and limits

- Public storefront data only. No customer data, no orders, no accounts, nothing behind a login.
- Prices are returned in the store's own currency as the storefront publishes them. No conversion is applied.
- A domain that is not a Shopify store returns a clear per-store error and the run continues with the rest.
- `changes` mode needs somewhere to keep the baseline between runs, so it writes to a named key-value store. Deleting that store resets the baseline and the next run reports `first_seen` again.

### Development

```bash
npm install
npm test
node src/main.js
```

# Actor input Schema

## `domains` (type: `array`):

Shopify storefronts to read. Full URLs, bare domains and www. prefixes all work, for example gymshark.com or https://www.allbirds.com.

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

changes: return only products that moved since the previous run. catalog: return the full catalogue every time.

## `stateKey` (type: `string`):

Names the saved baseline that changes are measured against. Use one key per watchlist so separate schedules do not overwrite each other's history.

## `inStockOnly` (type: `boolean`):

Skip products where no variant is available.

## `onSaleOnly` (type: `boolean`):

Keep only products whose compare-at price is above their current price.

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

Upper bound on how deep to read each catalogue.

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

Storefronts answer bursts with HTTP 429. Raise this if the log shows throttling.

## `maxRetries` (type: `integer`):

Retries with exponential backoff on 429 and 5xx, honouring Retry-After when present.

## Actor input object example

```json
{
  "domains": [
    "gymshark.com"
  ],
  "mode": "changes",
  "stateKey": "default",
  "inStockOnly": false,
  "onSaleOnly": false,
  "maxProductsPerStore": 2000,
  "requestDelayMs": 400,
  "maxRetries": 4
}
```

# Actor output Schema

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

Each row carries changeType (price\_drop, price\_rise, back\_in\_stock, out\_of\_stock, new\_product, removed\_product, first\_seen), the previous price and stock state, the current price range, availability, vendor, variants and the product URL.

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

Per-store outcome for the run: products fetched, rows delivered, and any store that failed with the reason.

# 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 = {
    "domains": [
        "gymshark.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("dottti/shopify-catalog-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 = { "domains": ["gymshark.com"] }

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

```

## MCP server setup

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