# Shopify Products Scraper with Price and Stock Change Tracking (`titian_fluorite/shopify-products-scraper`) Actor

Shopify products scraper for any storefront. Export the full catalog with variants, prices and stock, and see what changed since the last run. Pay per product.

- **URL**: https://apify.com/titian\_fluorite/shopify-products-scraper.md
- **Developed by:** [Gerald Dobin](https://apify.com/titian_fluorite) (community)
- **Categories:** E-commerce, Lead generation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 scraped 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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Shopify Products Scraper

Give this Actor a list of Shopify stores and it hands back the whole public catalog as one clean row per product: title, URL, vendor, product type, tags, the full price range, whether anything is in stock, and every size and colour variant with its own price, compare at price, SKU and stock flag, plus images and option axes. Run it again tomorrow and it tells you what moved, marking each product new, priceChanged, availabilityChanged, updated, unchanged or removed, with the old prices sitting next to the new ones. No browser, no login, no API key, no setup: paste a store address and press start.

### Who it is for

**Merchants watching competitors.** Put your rivals in the list, schedule it daily, and switch on "Only return what changed". You get a short dataset every morning holding exactly the price cuts, the restocks, the sell outs and the new arrivals, and nothing else.

**Agencies and dropshippers researching catalogs.** Pull an entire store, or just the collections you care about, into a spreadsheet in one run. Every variant is a row you can sort by price, filter by vendor, or join to your own SKU list on the stable Shopify product id.

**Analysts looking across a market.** Point it at fifty stores in one go and get a single comparable table: pricing distributions, discount depth from the compare at price, assortment size by vendor and product type, and how fast each store turns its catalog over.

### Sample output

```json
{
  "store": "www.allbirds.com",
  "storeName": "Allbirds",
  "currency": "USD",
  "productId": 7205177819216,
  "handle": "anytime-crew-sock-blizzard-verdant-green",
  "url": "https://www.allbirds.com/products/anytime-crew-sock-blizzard-verdant-green",
  "title": "Anytime Crew Sock - Blizzard, Verdant Green",
  "vendor": "Allbirds",
  "productType": "Socks",
  "tags": ["allbirds::material => cotton", "YCRF_socks"],
  "available": true,
  "priceMin": 15,
  "priceMax": 18,
  "compareAtPriceMin": 18,
  "compareAtPriceMax": 18,
  "variantCount": 4,
  "variants": [
    {
      "id": 41271102799952,
      "title": "S (W5-7)",
      "sku": "A12705U001",
      "price": 15,
      "compareAtPrice": 18,
      "available": true,
      "options": ["S (W5-7)"],
      "grams": 59,
      "requiresShipping": true
    }
  ],
  "images": ["https://cdn.shopify.com/s/files/1/.../sock.jpg"],
  "featuredImage": "https://cdn.shopify.com/s/files/1/.../sock.jpg",
  "options": [{ "name": "Size", "values": ["S (W5-7)", "M (W8-10 / M8)", "L (W11 / M9-12)", "XL (M13-14)"] }],
  "bodyHtml": null,
  "description": null,
  "createdAt": "2025-08-06T23:11:35.000Z",
  "updatedAt": "2026-09-08T01:10:51.000Z",
  "publishedAt": "2026-06-11T23:48:52.000Z",
  "changeType": "priceChanged",
  "previous": { "priceMin": 18, "priceMax": 18, "available": true },
  "scrapedAt": "2026-09-08T02:14:07.881Z",
  "error": null
}
```

### How change tracking works

The first run of a store labels everything `new`. At the end of every run the Actor saves a small snapshot of the store in a key-value store called `shopify-products-scraper-state` in your own account: for each product its price range, stock flag, variant count and a short fingerprint of the fields a shopper can see, and for each variant its price, compare at price and stock flag. No descriptions, no image addresses, no tag lists, so even a catalog of tens of thousands of products stays small.

The next run compares against that snapshot and sets `changeType` on every row:

| Value | What it means |
| --- | --- |
| `new` | The product was not in the previous run. |
| `priceChanged` | A price or a compare at price moved, on the product or on any single variant. |
| `availabilityChanged` | The product or one of its variants went in or out of stock. |
| `updated` | The store changed the title, vendor, product type, tags, options or images, or added or dropped a variant, without moving a price or the stock. |
| `unchanged` | Nothing at all moved. |
| `removed` | The product was in the previous run and is gone from the catalog now. |

A price move is reported ahead of a stock move, and a stock move ahead of any other edit, so a price drop is never hidden behind a title tweak that happened in the same hour. The `previous` field on every row carries the price range and the stock flag the last run saw, so you can compute the size of a move without keeping your own history.

**Description edits on their own do not count as a change.** This is a price and stock monitor, and comparison is done on prices, stock, the variant lineup, and the title, vendor, product type, tags, options and images. A merchant rewriting the marketing copy under an otherwise identical product leaves the row as `unchanged`, so a monitoring schedule is not billed for copywriting. Turn on "Include the product description" and read `description` yourself if copy changes are what you are after.

Shopify's own `updated_at` timestamp is deliberately not used to decide this. Many large stores rewrite it on every product on every request as their inventory syncs, so a scraper that trusted it would report the entire catalog as changed on every single run and bill you for all of it. Comparison is done on the things a shopper would actually notice instead. The raw `updatedAt` is still on every row, so you can use it if you want it.

**To run it as a monitor:** switch on "Only return what changed", then schedule the Actor daily or hourly. Each run gives you a dataset holding only the products that actually moved, and you are billed only for those rows. Point a webhook or an integration at the run and you have a price and stock alert.

**What `removed` means.** It means the product was absent from a full read of the catalog, and the Actor is careful about claiming it. A store is only allowed to report removals when every page of it came back without error and the last page came back empty, nothing was cut short by "Maximum products per store", a collection filter or the run's cost limit, and the previous snapshot loaded whole. Anything less and a product it did not see is treated as unseen rather than gone, and the run says so in the log. A limited run also merges what it saw into the saved snapshot rather than replacing it, so the next full run still knows about the rest.

One run at a time per store and settings. Two runs of the same task overlapping is not a supported way to use this Actor: they would each write a baseline over the other. Schedule them far enough apart to finish.

**Each set of settings keeps its own baseline.** The snapshot is keyed by the store plus the settings that decide which products a run even looks at: the collection filter, the per store limit and the store address. So a daily task reading a whole catalog and a second task watching one collection on the same store do not overwrite each other, which would otherwise make the narrower one report everything outside its collection as removed. The practical consequence is that changing those settings starts a fresh baseline: the next run after such a change labels everything `new` once, then settles down.

### Input reference

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `stores` | list of text | three example stores | The storefronts to read. A bare domain like `gymshark.com`, a full address like `https://www.allbirds.com`, or a myshopify.com address. A link to a product or collection page is reduced to the store root. |
| `collections` | list of text | empty | Limit the run to certain collections by handle, for example `mens` or `new-arrivals`. Empty means the whole catalog. |
| `maxProductsPerStore` | number | `50` | How many products to read from each store, so a first run is quick and cheap. Set `0` to read the whole catalog. |
| `includeDescription` | true or false | `false` | Add the product description as cleaned HTML (`bodyHtml`) and as plain text (`description`). |
| `trackChanges` | true or false | `true` | Compare against the previous run and set `changeType` and `previous`. |
| `onlyChanges` | true or false | `false` | Return only the rows whose `changeType` is not `unchanged`. |
| `maxConcurrency` | number | `5` | How many requests to run at once across all stores. Each store is always read one page at a time. |

Every run with the defaults works as it stands, so you can press start and see real output before changing anything.

### Pricing

Pay per event, one event: `Scraped product`, charged once for every product row you receive. Error rows are never charged, and in "Only return what changed" mode the products that did not move are neither returned nor charged, which is what makes a daily monitor cheap to run. Set a maximum cost on the run and the Actor stops cleanly at that limit rather than handing you rows it cannot bill for, and a run stopped that way leaves the changes it did not deliver in place, so the next run still reports them.

### Limits

The public catalog is paged by number over a catalog the merchant may be editing while the run reads it, so a product that moves between pages mid run can be missed by that run and picked up by the next one. This is why a removal is only ever reported after a clean full read, described above. Some stores turn the public product endpoint off, or put a bot check in front of it. Those produce an error row saying so, and they are never billed. The endpoint carries the catalog only, so there is no collection level metadata such as a collection title or description, and no inventory counts beyond the in stock flag each variant publishes. Reviews, ratings, question and answer content and anything behind a customer login are not included, because none of that is in the public catalog JSON. Prices are exactly what the storefront quotes to an anonymous visitor in the store's own currency, so a store that shows different prices by country returns the ones its default market publishes.

Product descriptions are returned as cleaned HTML. Scripts, styles, embeds, frames, forms, images and every event handler are stripped out, and only a small allowlist of formatting tags and safe links survives, so a description is safe to put straight into a page of your own.

This Actor reads only the public storefront JSON that every Shopify store serves to anonymous visitors. No cookies, no login, no accounts, no personal data.

# Actor input Schema

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

The storefronts to read. Paste the store address, for example https://www.allbirds.com or gymshark.com. A myshopify.com address works too. If you paste a link to a product or a collection page, the whole store is read.

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

Optional. Limit the run to certain collections by handle, for example mens or new-arrivals. The handle is the last part of a collection address. Leave this empty to read the whole catalog.

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

How many products to read from each store. The default of 50 keeps a first run quick and cheap. Set 0 to read the whole catalog. Every product row you receive is billed.

## `includeDescription` (type: `boolean`):

Add the product description as cleaned HTML and as plain text. Descriptions are long, so this makes the dataset much bigger. Leave it off for price and stock monitoring.

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

Compare every product and variant with the previous run of this Actor on the same store and label each row new, priceChanged, availabilityChanged, updated, unchanged or removed. The first run labels everything new.

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

Skip products that are exactly as they were last run. Turn this on for a scheduled monitor: you get a dataset that holds only the price moves, the restocks and the new arrivals, and you are not billed for the rest.

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

How many requests to run at once across all stores. Each individual store is always read one page at a time, so this only matters when you give the Actor a long list of stores.

## Actor input object example

```json
{
  "stores": [
    "https://www.allbirds.com",
    "https://gymshark.com",
    "https://www.rothys.com"
  ],
  "collections": [],
  "maxProductsPerStore": 50,
  "includeDescription": false,
  "trackChanges": true,
  "onlyChanges": false,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

Every row with the full nested variants, images and options.

## `productsCsv` (type: `string`):

The same rows as CSV for spreadsheets.

## `changesCsv` (type: `string`):

Just the products that moved: new arrivals, price changes, stock changes and removals.

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

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

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

```

## MCP server setup

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