# Shopify Store Scraper: Products, Prices & Stock (`impressionable_lupine/shopify-store-scraper`) Actor

Scrape any Shopify store's full catalog from public products.json: prices, compare-at, variants, SKUs, stock and inventory. Track new, removed and price changes between runs. WooCommerce too. $1 per 1,000 products.

- **URL**: https://apify.com/impressionable_lupine/shopify-store-scraper.md
- **Developed by:** [Majd Hijazin](https://apify.com/impressionable_lupine) (community)
- **Categories:** E-commerce
- **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 Scraper — products, prices, variants & stock

**Shopify Store Scraper** exports the full product catalog of any Shopify store to JSON, CSV or Excel. It reads the public `products.json` endpoint that every Shopify store exposes, so it needs no login, browser or proxy. You get prices, compare-at prices, every variant and SKU, stock availability and (where the store exposes it) **inventory quantity**.

Turn on **change tracking** and schedule the scraper. Each run then reports which products are **new**, **removed**, **price changed** or **availability changed** since the last run. That makes it a cheap Shopify price and stock monitor for competitor research, MAP monitoring, dropshipping and AI agents.

- 🛍️ **Any Shopify store**: paste a domain like `allbirds.com`, a collection URL or product URLs.
- 💲 **Prices, discounts, variants and SKUs**: `priceMin`, `priceMax`, `compareAtPrice`, `onSale` and `discountPercent`, plus a `variants[]` array with SKU, options, price, availability, barcode and weight.
- 📦 **Stock and inventory**: `available`, available-variant counts, and per-variant `inventory_quantity` when the store exposes it.
- 🔁 **Change tracking**: new, removed, price_changed and availability_changed, including per-variant price and stock deltas.
- 🏪 **Optional store profile**: theme, currency, country, contact page, emails, social links, detected apps (Klaviyo, Judge.me, Recharge, Gorgias…) and the product count.
- 🧩 **WooCommerce too**: if a URL is not Shopify, the scraper tries the public WooCommerce Store API (`/wp-json/wc/store/v1/products`).
- 💵 **$1 per 1,000 products** ($0.001 each). No start fee, and platform usage is included.

### What data can you extract from a Shopify store?

| Field | Example | Notes |
|---|---|---|
| `store`, `storeUrl` | `kith.com` | Canonical host after redirects |
| `productId`, `handle`, `title`, `url` | `8286105305216`, `bb-139855` | The ID is a string, so no 64-bit precision loss |
| `vendor`, `productType`, `tags[]` | `BAUBLEBAR`, `Toys & Collectibles` | |
| `priceMin`, `priceMax`, `currency` | `35`, `35`, `USD` | Currency comes from `/meta.json` or `/cart.js` |
| `compareAtPrice`, `onSale`, `discountPercent` | `50`, `true`, `50` | The highest compare-at price across variants |
| `available`, `variantCount`, `availableVariantCount` | `true`, `7`, `5` | |
| `totalInventory`, `variants[].inventoryQuantity` | `172` | Needs `includeInventory`, or product URLs. Null when the store hides stock |
| `variants[]` | id, sku, title, option1-3, price, compareAtPrice, available, barcode, grams | Can be turned off with `includeVariants: false` |
| `options[]`, `images[]`, `featuredImage` | Size: S/M/L | |
| `createdAt`, `updatedAt`, `publishedAt` | ISO 8601 | From Shopify |
| `changeType`, `changes[]`, `variantChanges[]`, `previousPriceMin`, `previousAvailable` | `price_changed` | Only when `trackChanges` is on |
| `description` | plain text | Opt-in with `includeDescription` |
| `scrapedAt` | ISO 8601 | |

The dataset uses one flat record per product. Store profiles (`recordType: "store"`) and error records (`recordType: "error"`) are in the same dataset. Use the **Products**, **Changes since last run**, **Store profiles** and **Errors** views to see each type.

### How to scrape a Shopify store

1. Paste one or more store URLs into **Store URLs**, e.g. `https://www.allbirds.com`.
2. Optionally restrict the scrape with **Collection URLs** (`/collections/mens`) or **Product URLs** (`/products/tree-runner`).
3. Set **Max products per store**, then click **Start**.
4. Download the results as JSON, CSV, Excel or HTML, or read them through the API.

To build a **Shopify price monitor**, enable **Track changes since the last run** and **Output only changed products**. Then create a schedule (for example daily). The first run records a baseline. Later runs output and bill only the products that changed.

### Input example

```json
{
  "storeUrls": ["https://www.allbirds.com", "https://kith.com"],
  "collectionUrls": ["https://www.brooklinen.com/collections/sheets"],
  "productUrls": ["https://www.allbirds.com/products/mens-tree-runners"],
  "maxProductsPerStore": 500,
  "includeVariants": true,
  "includeInventory": false,
  "trackChanges": true,
  "onlyChanges": false,
  "analyzeStore": true
}
```

| Option | Default | What it does |
|---|---|---|
| `storeUrls` | — | Store home pages. The whole catalog is scraped via `/products.json?limit=250&page=N` |
| `collectionUrls` | — | Uses `/collections/<handle>/products.json` |
| `productUrls` | — | Uses `/products/<handle>.js` (includes inventory when it is exposed) |
| `maxProductsPerStore` | 1000 | Hard cap per store, from 1 to 100,000 |
| `includeVariants` | true | Adds the `variants[]` array |
| `includeInventory` | false | Makes one extra `.js` request per product to read `inventory_quantity`. Slower, same price |
| `includeDescription` | false | Adds the plain-text description |
| `trackChanges` / `onlyChanges` | false | Diffs against the previous run, which is stored in the named key-value store `stateStoreName` |
| `analyzeStore` | false | Adds one store profile per store (+$0.005) |
| `includeWooCommerce` | true | Falls back to the WooCommerce Store API |
| `requestDelayMs` | 500 | Polite delay between requests to the same store |
| `maxConcurrency` | 3 | Number of different stores scraped in parallel |

### Output example

```json
{
  "recordType": "product",
  "platform": "shopify",
  "store": "kith.com",
  "productId": "8286105305216",
  "handle": "bb-139855",
  "title": "BAUBLEBAR Kids DIY Bag Charm Craft Kit - Multi",
  "vendor": "BAUBLEBAR",
  "productType": "Toys & Collectibles",
  "tags": ["accessories"],
  "priceMin": 35,
  "priceMax": 35,
  "compareAtPrice": null,
  "onSale": false,
  "currency": "USD",
  "available": true,
  "variantCount": 1,
  "availableVariantCount": 1,
  "totalInventory": null,
  "sku": "10866430",
  "variants": [
    { "id": "45137916461184", "sku": "10866430", "title": "OS", "price": 35, "compareAtPrice": null, "available": true, "inventoryQuantity": null, "grams": 1361 }
  ],
  "images": ["https://cdn.shopify.com/s/files/1/0094/2252/files/139855_G_01_SQ.jpg"],
  "url": "https://kith.com/products/bb-139855",
  "createdAt": "2026-01-27T18:20:26-05:00",
  "updatedAt": "2026-10-20T14:19:33-04:00",
  "publishedAt": "2026-10-20T10:57:22-04:00",
  "source": "products.json",
  "changeType": "new",
  "scrapedAt": "2026-10-20T18:19:32.058Z"
}
```

Store profile (`analyzeStore: true`):

```json
{
  "recordType": "store",
  "store": "www.allbirds.com",
  "storeName": "Allbirds",
  "currency": "USD",
  "country": "US",
  "myshopifyDomain": "weareallbirds.myshopify.com",
  "theme": { "name": "[DNAM Theme July 2026]", "schemaName": "allbirds-theme", "schemaVersion": "1.231.12", "themeStoreId": null },
  "contactPage": null,
  "emails": ["help@allbirds.com"],
  "socialLinks": { "instagram": "https://www.instagram.com/allbirds", "tiktok": "https://www.tiktok.com/@weareallbirds" },
  "detectedApps": ["Attentive", "Google Tag Manager", "Shop Pay", "Elevar"],
  "productCount": 692,
  "collectionCount": 1346
}
```

A per-store run summary (status, counts, change totals, billing and HTTP stats) is saved as the `RUN_SUMMARY` record in the default key-value store.

### Pricing

This Actor uses **pay-per-event** pricing, and Apify platform usage is included.

| Event | Price | When it is charged |
|---|---|---|
| `product-scraped` | **$0.001** ($1 per 1,000) | For each product record delivered |
| `store-analyzed` | **$0.005** | Once per store, only with `analyzeStore: true` and only when a profile was built |

You are **not** charged for error records, `removed` records, or (with `onlyChanges`) unchanged products. Examples: 1,000 products cost $1.00. A daily monitor of a 2,000-product store where about 50 products change per day costs about $0.05 per day after the first run.

The run respects your **maximum charge per run** (`maxTotalChargeUsd`). The scraper stops delivering once the budget is used up, and it never bills a product it did not deliver. If the budget cannot cover a single product, the run stops immediately with `BUDGET_TOO_LOW`.

### API

Run the scraper and fetch the products in one call with the Apify API:

```bash
curl -X POST "https://api.apify.com/v2/acts/impressionable_lupine~shopify-store-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"storeUrls":["https://www.allbirds.com"],"maxProductsPerStore":100}'
```

JavaScript (`apify-client`):

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('impressionable_lupine/shopify-store-scraper').call({
  storeUrls: ['https://kith.com'],
  maxProductsPerStore: 500,
  trackChanges: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems({ view: 'overview' });
console.log(items.filter(i => i.changeType === 'price_changed'));
```

Python (`apify-client`):

```python
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("impressionable_lupine/shopify-store-scraper").call(run_input={"storeUrls": ["https://www.brooklinen.com"], "maxProductsPerStore": 200})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["title"], item["priceMin"], item["currency"], item["available"])
```

### MCP (Claude, Cursor and other AI agents)

AI agents can call this scraper as a tool through the [Apify MCP server](https://mcp.apify.com). Add it to your MCP client:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com/?actors=impressionable_lupine/shopify-store-scraper",
      "headers": { "Authorization": "Bearer <APIFY_TOKEN>" }
    }
  }
}
```

Then ask, for example: *"Get the 50 newest products from allbirds.com with prices and stock"*, or *"Which kith.com products changed price since the last run?"*. The flat output and the `changeType` field let an agent answer without extra parsing.

### Integrations

Send the results to Google Sheets, Airtable, Slack, Zapier, Make, n8n or a webhook with Apify integrations. For example, post every `price_changed` product to Slack after each scheduled run.

### Limitations

- **Public catalog only.** The scraper reads the same data as a store's visitors. It cannot see unpublished products, B2B or wholesale price lists, customer-specific pricing or cost of goods.
- **Inventory quantity is optional per store.** Many stores hide `inventory_quantity` (then `totalInventory` is `null`), and some cap or round it. Availability (`available`) is always present.
- **Prices are in the store's default currency and market.** Shopify Markets prices for other countries are not fetched.
- **Some stores block public catalog endpoints.** Headless storefronts (Hydrogen, Next.js) may block `products.json` on their public domain. The scraper then automatically tries the www/apex twin host and the store's `*.myshopify.com` domain. If all of these are blocked, you get a `PRODUCTS_JSON_DISABLED` error record. Sites behind a JavaScript bot challenge return `BOT_CHALLENGE`. Password-protected stores return `STORE_PASSWORD_PROTECTED`. No proxies or CAPTCHA solving are used.
- **WooCommerce support** uses the public Store API. It gives prices, stock status, images and categories, but no variant-level prices and no created/updated dates. Collection and product URL scoping is Shopify-only.
- **Removed-product detection** needs a complete crawl. If a run is capped by `maxProductsPerStore` or by your budget, no product is reported as removed.
- Very large catalogs are paginated 250 products at a time with a polite delay. A 10,000-product store takes about 40 requests, which is roughly 20–30 seconds.

### FAQ

**Is it legal to scrape Shopify stores?**
The scraper only reads public, unauthenticated catalog data that the store itself publishes as JSON, at a polite rate. You are responsible for how you use the data. Do not republish copyrighted descriptions or images without permission, and check the store's terms and your local law.

**Do I need a Shopify API key or the store owner's permission to access `products.json`?**
No. `products.json` is a public endpoint on every Shopify storefront. The Shopify Admin API (with inventory levels, orders and costs) is only available to the store owner and is not used.

**How do I know whether a site is a Shopify store?**
Just paste it. The scraper detects Shopify automatically and returns a `NOT_SHOPIFY` error record (free) for other sites. With `includeWooCommerce` on, it also scrapes WooCommerce stores.

**How do I monitor competitor prices on Shopify?**
Enable `trackChanges` and `onlyChanges`, then schedule the Actor. Each run outputs only new, removed, price-changed and availability-changed products, with the previous price and stock.

**Can I get inventory or stock levels?**
Yes, where the store exposes them. Enable `includeInventory` (or pass product URLs) to read `inventory_quantity` per variant from `/products/<handle>.js`. `available` and `availableVariantCount` are always included.

**How many products can I scrape?**
Up to 100,000 per store per run. Use `maxProductsPerStore` to keep test runs small and cheap.

**Why do I see records with `recordType: "error"`?**
Each input URL that could not be scraped (not Shopify, password page, missing collection or product, rate limit) produces one free error record. This way you always know why a store returned no products. The run itself still succeeds.

**Does it use proxies or a browser?**
No. It makes plain HTTPS JSON requests, one at a time per store, and backs off on HTTP 429 using `Retry-After`. That keeps it fast, cheap and reliable.

# Actor input Schema

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

Shopify store home pages, e.g. https://www.allbirds.com or allbirds.com. The whole public catalog is scraped (up to the per-store limit). Collection and product URLs pasted here are detected automatically.

## `collectionUrls` (type: `array`):

Scrape only these collections, e.g. https://www.allbirds.com/collections/mens. Uses /collections/<handle>/products.json.

## `productUrls` (type: `array`):

Scrape only these products, e.g. https://www.allbirds.com/products/mens-tree-runners. Uses /products/<handle>.js, which also returns inventory quantity when the store exposes it.

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

Stop after this many products per store. Each delivered product is one billable event.

## `includeVariants` (type: `boolean`):

Add the variants\[] array (id, SKU, title, options, price, compare-at price, availability, inventory quantity, barcode). Price min/max and availability counts are always included.

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

Fetch /products/<handle>.js for every product to read variant inventory_quantity where the store exposes it. One extra request per product, so runs take longer. Same price per product.

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

Add the product description as plain text (HTML stripped).

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

Compare with the previous run (state kept in a named key-value store) and mark every product as new, price_changed, availability_changed or unchanged. Products that disappeared are emitted as removed (free). Schedule the Actor to get a price & stock monitor.

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

With change tracking on, skip unchanged products (they are not charged). The first run outputs every product as new.

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

Named key-value store that keeps the previous snapshot. Use different names for independent monitors. Lowercase letters, digits and dashes.

## `analyzeStore` (type: `boolean`):

Add one store profile record: theme, currency, country, contact page, emails, social links, detected apps (Klaviyo, Judge.me, Recharge…), product and collection counts.

## `includeWooCommerce` (type: `boolean`):

If a URL is not Shopify, try the public WooCommerce Store API (/wp-json/wc/store/v1/products).

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

Polite pacing per store host. 429 responses are retried with Retry-After / exponential backoff.

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

How many different stores are scraped at the same time. Requests to a single store are always sequential.

## Actor input object example

```json
{
  "storeUrls": [
    "https://www.allbirds.com",
    "https://kith.com"
  ],
  "maxProductsPerStore": 50,
  "includeVariants": true,
  "includeInventory": false,
  "includeDescription": false,
  "trackChanges": false,
  "onlyChanges": false,
  "stateStoreName": "shopify-store-scraper-state",
  "analyzeStore": false,
  "includeWooCommerce": true,
  "requestDelayMs": 500,
  "maxConcurrency": 3
}
```

# Actor output Schema

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

Flat product records (also store profiles and error records, see recordType).

## `changes` (type: `string`):

No description

## `allItems` (type: `string`):

No description

## `runSummary` (type: `string`):

Per-store status, counts, change totals, billing and HTTP stats.

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

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

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

```

## MCP server setup

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