# Shopify Store Monitor: Price & Stock Changes (`reventadata/shopify-scraper`) Actor

Monitor Shopify stores for price drops, restocks, sold-out variants, and new or removed products. The first run saves a baseline, one row per product; each later run returns only what changed. Public catalogue data, no login.

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

## Pricing

from $1.00 / 1,000 results

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

> **Unofficial.** This Actor is not affiliated with, authorised by, endorsed by, or in any
> way officially connected to Shopify Inc. or to any store it reads. "Shopify" is a trademark
> of its respective owner and is used here only to describe what this tool reads. It reads the
> same public catalogue data a browser does.

Watch Shopify stores and get only what changed since your last run: price drops and
increases, variants back in stock or sold out, new products and variants, removed products,
and sales starting or ending.

Unlike a catalogue scraper that bills every product on every run, you pay for the full
catalogue once (one row per product), then only for what changed: a Shopify price tracker
and restock feed in one schedule (no built-in notifications; connect the dataset to your own).

### What does it do?

1. **First run per store: a baseline.** It reads the store's public catalogue
   (`/products.json`, falling back to the sitemap and product pages when that feed is closed),
   outputs one `baseline` row per product with every variant nested inside it, and saves a
   snapshot in a named key-value store.
2. **Every later run: changes only.** It reads the catalogue again, compares it with the
   snapshot, outputs one row per changed variant plus one `summary` row per store, and
   updates the snapshot.

Put it on an hourly or daily Apify schedule and the dataset of each run is your change feed.

### Who is it for?

- Brands and retailers watching competitors' prices and markdowns.
- Resellers and deal hunters waiting for restocks of specific products.
- Analysts building a price history for a set of stores.

If you only need a one-off catalogue export, the first run already gives you that: one row
per product, all variants included.

### Input

| Field | Type | Description |
|---|---|---|
| `storeUrls` | array | Store addresses, e.g. `https://allbirds.com`. Required. |
| `maxProductsPerStore` | integer | Read at most this many products per store (default 2000). A capped read cannot report removed products. |
| `collection` | string | Optional collection handle to watch just one collection. It has its own snapshot. |
| `emitBaseline` | boolean | Output all products on a store's first run (default on). |
| `includeUnchanged` | boolean | Also output unchanged variants (default off). |
| `includeInventory` | boolean | Add inventory quantity and barcode from product detail endpoints (default off). |
| `maxInventoryLookups` | integer | Maximum distinct product detail requests for inventory per store (default 200). |
| `minPriceChangePercent` | number | Ignore price-only moves smaller than this percent. Stock changes are never hidden. |
| `stateStoreName` | string | Named key-value store that holds snapshots (default `shopify-monitor-state-v2`). Use another name for a separate watch-list. |
| `resetState` | boolean | Treat the run as a first run and save a fresh snapshot. |
| `proxyConfiguration` | object | Off by default. |

There is no keyword search: the Actor watches whole stores or one collection.

Example input:

```json
{
  "storeUrls": ["https://allbirds.com"],
  "maxProductsPerStore": 20
}
```

### Output

`changeType` is one of `baseline`, `new_product`, `new_variant`, `removed_product`,
`removed_variant`, `price_drop`, `price_increase`, `back_in_stock`, `sold_out`,
`sale_changed`, `unchanged` (only with `includeUnchanged`), `summary`, `error`. When one
variant changed in several ways at once, `changes` lists all of them and `changeType` holds
the first.

**Baseline row** (first run). Real row from a run on allbirds.com on 2026-10-12, shortened to
3 of its 16 tags and 2 of its 7 variants:

```json
{
  "changeType": "baseline",
  "changes": [],
  "storeDomain": "allbirds.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::carbon-score => undefined",
    "allbirds::cfId => color-a4d5d0f6620dc304306f503bf67bc5f9",
    "allbirds::complete => true"
  ],
  "url": "https://allbirds.com/products/womens-allbirds-flip-flop-dusty-pink",
  "imageUrl": "https://cdn.shopify.com/s/files/1/1104/4168/files/A12513_26Q2_Allbirds-Flip-Flop-Dusty-Pink_PDP_LEFT.png?v=1774646345",
  "productUpdatedAt": "2026-10-12T01:14:41-07:00",
  "variants": [
    {
      "variantId": 42146889039952,
      "variantTitle": "5",
      "sku": "A12513W050",
      "price": 25,
      "compareAtPrice": 50,
      "available": false,
      "cartUrl": "https://allbirds.com/cart/add?id=42146889039952"
    },
    {
      "variantId": 42146889072720,
      "variantTitle": "6",
      "sku": "A12513W060",
      "price": 25,
      "compareAtPrice": 50,
      "available": false,
      "cartUrl": "https://allbirds.com/cart/add?id=42146889072720"
    }
  ],
  "scrapedAt": "2026-10-12T08:14:41.168712+00:00"
}
```

