# Shopify Catalogue + Price & Stock Monitor (`multiplex/shopify-monitor`) Actor

Pull any Shopify store's full public catalogue from its own /products.json endpoint — every product and every variant in one normalised schema with stable IDs — and run it on a schedule as a price and stock change monitor that tells you what changed and what it was before.

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

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-usage

## 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 & Stock Monitor

Pull **any Shopify store's full public catalogue** — every product, every variant — from the store's own `/products.json` endpoint, normalised to one clean schema with stable IDs. Then run it on a schedule and it becomes a **price and stock change monitor** that tells you not just *that* something changed, but **what changed and what it was before**.

No browser, no proxy, no HTML parsing, no API key. This is the merchant's own public product feed, on the merchant's own domain, served by Shopify for exactly this purpose — which makes it fast, reliable and compliance-friendly.

### What you get

One row per **variant**, because price and stock live on the variant. A monitor that only watched the product would miss *"size 9 sold out"* — the single most valuable signal in retail data.

```json
{
  "id": "a4e027feb0623770059af879b31c256dbce0d577",
  "store": "allbirds.com",
  "productId": "7199699927120",
  "variantId": "41243839496272",
  "handle": "mens-strider-medium-grey",
  "title": "Men's Strider - Medium Grey (Blizzard Sole)",
  "variantTitle": "8",
  "sku": "A11718M080",
  "vendor": "Allbirds",
  "productType": "Shoes",
  "price": 91,
  "compareAtPrice": 130,
  "currency": "USD",
  "onSale": true,
  "discountPercent": 30,
  "available": false,
  "productAvailable": false,
  "availableVariantCount": 0,
  "variantCount": 13,
  "productUrl": "https://allbirds.com/products/mens-strider-medium-grey?variant=41243839496272",
  "imageUrl": "https://cdn.shopify.com/s/files/…"
}
```

- **Prices are numbers.** Shopify ships money as strings (`"91.00"`), so sorting a competitor's catalogue by price gives you nonsense unless somebody parses it. We parse it.
- **Stable IDs** — `sha1(store:productId:variantId)`. The same variant keeps the same `id` across every run, so joins and diffing downstream just work.
- **Deep links to the exact variant** — `productUrl` includes `?variant=…`, so a click lands on the right size, not the product page.
- **Sale maths done** — `onSale` and `discountPercent`, with the fake discounts removed (see below).
- **No description HTML** — it is the bulk of the payload, nobody sorts by it, and under pay-per-event you would be paying for it.

### Monitor mode (`changesOnly`)

Turn any store into a price feed. The actor remembers each store's state between runs and emits only what moved:

| `changeType` | Meaning |
|---|---|
| `new` | a product or a variant that wasn't there last run — a launch, or a new colourway/size |
| `updated` | price, compare-at price or availability changed |
| `removed` | the variant is gone from the catalogue — discontinued, or delisted |

Updated rows carry the **before and after**, which is the part most change-monitors leave out:

```json
{
  "changeType": "updated",
  "changedFields": ["price", "available"],
  "previousPrice": 116,
  "price": 91,
  "priceDelta": -25,
  "previousAvailable": true,
  "available": false,
  "title": "Men's Strider - Medium Grey (Blizzard Sole)",
  "variantTitle": "8.5"
}
```

Removed rows keep the `productId`, `variantId` and last known price, so a delisting is actionable rather than just an ID you can no longer look up.

**The change signal is deliberately quiet.** Shopify bumps a product's `updated_at` on *any* edit, and CDN image URLs churn on their own. Both are excluded from the change hash, so you are billed for rows where **price, compare-at price, availability, title, SKU or link actually moved** — not for background noise.

Run it hourly against your competitors and you have a price-and-stock intelligence pipeline: who discounted what, by how much, what sold out, and what quietly disappeared.

### Input

| Field | Example | Notes |
|---|---|---|
| `stores` | `allbirds.com`, `https://kith.com/collections/new` | a bare domain, a full URL, or **any page on the store** — everything reduces to the store's origin, so paste whatever you have |
| `changesOnly` | `false` | monitor mode as above |
| `includeImages` | `true` | one image URL per row (variant image, falling back to the product image) |
| `maxProductsPerStore` | `0` | 0 = unlimited |

### Things that quietly break other Shopify actors

Everything here is verified against live payloads — the evidence is in `tests/fixtures/SOURCES.md`.

- **`www.` vs the bare domain is not cosmetic.** The same merchant can serve one and refuse the other: `www.gymshark.com` answers **403**, `gymshark.com` answers **200**. This actor retries a failed host once on its twin and tells you in the log which one served, so you don't get an empty run because of a prefix.
- **`compare_at_price: "0.00"` is not a was-price.** Some themes write `null` where there is no discount, others write `"0.00"`. Take it literally and you publish a 100% discount on the entire catalogue. We drop it — along with any compare-at price at or below the live price, which is a stale field, not a saving.
- **`since_id` does nothing here.** It is an Admin-API parameter; on the storefront endpoint it is silently ignored and returns the same first page forever. Pagination is `?limit=250&page=N`, and an empty page is the only end-of-catalogue signal the endpoint gives you.
- **A password-protected store returns HTML, not an error.** That is a `JSON.parse` crash in a naive actor. Here it is one clear line: *"the store is password-protected, so its catalogue is not public"* — and the other stores in your run finish normally.

### Honest limits

- **No stock quantities exist on this endpoint.** Shopify publishes `available` (true/false) per variant and nothing more, so this actor reports availability, `availableVariantCount` and `variantCount` — never an invented number. If you need real inventory counts you need the merchant's own Admin API credentials, which no third party has.
- **Currency is best-effort.** `/products.json` carries no currency at all; we read it from the store's `/cart.js` in one extra request. Where a store doesn't serve it, `currency` is `null` and the run log says so rather than guessing from the domain.
- Stores that have disabled the catalogue endpoint return 404 — you get a clear per-store `FAILED` line, and the rest of the run continues.
- Multi-currency storefronts report their default presentment currency, not a per-visitor one.
- Catalogues are capped at 200 pages (50,000 products) per store to bound a runaway endpoint.
- Monitor state is one record per variant per store; comfortable to roughly 50k variants per store.

### Typical costs

Pure JSON over HTTP — no browser, no proxy — so platform usage is tiny: a four-store run pulling 167 variants completed in a few seconds. You pay per result via the event pricing shown on this page.

# Actor input Schema

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

Storefront domains, one per line — a bare domain (`allbirds.com`), a full URL, or any page on the store (`https://kith.com/collections/new`). Everything is reduced to the store's origin, so paste whatever you have. Each store must be a live Shopify storefront with its public catalogue enabled.

## `changesOnly` (type: `boolean`):

Emit only variants that are new, changed or removed since the previous run of this actor on the same store. Changed rows carry `changedFields`, `previousPrice` and `priceDelta`. First run emits everything as `new`.

## `includeImages` (type: `boolean`):

Include one image URL per row (the variant image, falling back to the product's first image).

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

Caps products (not variants) fetched per store. 0 = unlimited, bounded by an internal 200-page ceiling.

## Actor input object example

```json
{
  "stores": [
    "allbirds.com"
  ],
  "changesOnly": false,
  "includeImages": true,
  "maxProductsPerStore": 0
}
```

# Actor output Schema

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

One item per product variant with price, compare-at price and availability (monitor mode adds changeType and price/stock deltas).

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

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

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

```

## MCP server setup

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