# Shopify Store Scraper & Price/Stock Change Monitor (`orbiting_solutions/shopify-catalog-monitor`) Actor

Export any Shopify store's full catalog from its domain, then get only what changed: new or removed products, price and stock changes. No login needed.

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

## Pricing

Pay per event

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

**Shopify Store Scraper & Price/Stock Change Monitor** exports the **full public catalog of any Shopify store** from just its domain: products, variants, prices, compare-at (sale) prices, stock flags, SKUs, images and tags. Schedule it in **changes mode** and each run returns **only what changed**: new and removed products, price moves, sale changes and stock flips.

Try it in one click: the form is pre-filled with one store (`colourpop.com`) and 100 products. A real run of it on Apify finished in 6 seconds with 100 product rows and one free status row, for $0.085.

- ✅ Just type a domain (`gymshark.com`): no login, no API key, no store access needed
- ✅ Full catalogs, including **stores above 25,000 products** (Kith: 35,206 of 35,206, matching its sitemap)
- ✅ **Change monitor** built in: you pay a small scan fee plus a fee per change, not for re-exporting the catalog
- ✅ Headless stores (custom front ends) are read from their verified `myshopify.com` shop
- ✅ **Pay only for stores that finish**: rows are held back until a store's crawl ends `ok`; blocked, failed or unfinished stores cost nothing
- ✅ One free **status row per store** that says what worked and why
- ✅ Collects **public data only**: the same product data any logged-out shopper sees, no customer data

### What can you use it for?

| You are | You use it to |
|---|---|
| Dropshipper or reseller | Watch supplier catalogs for price and stock changes |
| DTC brand or repricing team | Track competitor prices, discounts and new launches |
| Market or assortment analyst | Build price-band and launch datasets across many stores |
| AI agent (Apify MCP) | Answer "what does this store sell, and what changed this week?" |

### What data does it extract?

Product row (`type: "product"`), one per product (or one per variant with **One row per variant**):

| Field | What it is |
|---|---|
| `store`, `myshopifyDomain`, `currency` | Store domain, its Shopify shop domain, and the store's default currency |
| `productId`, `handle`, `title`, `url` | Shopify product ID, handle, title and product link |
| `vendor`, `productType`, `tags` | Brand, product type and tags |
| `featuredImage`, `imageUrls` | Main image and all image URLs |
| `priceMin`, `priceMax`, `compareAtMax`, `onSale` | Price range across variants, highest compare-at (crossed-out) price, and whether anything is discounted |
| `available`, `variantsInStock`, `variantCount` | Stock flag for the product and how many variants are in stock |
| `variants[]` | Per variant: `id`, `title`, `sku`, `price`, `compareAtPrice`, `available`, `grams`, `options` |
| `createdAt`, `publishedAt` | When the product was created and published (UTC) |
| `likelyTestProduct` | `true` for products that look like store test or placeholder items |
| `inventoryQuantity` | Inventory add-on only: total for the product and per variant, where the store makes it public (`null` otherwise) |
| `descriptionHtml` | Only with **Include description HTML** |
| `source`, `scrapedAt` | Where the row came from (`products.json` or the Storefront API) and when |

Change row (`type: "change"`): `store`, `productId`, `variantId`, `handle`, `title`, `variantTitle`, `changeType`, `oldValue`, `newValue`, `changePercent`, `previousSeenAt`, `url`, `scrapedAt`. `changeType` is one of `added`, `removed`, `priceUp`, `priceDown`, `compareAtChanged`, `backInStock`, `outOfStock`, and optionally `titleChanged`, `tagsChanged`, `imagesChanged`.

Store status row (`type: "storeStatus"`, always free): `status`, `productsFound`, `expectedCount` and where it came from, `complete`, `route`, `httpStatus`, `proxyUsed`, `error` and notes.

| status | Meaning | Charged? |
|---|---|---|
| `ok` | Catalog read completely (or up to your `maxProductsPerStore`) | Yes, its rows |
| `notProcessed` | The run stopped first, your max charge was too low for this store, or the store's time budget ran out | No |
| `invalidInput` | Not a domain or URL | No |
| `passwordProtected` | The store is behind its storefront password | No |
| `blocked` | A bot challenge or HTTP 429 that stayed after retries and fresh proxy sessions | No |
| `notShopify` | Not a Shopify store, or its product feed is switched off | No |
| `error` | Anything else, including a catalog well short of the store's own product count | No |

### How to use it

1. Click **Try for free** and keep the pre-filled store, or enter your own domains (`gymshark.com`, `https://kith.com`, or a collection URL such as `https://kith.com/collections/mens`).
2. Pick a **mode**: `snapshot` (all products), `changes` (only what changed since the last run) or `snapshotWithChanges` (all products, each tagged with its change).
3. For monitoring, set a **Watchlist name** and schedule the Actor (for example daily). Each watchlist keeps its own baseline.
4. Click **Start**, then download the results as JSON, CSV, Excel or HTML, or read them through the Apify API.

