# Shopify Store Products & Price Monitor: Prices, Stock, Variants (`charlesmarkette/shopify-store-products-price-monitor`) Actor

Get every product, price, variant and stock flag from any Shopify store's public catalog, or run it on a schedule to see only what changed: new products, price drops, price rises, out of stock, back in stock and removed items. No login, no browser.

- **URL**: https://apify.com/charlesmarkette/shopify-store-products-price-monitor.md
- **Developed by:** [Charles Markette](https://apify.com/charlesmarkette) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 products

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 Products & Price Monitor** exports the product catalogue of any Shopify store you give it: titles, handles, vendors, product types, tags, **prices and compare-at prices**, an **on sale** flag, **in stock** status, every **variant** (size, colour…) with its **SKU**, images and the description as plain text. Turn on **monitor mode** and schedule it: each run then outputs only what changed since the last run, such as **new products, price drops, price increases, restocks, sell-outs and removed products**, so you can send alerts or update a spreadsheet.

It reads each store's **public storefront feed** (`/products.json`, the same data the store's own theme uses). It doesn't log in, scrape HTML pages, or touch the cart or checkout.

### Why this Actor

- **Clean, typed data.** Prices are numbers, booleans are real booleans, dates are ISO 8601 UTC, tags are a list, and the HTML description is turned into readable text.
- **Two output shapes.** One row per product with its variants nested, or one row per variant with the product's fields copied onto it (easier in Excel and Google Sheets).
- **Change monitoring built in.** It remembers a small fingerprint of each product (lowest and highest price, in stock or not, number of variants, a hash of the title) and outputs only the products that changed, with the previous price and stock status next to the new ones.
- **Polite.** One request at a time per store, a delay between pages (500 ms by default), and retries with backoff when a store answers 429 or 5xx, honouring `Retry-After`. Up to 3 different stores are read in parallel.
- **Honest errors.** A URL that isn't a Shopify store, a password-protected store or a store that has turned off its products feed gets one row with an `error` column, which is free, and the run carries on.

### Use cases

- **Competitor price tracking:** a daily monitor of competitors' stores that outputs every price drop and increase.
- **Restock and sell-out alerts:** get notified when a product comes back in stock (`backInStock`) or sells out (`outOfStock`).
- **New product launches:** see what competitors or suppliers add (`new`) and remove (`removed`).
- **Catalogue export:** a full product and variant list with SKUs, prices and images, for analysis, price comparison or migration planning.
- **Market research:** vendors, product types, tags and price ranges across many stores.

### How to use it

1. Add **Store URLs**: a store's home page (`https://shop.example.com`), a bare domain (`shop.example.com`) or a collection URL (`https://shop.example.com/collections/sale`) to read only that collection. You can also pick a **Dataset of store URLs** and set **URL field**.
2. Choose the **Output**: one row per product, or one row per variant.
3. For monitoring, turn on **Only changes since the last run** and optionally give it a **Monitor name**, then schedule the Actor (for example daily).
4. Run it. Export as CSV, Excel or JSON, or chain it with an integration.

**Input example**

```json
{
  "startUrls": ["https://shop.example.com", "https://other-store.example/collections/sale"],
  "outputMode": "products",
  "maxProductsPerStore": 1000,
  "onlyChanges": true,
  "monitorName": "competitors"
}
```

**Output example** (one product, shortened)

```json
{
  "storeDomain": "shop.example.com",
  "collection": null,
  "productId": 7781234567890,
  "handle": "merino-runner",
  "title": "Merino Runner",
  "vendor": "Example Co",
  "productType": "Shoes",
  "tags": ["mens", "running"],
  "url": "https://shop.example.com/products/merino-runner",
  "imageUrl": "https://cdn.shopify.com/s/files/1/0000/0001/products/runner.jpg",
  "imagesCount": 6,
  "priceMin": 89.0,
  "priceMax": 95.0,
  "compareAtPriceMax": 110.0,
  "currency": "USD",
  "onSale": true,
  "available": true,
  "variantsCount": 2,
  "variants": [
    {"id": 41234567890001, "title": "9", "sku": "MR-9", "price": 89.0, "compareAtPrice": 110.0, "available": true,
     "option1": "9", "option2": null, "option3": null},
    {"id": 41234567890002, "title": "10", "sku": "MR-10", "price": 95.0, "compareAtPrice": null, "available": false,
     "option1": "10", "option2": null, "option3": null}
  ],
  "createdAt": "2025-03-01T14:00:00Z",
  "updatedAt": "2026-10-20T08:12:44Z",
  "publishedAt": "2025-03-02T09:00:00Z",
  "descriptionText": "Soft merino upper.\n\nMachine washable.",
  "changeType": "priceDrop",
  "previousPriceMin": 99.0,
  "previousPriceMax": 105.0,
  "previousAvailable": true,
  "scrapedAt": "2026-10-22T07:00:03Z"
}
```

