# Shopify Products Scraper & Price Tracker: Variants, Stock, SKUs (`jtpalms/shopify-store-products`) Actor

Scrape every product and variant from Shopify stores: prices, compare-at prices, stock, SKUs, images, tags and descriptions. Schedule it as a Shopify price tracker for price drops, price increases, restocks, sell-outs, new and removed products. Pay only per product.

- **URL**: https://apify.com/jtpalms/shopify-store-products.md
- **Developed by:** [JT Palms](https://apify.com/jtpalms) (community)
- **Categories:** E-commerce, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Products Scraper & Price Tracker: scrape Shopify stores, then track their price changes

A Shopify products scraper with built-in price tracking. Give it a list of Shopify stores and get every product back: title, vendor, type, tags, prices, compare-at prices, sale flag, stock status, every variant with SKU and options, images and description. Turn on **change tracking**, schedule it daily, and each run tells you which products dropped in price, went up, came back in stock, sold out, are new, or were removed.

**USD 2 per 1,000 products.** Stores that are not Shopify, unreachable or blocked are reported free. Removed-product alerts are free.

### What people use it for

- **Competitor price monitoring.** Schedule a daily run over your competitors' stores with **Only output changes** on. You get a short list of price drops, price increases, restocks, sell-outs and new launches, and pay only for those rows.
- **Catalog research for dropshipping and DTC brands.** See a store's full range, price points, discount depth (compare-at vs price), best vendors and product types before you pick a niche or a supplier.
- **Product feeds.** Pull a clean product and variant feed (SKU, price, stock, images) from your own store or a partner's for Google Sheets, a comparison site, or an affiliate catalog.
- **Market research.** Compare pricing, assortment size, sale frequency and launch pace across a whole category of Shopify brands.

### Change tracking (the main reason to schedule it)

With **Track changes between runs** on, every row gets a `change` field that compares it with the previous run:

| `change` | Meaning |
|---|---|
| `new` | Not seen in the previous run (a new launch, or the first run for this store) |
| `price_drop` | Lowest price went down (or, if equal, the highest price went down) |
| `price_increase` | Lowest price went up (or, if equal, the highest price went up) |
| `back_in_stock` | Was sold out, now at least one variant is available |
| `sold_out` | Was available, now every variant is sold out |
| `unchanged` | Same price range and same stock status |
| `removed` | Was in the previous run, no longer listed (deleted, unpublished or moved out of the collection). Free. |

`previousPriceMin` and `previousPriceMax` hold the earlier prices, and `changes` lists every label that applies when more than one does (for example `["price_drop", "back_in_stock"]`). A product that got cheaper looks like this (other fields left out):

```json
{ "title": "Classic Crew Tee", "priceMin": 25, "previousPriceMin": 35, "change": "price_drop", "changes": ["price_drop"] }
```

**Only output changes** skips the `unchanged` rows, so you pay only for products that actually changed.

**Schedule tip.** Save your store list as a task with **Track changes** and **Only output changes** on, then add a schedule (for example daily at 06:00). The first run sets the baseline and marks every product `new`; from the second run on you get only real changes. Add an integration (email, Slack, Google Sheets, Make, Zapier or a webhook) on "run succeeded" to receive each day's changes automatically.

How it works: after each run the actor keeps a small snapshot per store (product ID, title, price range, in stock) in a key-value store named `shopify-price-tracker` in your account, one record per store (and per collection, if you use one). Named stores do not expire, so the history survives between runs. Good to know:

- Snapshots are shared by every run in your account that tracks the same store and collection. An extra manual run with tracking on moves the baseline forward.
- `removed` is only reported when the whole catalog was read. Runs limited by **Max products per store**, stopped by your cost limit, or cut short by an error do not report removals, and they keep the older snapshot entries for products they did not reach.
- To start over for a store, delete its record from the `shopify-price-tracker` store under **Storage**.

### Sample output

One row per product (from a real run on allbirds.com with tracking on; arrays and text shortened):

```json
{
  "store": "allbirds.com",
  "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", "shoprunner"],
  "url": "https://allbirds.com/products/womens-allbirds-flip-flop-dusty-pink",
  "createdAt": "2026-03-27T14:03:50-07:00",
  "publishedAt": "2026-09-25T16:58:13-07:00",
  "updatedAt": "2026-09-28T11:14:26-07:00",
  "priceMin": 25,
  "priceMax": 25,
  "compareAtPriceMax": 50,
  "currency": "USD",
  "onSale": true,
  "available": true,
  "variantCount": 7,
  "optionNames": ["Size"],
  "variants": [
    { "id": 42146889039952, "title": "5", "sku": "A12513W050", "price": 25, "compareAtPrice": 50, "available": false, "option1": "5", "option2": null, "option3": null, "grams": 455 },
    { "id": 42146889072720, "title": "6", "sku": "A12513W060", "price": 25, "compareAtPrice": 50, "available": false, "option1": "6", "option2": null, "option3": null, "grams": 492 }
  ],
  "images": ["https://cdn.shopify.com/s/files/1/1104/4168/files/A12513_26Q2_Allbirds-Flip-Flop-Dusty-Pink_PDP_LEFT.png?v=1774646345"],
  "bodyText": "Sun on your feet. Comfort underneath. Light, easy, and made for warm weather, these flip flops bring everyday comfort to sunny days, beach walks, and everything...",
  "scrapedAt": "2026-09-28T18:14:27.498Z",
  "change": "unchanged",
  "changes": [],
  "previousPriceMin": 25,
  "previousPriceMax": 25
}
```

In **Variant** mode you get one row per size or color instead, with the product fields repeated and these added: `variantId`, `variantTitle`, `sku`, `price`, `compareAtPrice`, `variantAvailable`, `variantOnSale`, `option1` to `option3`, `grams`. The row `url` links straight to that variant.

A store that could not be read comes back as one free row with an `error`, for example `"Not a Shopify store or products.json disabled (HTTP 404)."`, `"Domain not found (DNS lookup failed). Check the spelling."` or `"Store is password protected. Its products are not public."`

Export as JSON, CSV or Excel, or read the results through the Apify API. The dataset has three table views: Products, Changes and Variants.

### How to use it

1. Put your stores in **Shopify stores**, one per line. Bare domains (`allbirds.com`), full URLs and `*.myshopify.com` addresses all work. A collection URL such as `https://store.com/collections/sale` reads only that collection.
2. Optional: set **Collection** to read one collection from every store, **One row per** to Variant, or **Max products per store** for a quick sample.
3. Optional: turn on **Track changes between runs** and **Only output changes**, then schedule the actor (see the tip above).
4. Click **Start**, then download the results or connect them to your tools.

### Pricing

| What | Price |
|---|---|
| Product row (or variant row in Variant mode) | USD 0.002 (USD 2 per 1,000) |
| Unchanged product skipped by **Only output changes** | Free |
| `removed` row | Free |
| Store that is not Shopify, unreachable, blocked or password protected | Free |

Examples: a full read of a 2,000-product store costs USD 4. Tracking 5 stores of 2,000 products each with **Only output changes** costs USD 20 for the first (baseline) run; after that you pay per changed product, so a day on which 200 products change costs USD 0.40.

In Variant mode every variant row counts, so a product with 8 sizes is 8 rows. Set a maximum cost per run in the run options and the actor stops cleanly when it reaches it; everything saved before that is kept.

### Limits

- **The actor identifies itself.** Every request carries the user agent `Mozilla/5.0 (compatible; shopify-store-products/1.0; +https://apify.com/JTPalms/shopify-store-products)`, with plain request headers. It never pretends to be a browser. Some sites block bots; a blocked request is reported as an error, not hidden.
- It reads the public `/products.json` feed that Shopify storefronts serve. Only products published to the online store appear; hidden, draft and B2B-only products do not.
- Some stores disable or block that feed (headless storefronts, custom proxies, password pages). Those are reported free with an error. The actor also tries the `www.` host and the store's `*.myshopify.com` address before giving up.
- Stock is a yes or no per variant. Shopify does not publish inventory quantities in this feed.
- Prices are what the feed returns, normally in the store's default currency, which is read from the store's `/meta.json`. Localized prices for other countries are not included.
- Descriptions are plain text capped at 2,000 characters, and up to 10 images are listed per product.
- Pages are read one at a time per store with a short pause between them, and rate limits (HTTP 429) and server errors are retried up to 3 times. Large stores (10,000+ products) take a minute or two each.

### Related actors

- [WooCommerce Products Scraper & Price Tracker](https://apify.com/JTPalms/woocommerce-store-products): Scrape every product from any WooCommerce store through its public Store API: prices, sale prices, stock, SKUs, categories, tags,...
- [Website Tech Stack Detector (Wappalyzer Alternative)](https://apify.com/JTPalms/tech-stack-detector): Find the technology behind any website in bulk: CMS, ecommerce platform, analytics, frameworks, CDN, hosting, payment, email and...

### FAQ

**Is it legal?** It reads `/products.json`, a public endpoint every Shopify storefront serves to anyone, the same product data shown on the store's own pages. It does not log in, bypass protections, or collect personal data. As with any data, check how you use it against the store's terms and your local rules.

**Do I need a Shopify API key or store access?** No. It works on any public Shopify store without keys, apps or login.

**How do I know which sites run on Shopify?** Just add them. Non-Shopify sites come back free with an error. To filter a big list first, run it through a tech stack detector.

**Why do I see a different price in my browser?** Stores that sell in several countries can show a converted or localized price to visitors. This actor reports the feed price in the store's default currency.

**Can I get an alert when a competitor drops a price?** Yes. Schedule a task with **Only output changes** on and add an email, Slack or webhook integration. Each run then sends only the changed products.

**Something looks wrong for a store?** Open an issue with the store domain.

# Actor input Schema

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

Store domains or URLs, one per line. Examples: allbirds.com, https://www.gymshark.com, mystore.myshopify.com. A collection URL (https://store.com/collections/sale) reads only that collection. Duplicates are removed automatically.

## `collectionHandle` (type: `string`):

Read only one collection from every store, for example "sale" or "new-arrivals" (the part after /collections/ in the URL). Leave empty to read the whole catalog.

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

Product: one row per product with its variants listed inside. Variant: one row per size or color, with the product fields repeated on each row. Each row is billed as one product event.

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

Stop after this many products from each store. 0 means no limit. In variant mode this still counts products, not variants.

## `trackChanges` (type: `boolean`):

Compare with the previous run and add a "change" field to each row: new, price\_drop, price\_increase, back\_in\_stock, sold\_out or unchanged. Products that disappeared are reported as removed (free). Snapshots are kept in your "shopify-price-tracker" key-value store. Schedule the actor daily to use this.

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

Skip unchanged products, so you pay only for new, removed, repriced and restocked or sold-out products. Turns on change tracking. On the first run every product counts as new.

## `maxConcurrency` (type: `integer`):

How many stores to read at the same time. Pages within one store are always read one by one, with a short pause, to be polite.

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

Optional. Only needed if a store blocks requests from data centers (HTTP 403 or 429 errors).

## Actor input object example

```json
{
  "stores": [
    "allbirds.com"
  ],
  "outputMode": "products",
  "maxProductsPerStore": 100,
  "trackChanges": false,
  "onlyChanges": false,
  "maxConcurrency": 5
}
```

# Actor output Schema

## `results` (type: `string`):

All output rows in the default dataset (JSON, CSV, Excel via the format parameter).

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

Counts and per-input status for this run.

# 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"
    ],
    "maxProductsPerStore": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("jtpalms/shopify-store-products").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"],
    "maxProductsPerStore": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("jtpalms/shopify-store-products").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"
  ],
  "maxProductsPerStore": 100
}' |
apify call jtpalms/shopify-store-products --silent --output-dataset

```

## MCP server setup

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

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/H5Zawrh4pLlhwIRBi/builds/IAuGg34z53UqUYlso/openapi.json