#### Input example

```json
{
  "stores": ["gymshark.com", "colourpop.com", "allbirds.com"],
  "mode": "changes",
  "watchlistName": "competitors",
  "minPriceChangePercent": 2
}
```

Other options: `collections`, `maxProductsPerStore`, `rowPerVariant`, `trackFields` (add `title`, `tags`, `images`), `includeInventory`, `includeDescriptionHtml`, `originFallback`, `resetState`, `maxConcurrencyPerStore`, `storeTimeBudgetSecs` (default 1,200 s, 60 to 3,600) and `proxyConfiguration` (Apify datacenter proxy by default). Every field is explained in the input form.

#### Output examples

A product row from a real run on 24 October 2026 (tags, images and variants shortened):

```json
{
  "type": "product",
  "store": "allbirds.com",
  "myshopifyDomain": "weareallbirds.myshopify.com",
  "currency": "USD",
  "productId": "7340901859408",
  "handle": "womens-allbirds-flip-flop-dusty-pink",
  "title": "Women's Allbirds Flip Flop - Dusty Pink",
  "vendor": "Allbirds",
  "productType": "Shoes",
  "tags": ["allbirds::edition => limited", "allbirds::gender => womens", "allbirds::material => sugar", "…"],
  "url": "https://allbirds.com/products/womens-allbirds-flip-flop-dusty-pink",
  "featuredImage": "https://cdn.shopify.com/s/files/1/1104/4168/files/A12513_26Q2_Allbirds-Flip-Flop-Dusty-Pink_PDP_LEFT.png?v=1774646345",
  "priceMin": 25,
  "priceMax": 25,
  "compareAtMax": 50,
  "onSale": true,
  "available": true,
  "variantsInStock": 1,
  "variantCount": 7,
  "variants": [
    { "id": "42146889039952", "title": "5", "sku": "A12513W050", "price": 25, "compareAtPrice": 50, "available": false, "grams": 455, "options": ["5"] }
  ],
  "createdAt": "2026-03-27T21:03:50Z",
  "publishedAt": "2026-09-25T23:58:13Z",
  "likelyTestProduct": false,
  "source": "products.json",
  "scrapedAt": "2026-10-24T12:50:04Z"
}
```

A change row from a real scheduled run (a size sold out between two runs):

```json
{
  "type": "change",
  "store": "gymshark.com",
  "currency": "USD",
  "productId": "6806899851466",
  "handle": "gymshark-gslc-stringer-sleeveless-tops-purple-aw26",
  "title": "Gymshark GSLC Stringer - Cherry Purple/Soft White",
  "variantId": "39799451844810",
  "variantTitle": "Medium",
  "changeType": "outOfStock",
  "oldValue": true,
  "newValue": false,
  "changePercent": null,
  "previousSeenAt": "2026-10-24T13:15:30Z",
  "url": "https://gymshark.com/products/gymshark-gslc-stringer-sleeveless-tops-purple-aw26?variant=39799451844810",
  "scrapedAt": "2026-10-24T13:39:02Z"
}
```

A store status row (free):

```json
{ "type": "storeStatus", "store": "allbirds.com", "status": "ok", "route": "products.json", "productsFound": 397, "expectedCount": 397, "expectedCountSource": "meta.json", "complete": true, "proxyUsed": "apify-datacenter", "rowsOutput": 397, "error": null }
```

The dataset has three views: **Products**, **Changes** and **Store status**. The run's `OUTPUT` record holds a run summary (events, stores by status, timing, request statistics) and `STATUS` a compact list of each store's result.

### How much does it cost to scrape a Shopify store?

You pay per result. Platform usage (compute, proxy, storage) is included in the price.

