# Shopify Store Products & Price Monitor (`lemniworks/shopify-products-price-monitor`) Actor

Export every product and variant from public Shopify stores: price, compare-at price, stock, SKU, images, tags. Optional price-change, out-of-stock and new-product flags between runs. No browser, no login.

- **URL**: https://apify.com/lemniworks/shopify-products-price-monitor.md
- **Developed by:** [Lemni Works](https://apify.com/lemniworks) (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 product scrapeds

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 every product and variant from any public **Shopify store**: price, compare-at (sale) price, stock status, SKU, images, tags and more. It reads each store's public product feed, so there's **no browser, no login and no Shopify API key**. Turn on **Compare with the previous run**, schedule it, and every row also tells you what changed since last time: price moves, variants that sold out or came back, and new products.

### 3 ways to use it

#### 1. Track competitor prices and sales

Add 5–20 competitor stores, turn on **Compare with the previous run** and schedule a daily run. Open the **Price & stock changes** view and sort by `priceChange` to see who cut prices overnight. A filled `compareAtPrice` means the item is on sale. Send the results to Google Sheets or Slack with Apify integrations.

#### 2. Get alerts when sizes sell out or come back

Every size and color is its own row, with `wentOutOfStock` and `backInStock` flags. Connect a webhook or the Slack integration and alert only on rows where `backInStock` is `true`. Useful for resellers, merchandisers and brand-protection teams.

#### 3. Export a full catalog for research or migration

Download a store's whole catalog as CSV, Excel or JSON: vendor, product type, tags, prices, images and product URLs. Use it for product research, supplier vetting, price-distribution analysis, or as a starting point for a catalog migration. The `newProduct` flag shows launches as they happen.

### Input

| Field | Required | What it does |
|---|---|---|
| `stores` | yes | Store URLs or domains, one per line (`allbirds.com`, `https://www.brooklinen.com`). Paste a collection URL (`…/collections/mens`) to scrape only that collection. |
| `collection` | no | Collection handle (or URL) applied to every store above. |
| `compareToPrevious` | no | Adds `priceChange`, `previousPrice`, `wentOutOfStock`, `backInStock` and `newProduct` to each row. Default `false`. |
| `maxProductsPerStore` | no | Stop after this many products per store. `0` (default) = everything. |
| `snapshotStoreName` | no | Where snapshots are kept between runs (a named key-value store in your account). |
| `proxyConfiguration` | no | Off by default. Turn on Apify Proxy only if a store refuses requests. |

Example input:

```json
{
  "stores": ["https://www.allbirds.com", "brooklinen.com"],
  "compareToPrevious": true
}
```

### Output example

One row per **variant** (each size/color). You can download the dataset as JSON, CSV, Excel or HTML, or read it through the API. This is a real row from allbirds.com, from a run with **Compare with the previous run** turned on, three minutes after the previous one, so nothing had changed (tags shortened):

```json
{
  "store": "allbirds.com",
  "storeName": "Allbirds",
  "collection": null,
  "productId": 6858599596112,
  "title": "Women's Tree Dasher Relay - Blizzard/Hazy Cobalt (Clarity Blue Sole)",
  "vendor": "Allbirds",
  "productType": "Shoes",
  "tags": ["allbirds::carbon-score => 5.47", "allbirds::cfId => color-womens-tree-dasher-relay-blzz-hzy-cblt-clrty-bl"],
  "variantId": 40234510123088,
  "variantTitle": "5",
  "sku": "A10692W050",
  "price": 67,
  "compareAtPrice": 135,
  "currency": "USD",
  "available": false,
  "createdAt": "2023-07-15T21:42:13-07:00",
  "updatedAt": "2026-10-31T07:23:24-07:00",
  "publishedAt": "2026-09-25T16:49:33-07:00",
  "imageUrl": "https://cdn.shopify.com/s/files/1/1104/4168/files/A10691_24Q1_Tree-Dasher-Relay-Blizzard-Hazy-Cobalt-Clarity-Blue_PDP_LEFT.png?v=1776183508",
  "productUrl": "https://www.allbirds.com/products/womens-tree-dasher-relay-blizzard-hazy-cobalt",
  "scrapedAt": "2026-10-31T14:23:24.094Z",
  "previousSnapshotAt": "2026-10-31T14:19:56.156Z",
  "newProduct": false,
  "previousPrice": 67,
  "priceChange": 0,
  "wentOutOfStock": false,
  "backInStock": false
}
```

If the price had dropped from $67 to $55, the row would show `"previousPrice": 67, "priceChange": -12`. On the **first** monitored run there is nothing to compare yet, so the comparison fields are `null`. Without `compareToPrevious` they're left out.

Stores that can't be read produce an **error row** instead of stopping the run:

```json
{
  "store": "example.com",
  "storeUrl": "https://example.com/products.json?limit=250&page=1",
  "collection": null,
  "page": 1,
  "errorCode": "NOT_FOUND",
  "error": "No /products.json here. The site may not run on Shopify, or uses a headless storefront; try its .myshopify.com domain.",
  "scrapedAt": "2026-10-31T14:19:43.468Z"
}
```

### Pricing

Pay per event. You pay only for products you receive.

| Event | Price | Charged when |
|---|---|---|
| Actor start | $0.00005 | Once per run (Apify's standard start fee) |
| Product scraped | **$1.00 per 1,000 products** | Once per product saved, **however many variants it has** |

Error rows are free. Example: all of allbirds.com is 356 products and 4,168 variant rows, so a full export costs about $0.36. Set a maximum cost per run in Apify Console; the Actor saves and charges only what fits, then stops.

### How it works and what to expect

- **Which stores work?** Any Shopify store that serves its public `/products.json`, which is most of them. Headless storefronts (custom frontends on top of Shopify) often don't serve it on their main domain. Try the store's `.myshopify.com` domain instead.
- **25,000-product ceiling.** Shopify lists at most 25,000 products per store or collection through this feed. Bigger catalogs are cut there, and the log says so. Scrape them collection by collection to get past it.
- **Prices** are the store's default-market prices in the store currency (`currency`, read from the store's `/meta.json`). International prices from Shopify Markets aren't included.
- **Comparison snapshots** are kept in a named key-value store in your account (default `shopify-price-monitor-snapshots`), one record per store or collection, saved as soon as each store finishes. A run that doesn't see the whole catalog (product cap, error, spending limit) merges what it saw into the old snapshot instead of replacing it, so nothing is falsely reported as new next time. Products you didn't receive keep their old values, so their changes still show up when you do get them.
- **`newProduct` needs a full catalog read.** It's exact when the previous run read the whole store or collection. If the store is always cut short (a product cap, or over 25,000 products), it's `true` only for products created after the last snapshot and `null` (unknown) for the rest.
- **No double charges:** a product that shifts between pages while the store is being read (the catalog changed mid-run) is saved and charged once. If Apify moves the run to another server, product caps and duplicate checks carry over.
- **Polite by default:** a store's pages are fetched one after another, with at most 4 requests in flight and about 120 new page requests a minute. Rate limits (including Shopify's HTTP 430) and server errors are retried with exponential backoff, honoring `Retry-After`.