**Change rows** (later runs) are one row per changed variant with `storeDomain`, `currency`,
`productId`, `handle`, `title`, `vendor`, `productType`, `tags`, `url`, `cartUrl`,
`imageUrl`, `variantId`, `variantTitle`, `sku`, `price`, `previousPrice`, `priceChange`,
`priceChangePct`, `compareAtPrice`, `previousCompareAtPrice`, `available`,
`previousAvailable`, `scrapedAt`, plus `inventoryQuantity` and `barcode` when
`includeInventory` is on. Removed products and variants carry the ids, `handle`, `title` and
the previous values.

**Summary row** (every run, one per store). Real row from the run right after the baseline
above, where nothing had changed. `scanComplete` is `false` because the run was capped at 20
products:

```json
{
  "changeType": "summary",
  "storeDomain": "allbirds.com",
  "currency": "USD",
  "mode": "diff",
  "source": "products.json",
  "scanComplete": false,
  "variantsScanned": 243,
  "productsScanned": 20,
  "changeCounts": {},
  "scrapedAt": "2026-10-12T08:15:09.523351+00:00"
}
```

A store that cannot be read gives an `error` row instead, for example
`"error": "HTTP 403, 919 B body at https://www.gymshark.com/products/gift-card.json"`.

### What it costs

Billed per dataset item at $0.001, plus a $0.00005 start fee per run.

- **Baseline:** one row per product, not per variant. A 2,000-product store is about 2,001
  items, about $2.00, once.
- **Later runs:** only the change rows plus one summary row per store. A store where nothing
  changed costs one row ($0.001) plus the start fee per run.

### Limits

- **Stock is in stock / out of stock per variant.** The catalogue feed carries no quantities.
  `includeInventory` adds one request per distinct product to fetch quantity and barcode;
  many stores publish neither, and then both fields are `null`.
- **Not every store is reachable.** Some stores put a firewall in front of the whole site and
  refuse automated requests on every path. Such a store returns an `error` row; the other
  stores in the same run still complete. A run where every store fails ends as failed.
- **Prices are in the currency the store presents for this run** (from `/cart.js`, falling
  back to `/meta.json`); `currency` is null when neither publishes it. Prices are not converted,
  and a store can show different currencies to different regions.
- **Removed products are only reported after a complete read** (`scanComplete: true` on the
  summary row), so a capped run never reports false removals.
- Password-protected stores are not supported.
- Only public catalogue data is read. No customer, seller or personal data is collected.

### Scheduling

1. Run the Actor once with your store list to save the baselines.
2. Create an Apify schedule on this Actor (or on a saved task) with the same input.
3. Keep `stateStoreName` the same between runs; the snapshot lives there. Use a different
   name for each independent watch-list.

### FAQ

**Can I get every variant as its own row on the first run?** No; baseline rows nest variants
to keep the first run cheap. Change rows are per variant.

**How do I start over?** Set `resetState` to true for one run, or use a new `stateStoreName`.

# Actor input Schema

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

Shopify store addresses to monitor, e.g. https://allbirds.com. Each store keeps its own saved snapshot, so schedule the Actor and every run after the first returns only changes.

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

Stop reading a store after this many products. A capped read still reports changes, but cannot detect removed products.

## `collection` (type: `string`):

Optional. Monitor only one collection, e.g. new-arrivals (the last part of /collections/new-arrivals). Leave empty for the whole catalogue.

## `emitBaseline` (type: `boolean`):

On a store's first run there is nothing to compare with. On: output every variant as a 'baseline' row. Off: only save the snapshot.

## `includeUnchanged` (type: `boolean`):

Also output variants that did not change, with changeType 'unchanged'. Off by default so scheduled runs stay small.

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

Look up inventory quantity and barcode from each changed product's detail endpoint. Inventory is not part of change detection.

## `maxInventoryLookups` (type: `integer`):

Maximum number of distinct products to look up for inventory details when includeInventory is on.

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

Ignore price-only changes smaller than this percentage. Leave empty to report every price change.

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

Name of the key-value store that holds the snapshots between runs. Use a different name to keep separate watch-lists.

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

Ignore the saved snapshot and treat this run as a first run, then save a fresh one.

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

Off by default. Some stores refuse datacenter IPs; enable Apify Proxy if a store returns a blocked error.

## Actor input object example

```json
{
  "storeUrls": [
    "https://allbirds.com"
  ],
  "maxProductsPerStore": 20,
  "emitBaseline": true,
  "includeUnchanged": false,
  "includeInventory": false,
  "maxInventoryLookups": 200,
  "stateStoreName": "shopify-monitor-state-v2",
  "resetState": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

Items scraped from the target website.

# 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://allbirds.com"
    ],
    "maxProductsPerStore": 20,
    "includeInventory": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("reventadata/shopify-scraper").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://allbirds.com"],
    "maxProductsPerStore": 20,
    "includeInventory": False,
}

# Run the Actor and wait for it to finish
run = client.actor("reventadata/shopify-scraper").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://allbirds.com"
  ],
  "maxProductsPerStore": 20,
  "includeInventory": false
}' |
apify call reventadata/shopify-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,reventadata/shopify-scraper"
        }
    }
}
```

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/Dd2fwUSqZHuRqNRdF/builds/ClECf137y8cUb3Rl5/openapi.json