| Event | Price |
|---|---|
| Product row (or variant row) | **$0.85 per 1,000** ($0.00085 each) |
| Change row | **$2.00 per 1,000** ($0.002 each) |
| 100 products scanned in `changes` mode | **$0.0003 per 100** ($0.003 per 1,000) |
| Inventory add-on (only where the quantity is public) | $0.50 per 1,000 |
| Actor start | $0.00005 per run (Apify's standard start event; the default 512 MB counts once) |
| Status rows, stores that don't end `ok`, duplicates, price moves below your threshold | **free** |

**Snapshot mode** charges one product row per product. **Changes mode** charges a scan fee in batches of 100 products plus one change row per change; unchanged products are not output.

The scan fee is rounded up **once per store per run**: a 75-product store is 1 batch ($0.0003), a 153-product store 2 batches ($0.0006), a 10,359-product store 104 batches ($0.0312). The remainder never carries over to another store.

Worked examples (prices from the table above, counts from real runs):

- **The pre-filled run** (`colourpop.com`, 100 products, snapshot): 100 × $0.00085 + $0.00005 = **$0.08505**.
- **One store's full catalog**: Allbirds, 397 products = 397 × $0.00085 = **$0.34**.
- **First monitoring run** (baseline) on gymshark.com, colourpop.com and allbirds.com (10,359 + 1,064 + 397 products): 104 + 11 + 4 = 119 batches × $0.0003 = **$0.036**, no change rows.
- **Daily monitoring of the same 3 stores**: $0.036 scan fee plus $0.002 per change. A real run on Gymshark found 26 changes: 26 × $0.002 = $0.052.
- **100 stores, 119,977 products** checked in one changes-mode run: 1,244 batches × $0.0003 = **$0.37**.

Apify's Free plan includes $5 of monthly usage.

You never pay for status rows, for stores that end `blocked`, `notShopify`, `error`, `notProcessed` or any status other than `ok` (even if part of their catalog was read), for duplicate products, or for price moves below `minPriceChangePercent`. The run also respects the maximum charge you set: a store that wouldn't fit is skipped as `notProcessed` and costs nothing.

### Change monitoring (changes mode)

- The first `changes` run for a store saves a baseline and outputs no change rows: you pay only the scan fee.
- The baseline lives in the key-value store `shopify-monitor-<watchlistName>` in your Apify account, one record per store.
- A baseline is replaced only after a **complete** crawl that ended `ok`. A stopped, blocked or timed-out crawl never overwrites it, so you don't get false `removed` rows.
- A product is reported `removed` only after it was missing on **two complete runs in a row**. A variant that disappears from a product that is still listed is reported straight away.
- A product deleted and re-created with the same handle is reported once (`added` with `recreatedFromProductId`).
- `updated_at` is not used: Shopify bumps it on nearly every product daily. Changes are detected from price, compare-at price and stock per variant.
- With `maxProductsPerStore` set, the baseline isn't saved (a capped crawl isn't a full catalog).

In real tests, every detected change was checked against the live store: 9 of 9, 1 of 1 and 16 of 16 matched.

### How reliable is it?

- **Accuracy**: 30 of 30 products from 13 stores matched the store's own product pages on every field.
- **Blocking**: every request goes through Apify's datacenter proxy with one session per store, rotated (up to 3 times) on a bot challenge or HTTP 429. In our test runs on Apify with the proxy, the only store that kept challenging was bombas.com (Vercel). A 100-store run at 512 MB saw 0 challenges and 0 HTTP 429 responses. Challenges are never solved; a store that keeps challenging ends `blocked` and costs nothing.
- **Large stores**: Shopify's product feed stops at 25,000 products. Past that, the Actor continues through Shopify's public Storefront API and uses the store's sitemap as the target count (Kith: 35,206 of 35,206 on Apify in 669 s).
- **Scale**: one 100-store run at 512 MB read 191,064 products in 1,296 s (97 stores finished; the 3 largest needed a bigger time budget).
- **Restarts**: charged products are recorded before each charge, so if Apify migrates or restarts a run, nothing is charged twice (tested with forced reboots on Apify: charged batches equalled the expected count exactly).

### FAQ

**Is it legal to scrape Shopify stores?**
This Actor reads only public product data that any logged-out shopper can see, through the same JSON feeds Shopify serves to every visitor. It doesn't log in and reads no customer data. Check your own use against each store's terms and the law where you work; if you're unsure, ask a lawyer.