#### Error codes

| `errorCode` | Meaning |
|---|---|
| `INVALID_INPUT` | The store URL couldn't be parsed. |
| `NOT_FOUND` | No `/products.json` at that domain (not Shopify, or headless). |
| `NOT_SHOPIFY` | The response wasn't a Shopify product feed. |
| `PASSWORD_PROTECTED` | The store is behind a password page. |
| `BOT_CHALLENGE` | A CDN bot challenge answered instead of the store. |
| `NO_PRODUCTS` | The store or collection returned no products. Shopify returns an empty list for collections that don't exist, so check the handle. |
| `RATE_LIMITED`, `BLOCKED`, `SERVER_ERROR`, `NETWORK_ERROR`, `BAD_JSON` | Still failing after 6 attempts with backoff. |
| `HTTP_<code>` | Any other HTTP error (no retry). |
| `UNEXPECTED_ERROR` | Something else went wrong for this store; the rest of the run continues. |

### Is it legal to scrape Shopify stores?

This Actor reads only public product information that stores publish in their own product feed. It doesn't log in, check out, or collect customer data. You should still make sure your use of the data complies with the stores' terms and with the laws that apply to you. If you're unsure, consult your lawyers.

Questions, bugs or a store that doesn't work? Open an issue on the **Issues** tab.

# Actor input Schema

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

Public Shopify storefronts, one per line. A domain is enough (allbirds.com). To scrape a single collection of one store, paste its collection URL (https://www.allbirds.com/collections/mens).

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

Only scrape products in this collection, for every store above. Use the handle from the collection URL (…/collections/<handle>) or paste the full URL. A collection URL in the store list overrides this for that store.

## `compareToPrevious` (type: `boolean`):

Keep a snapshot of each store and add priceChange, previousPrice, wentOutOfStock, backInStock and newProduct to every row. The first run creates the snapshot; later runs compare against it. Schedule the Actor to get a price and stock monitor.

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

Stop after this many products per store (0 = no limit). Useful for testing or capping cost. Shopify itself lists at most 25,000 products per store or collection.

## `snapshotStoreName` (type: `string`):

Named key-value store in your account that keeps the last snapshot per store for “Compare with the previous run”. Use a different name per monitor if two schedules watch the same store at different frequencies. Letters, digits and hyphens only.

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

Usually not needed: product feeds are public. Turn on Apify Proxy if a store refuses requests (HTTP 403).

## Actor input object example

```json
{
  "stores": [
    "https://www.deathwishcoffee.com"
  ],
  "compareToPrevious": false,
  "maxProductsPerStore": 0,
  "snapshotStoreName": "shopify-price-monitor-snapshots",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

No description

## `changes` (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 = {
    "stores": [
        "https://www.deathwishcoffee.com"
    ]
};

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

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

```

## MCP server setup

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