- `onSale` is true when any variant's price is below its compare-at price. `available` is true when any variant is in stock.
- In **variants** mode each row has the product fields (without the nested `variants` list) plus `variantId`, `variantTitle`, `sku`, `price`, `compareAtPrice`, `variantAvailable`, `variantOnSale`, `option1`–`option3` and `variantUrl`.
- `changeType`, `previousPriceMin`, `previousPriceMax` and `previousAvailable` are only present in monitor mode.
- Error rows have `storeDomain`, `collection`, `inputUrl`, `error` and `scrapedAt`.
- A run summary (stores read, products, rows, changes by type, one line per store) is saved as `OUTPUT` in the key-value store.

### Monitor mode

With **Only changes since the last run** on, each product gets one of these `changeType` values, and unchanged products are left out:

| changeType | Meaning |
|---|---|
| `new` | Not seen in this store (or collection) before. |
| `priceDrop` / `priceIncrease` | The lowest variant price went down / up (or, if it's the same, the highest one did). |
| `backInStock` / `outOfStock` | No variant was in stock and now one is / some variant was in stock and now none is. A stock change is reported in preference to a price change. |
| `changed` | The title or the number of variants changed. |
| `removed` | Seen in an earlier run but not in the store's feed any more. These rows have the product ID, handle and URL plus the previous values. |

- **The first run outputs every product as `new`** (and is charged for each), because there is nothing to compare with yet. From the second run on you only get changes.
- `removed` rows are only produced when the whole store (or collection) was read. If a store is cut off by **Max products per store** or a page fails, no products are reported as removed for it in that run.
- The memory is kept in the named key-value store `shopify-products-monitor-state` in your account. With a **Monitor name** it is tied to that name, so you can add or remove stores without losing the memory of the others. Without one it is tied to the exact list of stores (with a dataset input, the list of stores found in it). It's saved after each store, regularly during long stores, and when the run migrates or is aborted.
- If a run stops early at **Max output rows** or your maximum cost, the products it didn't output are still reported as changed in the next run.

### How much does it cost?

Pay per event: **$1.00 per 1,000 products (or variant rows)**, which is $0.001 per output row, Apify platform usage included, plus Apify's run start fee of **$0.005** (one start event per GB of run memory; the default 256 MB run is charged one). Every output row is one event: a product row, a variant row in variants mode, or a monitor row (including `removed` rows). **Free:** error rows (not a Shopify store, password-protected, feed disabled, blocked, invalid input) and, in monitor mode, products that didn't change.

**Worked examples** (row charges only):

- One store with 800 products, one row per product: 800 × $0.001 = **$0.80**.
- The same store in variants mode, 4 variants per product on average: 3,200 rows = **$3.20**.
- A daily monitor of 10 stores with 1,000 products each: the first run outputs 10,000 `new` rows (**$10.00**); after that, if about 30 products change per day, about **$0.03 per day**.

If you set a maximum cost per run, the Actor stops cleanly at that limit.

### FAQ and limits

#### Which data does it read?

Only the store's public storefront feed: `https://<store>/products.json` (or `/collections/<handle>/products.json` for a collection), page by page, 250 products per page until a page comes back empty. To find the store's currency it also reads `https://<store>/meta.json` once per store. Nothing behind a login, no cart or checkout calls.

#### Why did a store give an error row?

Some Shopify stores turn off or block `/products.json`, are password-protected, or are protected by a bot filter (HTTP 401, 403 or 430). Sites that aren't on Shopify answer 404 or an HTML page. Each of these gets one free error row explaining what happened. Headless Shopify stores that run their storefront on a different system often don't serve the feed on their main domain; try the store's `*.myshopify.com` address if you know it.

#### Does it show stock quantities?

No. The public feed only says whether each variant is available to buy, not how many units are left.

#### Why is `currency` empty?

The products feed doesn't include a currency. The Actor takes it from the store's `/meta.json`, which most Shopify stores publish; when that isn't available the field is `null` rather than a guess. Prices are what the public feed returns, which is normally the store's main currency; stores selling in several currencies may show visitors other prices on their pages.

#### Are all products included?

Only published products that the store shows on its online storefront. Products hidden from the online store, or only sold in other channels, aren't in the feed. **Max products per store** (default 1,000, up to 50,000) caps how many are read per store.

#### Can I read a collection only?

Yes: give the collection URL, for example `https://shop.example.com/collections/sale`. Rows then have `collection: "sale"`, and monitor mode tracks that collection separately from the whole store.

#### Is it allowed?

It reads data the store publishes openly to every visitor, at a slow, polite pace. Check that your use of the data respects the store's terms and applicable law.

### Related Actors

| Actor | Use it to |
|---|---|
| [Send Email Report & Alerts (Slack, Discord, Teams, Telegram)](https://apify.com/charlesmarkette/send-email-report-slack-discord-teams-telegram-alerts) | Get an email or chat message with each run's price drops and restocks. |
| [Google Sheets Sync](https://apify.com/charlesmarkette/google-sheets-sync) | Keep a Google Sheet of products and prices up to date. |
| [Merge & Deduplicate Datasets](https://apify.com/charlesmarkette/merge-deduplicate-datasets-only-new-items) | Combine the outputs of several runs or stores into one de-duplicated list. |
| [Dataset Delivery (SFTP, FTP, Dropbox, Google Drive)](https://apify.com/charlesmarkette/dataset-delivery-export-to-sftp-ftp-dropbox-google-drive) | Deliver the product export as CSV, JSON or Excel files on a schedule. |
| [Website Tech Stack Detector](https://apify.com/charlesmarkette/website-tech-stack-detector-shopify-cms-analytics) | Find out which websites in a list run on Shopify, then read their products here. |

# Actor input Schema

## `startUrls` (type: `array`):

One per line. A Shopify store's home page (https://shop.example.com), a bare domain (shop.example.com) or a collection URL (https://shop.example.com/collections/sale) to read only that collection. The store's public products feed (/products.json) is read; no login, no cart or checkout.

## `datasetId` (type: `string`):

Optional. A dataset whose items hold store URLs or domains (for example from a tech-stack or Google Maps run). Each item's store is read like the URLs above. Use {{resource.defaultDatasetId}} in an integration.

## `urlField` (type: `string`):

The item field that holds the store URL or domain. Dotted paths work (company.website). Default: the first of url, website, domain, storeUrl that has a value.

## `outputMode` (type: `string`):

Products: one row per product, with its variants in a nested list. Variants: one row per variant (size, colour…) with the product's fields copied onto it, which is easier in spreadsheets. Each row is charged either way.

## `includeBodyText` (type: `boolean`):

Add the product description as plain text (HTML tags removed, up to 20,000 characters). Turn off for smaller rows.

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

Stop reading a store (or collection) after this many products. In monitor mode, a store cut off by this limit gets no 'removed' rows, because the products after the limit weren't seen.

## `maxResults` (type: `integer`):

Optional cap on output rows across all stores (products, variant rows or change rows).

## `onlyChanges` (type: `boolean`):

Remember each product between runs and output only products that are new, dropped or raised their price, came back in stock, sold out, or changed title or variant count, plus 'removed' rows for products that disappeared. The first run outputs every product as 'new'. Schedule it daily for price and stock alerts.

## `monitorName` (type: `string`):

Optional. Names the memory used by monitor mode, so you can add or remove stores without losing what was remembered for the others, or keep separate monitors for the same stores. Empty: the memory is tied to the exact list of stores.

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

Pause between two requests to the same store. Each store gets one request at a time; up to 3 different stores are read in parallel.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.allbirds.com"
  ],
  "outputMode": "products",
  "includeBodyText": true,
  "maxProductsPerStore": 50,
  "onlyChanges": false,
  "requestDelayMs": 500
}
```

# Actor output Schema

## `items` (type: `string`):

No description

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

No description

# 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 = {
    "startUrls": [
        "https://www.allbirds.com"
    ],
    "maxProductsPerStore": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("charlesmarkette/shopify-store-products-price-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 = {
    "startUrls": ["https://www.allbirds.com"],
    "maxProductsPerStore": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("charlesmarkette/shopify-store-products-price-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 '{
  "startUrls": [
    "https://www.allbirds.com"
  ],
  "maxProductsPerStore": 50
}' |
apify call charlesmarkette/shopify-store-products-price-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,charlesmarkette/shopify-store-products-price-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/MkBM5AimaGDPri1ux/builds/jsL4Gmw1JDIxjak9L/openapi.json