**Why did a store end `blocked`?**
The store put a bot challenge on its own domain (for example bombas.com sits behind Vercel's checkpoint). The Actor retries with fresh proxy sessions but never solves challenges. You aren't charged for that store.

**Why did a big store end `notProcessed`?**
Its time budget ran out. The largest stores (for example Burga, about 160,000 products, or Fashion Nova) may need more than the default 1,200 s. Raise **Time budget per store** (up to 3,600 s). Nothing is charged for an unfinished store.

**Why is the price in a different currency than on my screen?**
Prices are in the store's default currency (`currency`), as its public feed serves them, not your local currency.

**Can I monitor only one collection?**
Yes: enter a collection URL (`https://kith.com/collections/mens`) or list collection handles in `collections`.

**Can I get exact stock quantities?**
Turn on the inventory add-on. Many stores hide quantities; you're charged only for products where a quantity was public.

**Can I use it through the API or with integrations?**
Yes. Run it with the Apify API, the JavaScript or Python client, schedules, webhooks, or integrations such as Make, Zapier and Google Sheets.

### Limits

- Shopify stores only. Stores with their product feed switched off end `notShopify`.
- Very large stores (e.g. Burga, about 160,000 products, or Fashion Nova) may need a larger `storeTimeBudgetSecs`; in our tests Fashion Nova wasn't finished within a 600 s budget (34,095 products read on Apify at 512 MB).
- Stores behind their own bot protection (e.g. bombas.com, Vercel) end `blocked`.
- Prices are the storefront's default-currency prices; market-specific prices aren't read.
- A store whose public feed lists well under its own product count ends `error` (not charged), because the catalog can't be confirmed complete.

### Feedback

Found a bug or need a field? Open an issue on the **Issues** tab and include the run ID.

# Changelog

This Actor's version history is a separate document: https://apify.com/orbiting_solutions/shopify-catalog-monitor/changelog.md

# Actor input Schema

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

Shopify store domains or URLs, up to 500, e.g. gymshark.com. A collection URL (https://kith.com/collections/mens) limits that store to the collection.

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

Snapshot outputs every product. Changes compares against the baseline stored for this watchlist and outputs only what changed (the first run only stores the baseline). Snapshot with changes outputs every product with a changeType.

## `watchlistName` (type: `string`):

Names the baseline kept between runs (key-value store 'shopify-monitor-<name>' in your account). Use different names for independent monitors.

## `collections` (type: `array`):

Collection handles to limit every store to, e.g. mens. Empty = whole catalog.

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

0 = all products. In change modes a capped crawl does not replace the stored baseline.

## `rowPerVariant` (type: `boolean`):

One row per variant (size/colour) instead of per product. Charged per row.

## `trackFields` (type: `array`):

Which differences produce change rows.

## `minPriceChangePercent` (type: `number`):

Ignore smaller price moves (not output, not charged). The baseline keeps the old price so small moves add up.

## `includeInventory` (type: `boolean`):

One extra lookup per product. Adds inventoryQuantity where the store makes it public; charged only for those products. Snapshot modes only.

## `includeDescriptionHtml` (type: `boolean`):

Off keeps rows about 70% smaller.

## `originFallback` (type: `string`):

Headless stores (custom front end, products.json 404) are read from their <shop>.myshopify.com origin. Stores that put a bot challenge on their own domain are reported as blockedByStore unless you choose 'always'.

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

Deletes the stored baseline for these stores before crawling, so this run becomes a new baseline.

## `maxConcurrencyPerStore` (type: `integer`):

Halved automatically on HTTP 429.

## `storeTimeBudgetSecs` (type: `integer`):

Wall-clock budget for each store's crawl (listing pages, Storefront API top-up past 25,000 products). Default 1,200 s (20 min). If it runs out before the catalog is read, the store ends as notProcessed and is charged nothing. Very large stores (Fashion Nova, Burga: 100,000+ products) may need up to 3,600 s.

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

Default: Apify datacenter proxy, one session per store, switched to a new session (up to 3 times) when Shopify answers with a bot challenge or HTTP 429. A store that stays challenged is reported as 'blocked' (free). Challenges are never solved.

## Actor input object example

```json
{
  "stores": [
    "colourpop.com"
  ],
  "mode": "snapshot",
  "watchlistName": "default",
  "collections": [],
  "maxProductsPerStore": 100,
  "rowPerVariant": false,
  "trackFields": [
    "price",
    "compareAtPrice",
    "available",
    "added",
    "removed"
  ],
  "minPriceChangePercent": 0,
  "includeInventory": false,
  "includeDescriptionHtml": false,
  "originFallback": "headlessOnly",
  "resetState": false,
  "maxConcurrencyPerStore": 4,
  "storeTimeBudgetSecs": 1200,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Product (or variant) rows from stores that ended ok (snapshot and snapshotWithChanges modes).

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

Change rows: added, removed, priceUp, priceDown, compareAtChanged, backInStock, outOfStock (and optional titleChanged, tagsChanged, imagesChanged).

## `storeStatus` (type: `string`):

One free row per store: ok, notProcessed, invalidInput, passwordProtected, blocked, notShopify or error, with counts and notes.

## `allRows` (type: `string`):

Every row in the default dataset, unfiltered.

## `summary` (type: `string`):

OUTPUT record: events charged, stores by status, per-store details, runtime and HTTP/proxy statistics.

## `status` (type: `string`):

STATUS record: compact list of each store's final status, products found, rows output and error.

# 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": [
        "colourpop.com"
    ],
    "maxProductsPerStore": 100,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("orbiting_solutions/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 = {
    "stores": ["colourpop.com"],
    "maxProductsPerStore": 100,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("orbiting_solutions/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 '{
  "stores": [
    "colourpop.com"
  ],
  "maxProductsPerStore": 100,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call orbiting_solutions/shopify-catalog-monitor --silent --output-dataset

```

## MCP server setup

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