# Shopify Product & Price Scraper (`eiv/shopify-product-scraper`) Actor

Scrape full product catalogues from any Shopify store: variant-level prices, compare-at discounts, SKUs and live stock status. Bulk multi-store, one row per product or per SKU, with a per-store pricing summary. Built for daily price monitoring. From $0.40 / 1,000 products.

- **URL**: https://apify.com/eiv/shopify-product-scraper.md
- **Developed by:** [Eimantas V](https://apify.com/eiv) (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 $0.40 / 1,000 product scrapes

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/platform/actors/running/actors-in-store#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 Product & Price Scraper

Point it at any Shopify store. Get the **full catalogue with variant-level prices, discounts, SKUs and live stock** — in about a second per store, no browser.

```
allbirds.com   291 products · typical price 66.00 · 53% discounted, deepest 75% off
               163 fully out of stock
grind.co.uk    105 products · typical price 28.95 · 31 hidden from the storefront
hooksneedles   897 products · typical price 24.97 · 26 added in the last 30 days
```

It reads each store's own **public catalogue endpoint** — the one Shopify exposes so themes and apps can list products. No key, no login, and nothing that breaks when a theme changes.

***

### What you get

**Per product**

| Group | Fields |
|---|---|
| **Pricing** | `minPrice`, `maxPrice`, `isOnSale`, `maxDiscountPercent` |
| **Stock** | `stockStatus` (in-stock / partial / out-of-stock), `variantCount`, `availableVariantCount` |
| **Variants** | Per SKU: `price`, `compareAtPrice`, `discountPercent`, `available`, `sku`, `grams`, `option1-3` |
| **Catalogue** | `title`, `handle`, `productUrl`, `vendor`, `productType`, `tags`, `imageCount`, `featuredImage` |
| **Timing** | `createdAt`, `publishedAt`, `daysSinceCreated`, `isNewArrival` |
| **Visibility** | `isHidden` — tagged to stay off the storefront |

**Per store** — product count, price range, **median and mean price**, discount rate, out-of-stock count, new arrivals, hidden and zero-priced counts, top vendors and product types, and plain-English highlights.

#### Two row shapes

`granularity: "product"` gives one row per product with variants nested. **`granularity: "variant"` gives one row per SKU** — which is what price and stock monitoring actually needs, because a sold-out size or a discounted colour is invisible in a product row carrying only a min and max.

Billing counts **products** either way, so the row shape is a free choice.

***

### Who this is for

- **Competitor price monitoring** — run daily at variant granularity and diff on `productId` + `variant.sku`. Price moves, new discounts and stock-outs fall straight out of the diff.
- **Assortment and market research** — compare catalogue size, average price and discount depth across a set of stores in one run.
- **Dropshipping and sourcing** — filter by price band, vendor and product type across many suppliers at once.
- **Sales prospecting** — catalogue size and discount rate say a lot about a store's maturity. Pair with a tech-stack Actor: find prospects running Shopify, then scrape what they sell.

***

### Input

```json
{
  "stores": ["allbirds.com", "grind.co.uk"],
  "granularity": "variant",
  "onSaleOnly": true,
  "maxProductsPerStore": 1000
}
```

| Option | Default | Notes |
|---|---|---|
| `granularity` | `product` | `variant` for one row per SKU |
| `maxProductsPerStore` | `1000` | Your billing guard on large catalogues |
| `onSaleOnly` / `inStockOnly` | `false` | Applied **before** billing |
| `excludeHidden` | `false` | Drop storefront-hidden products |
| `minPrice` / `maxPrice` | — | In the store's own currency |
| `titleKeywords` / `vendors` / `productTypes` | — | |
| `addedWithinDays` | — | New-arrivals feed; see the caveat below |
| `includeDescriptions` | `false` | Off keeps the dataset far smaller |
| `includeCollections` | `false` | One extra request per store |

***

### Four things worth knowing

These are all things that produced visibly wrong numbers before they were handled, found by running the Actor against real stores.

**`updated_at` is not emitted, because Shopify does not mean by it what you would expect.** Every product in a catalogue response carries an identical `updated_at` equal to the moment of the request — verified across five unrelated stores, one distinct value each. Passing it through would make every product look modified on every run and quietly break exactly the change detection people build with it. Diff on `price` and `available` instead; those are real.

**Compare stores on `medianPrice`, not `averagePrice`.** Merchants park internal products at sentinel prices. One asset placeholder listed at 999,999 moved a quilting shop's mean from 31.05 to 1,068.36 — enough to rank it above a jeweller. Both figures are reported; the median is the one that a single absurd row cannot move.

**A large slice of most catalogues is hidden from the storefront.** Products tagged `hidden` or `hidden_product` — influencer exclusives, deal-site bundles, spare parts — sit in the feed where no shopper can browse to them. That was 30% of one store and 39% of another. Every record carries `isHidden`; set `excludeHidden: true` to drop them.

**`published_at` is not a reliable age signal.** Some stores republish their whole catalogue continuously — Gymshark's entire 2023-onwards range carries a publish date inside the last fortnight. Keying "new arrival" off it reported a three-year catalogue as 100% new. This Actor derives `isNewArrival` and `addedWithinDays` from **`created_at`** instead, and still reports `publishedAt` if you want it.

**A stale `compare_at_price` is not a discount.** Shopify leaves the field populated after a sale ends, so treating "field is set" as discounted invents permanent fake sales. A product only counts as on sale when a variant's compare-at price is genuinely *above* its current price.

Zero-priced products — free gifts, empty tins, bundle placeholders — are returned in full but kept out of the store's price statistics, and counted separately as `freeProducts`. They are not the cheapest thing a store sells.

***

### Output

```json
{
  "storeDomain": "allbirds.com",
  "title": "Wool Runner",
  "productUrl": "https://www.allbirds.com/products/wool-runner",
  "productType": "Shoes", "vendor": "Allbirds",
  "minPrice": 75, "maxPrice": 75,
  "isOnSale": true, "maxDiscountPercent": 25,
  "stockStatus": "partial", "variantCount": 2, "availableVariantCount": 1,
  "variants": [
    { "sku": "WR-9", "price": 75, "compareAtPrice": 100, "discountPercent": 25, "available": true }
  ],
  "createdAt": "2026-07-03T00:00:00.000Z", "daysSinceCreated": 40, "isNewArrival": false,
  "isHidden": false, "recordType": "product"
}
```

Three ready-made dataset views: **Products**, **Price monitor (SKU level)** and **Store comparison**. Set `flattenOutput: true` for CSV — at variant granularity the flat shape is already close to a price table.

***

### Honest limits

- **Some stores block the endpoint.** Headless storefronts and stores behind aggressive bot protection return 403. You get `HTTP_403` with an explanation and **are not charged**. Most Shopify stores serve it fine; note that a store may block `www.` while serving the apex, and the Actor already prefers whichever your input resolves to.
- **Non-Shopify domains** return `NOT_SHOPIFY` rather than an empty success.
- **No inventory quantities.** Shopify exposes `available` as a boolean, not a stock level. Anything claiming exact units for an arbitrary store is guessing.
- **Prices are in the store's own currency**, which the catalogue endpoint does not state. Compare within a store, not across countries, unless you know the currency.
- **Prices come as the store lists them**, sentinels included. `maxPrice` on a store summary can be a placeholder rather than a real product; that is why `medianPrice` exists.
- **`productsScanned` is a lower bound** when `truncated` is true — the pages past your cap were never fetched, so it is not the catalogue total.

***

### Pricing

| Event | Price | When |
|---|---|---|
| Actor start | $0.005 | Once per run |
| Store scraped | $0.01 | Per store whose catalogue was read |
| Product scraped | $0.0004 | Per distinct product returned, after filters |

**$0.40 per 1,000 products.** A 100-store run at 500 products each costs **$21**. Filters run before billing, and stores that block or aren't Shopify are never charged.

***

### Tips

- **Schedule daily, diff on `productId` + `variant.sku`.** New SKUs are launches, disappeared ones are discontinuations, and a changed `price` is the event you're watching for.
- **`onSaleOnly: true` is the cheapest promo tracker** — you pay only for products actually discounted that day.
- **`addedWithinDays: 7` at variant granularity** is a clean new-launch feed across a whole competitive set.
- **`excludeHidden: true` if you are benchmarking assortment size.** Counting products no shopper can reach overstates a competitor's range by a third on some stores.
- **Watch `discountRate` per store over time.** A rising discount rate with a rising out-of-stock count usually means end-of-season clearance.

# Actor input Schema

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

Store domains or URLs, one per line — 'allbirds.com', 'https://www.grind.co.uk'. Any Shopify storefront works; the Actor reads the store's own public catalogue endpoint.

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

Alternative to Stores, for feeding output from another Actor straight into this one.

## `sourceDatasetId` (type: `string`):

Read store domains from an existing dataset. Pairs with a tech-stack Actor: detect which of your prospects run Shopify, then scrape their catalogues.

## `sourceDatasetField` (type: `string`):

Which field on the source dataset holds the store domain.

## `granularity` (type: `string`):

'product' gives one row per product with its variants nested. 'variant' gives one row per purchasable SKU, which is what price and stock monitoring needs — a sold-out size or a discounted colour is invisible in a product row that only carries a min and max. Billing counts products either way.

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

Hard cap per store. Large catalogues run to many thousands of products, so this protects you from an unexpected bill. Records flag truncated:true when a catalogue exceeded the cap.

## `includeDescriptions` (type: `boolean`):

Store each product's full description body. Off by default because descriptions dominate dataset size; a 300-character plain-text snippet is always included. Note this does not reduce bandwidth — Shopify returns descriptions in the catalogue payload either way.

## `includeCollections` (type: `boolean`):

Also fetch the store's public collection list and attach the names to the store summary. One extra request per store.

## `includeStoreSummary` (type: `boolean`):

Add one rollup record per store (recordType: store-summary) with price range, average price, discount rate, out-of-stock count, top vendors and product types. Not billed as a product.

## `onSaleOnly` (type: `boolean`):

Keep only products where at least one variant is priced below its compare-at price.

## `inStockOnly` (type: `boolean`):

Keep only products with at least one purchasable variant.

## `excludeHidden` (type: `boolean`):

Drop products the store tags 'hidden' or 'hidden\_product' to keep them off its storefront — influencer exclusives, deal-site bundles, spare parts. They sit in the catalogue feed but no shopper can browse to them, and they can be a third of what a store returns. Every record carries isHidden either way.

## `minPrice` (type: `integer`):

Keep products whose highest variant price is at least this. In the store's own currency. 0 disables.

## `maxPrice` (type: `integer`):

Keep products whose lowest variant price is at most this. In the store's own currency. 0 disables.

## `titleKeywords` (type: `array`):

Keep only products whose title contains any of these (case-insensitive).

## `vendors` (type: `array`):

Keep only products from these vendors (exact, case-insensitive).

## `productTypes` (type: `array`):

Keep only these Shopify product types (exact, case-insensitive).

## `addedWithinDays` (type: `integer`):

Keep only products created in the last N days — the new-arrivals feed. Uses created\_at rather than published\_at, because stores that republish their catalogue would otherwise report every product as new. 0 keeps everything.

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

Stores processed in parallel. Catalogue pages can be several megabytes, so raise memory alongside this.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout. Large catalogue pages are slow to serialise, so this is generous by default.

## `maxRetries` (type: `integer`):

Retries for connection resets and 5xx responses. A 403 is not retried, because a store that blocks its catalogue endpoint keeps blocking it.

## `flattenOutput` (type: `boolean`):

Emit flat dot-notation columns with arrays joined by ' | '. Use for spreadsheets; leave off for warehouses. At variant granularity the flat shape is already close to a price table.

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

Usually unnecessary — the catalogue endpoint is public and unauthenticated. Worth enabling only if you are scraping many stores behind the same CDN and start seeing 403s.

## Actor input object example

```json
{
  "stores": [
    "allbirds.com",
    "grind.co.uk",
    "bruvi.com"
  ],
  "sourceDatasetField": "domain",
  "granularity": "product",
  "maxProductsPerStore": 1000,
  "includeDescriptions": false,
  "includeCollections": false,
  "includeStoreSummary": true,
  "onSaleOnly": false,
  "inStockOnly": false,
  "excludeHidden": false,
  "minPrice": 0,
  "maxPrice": 0,
  "addedWithinDays": 0,
  "maxConcurrency": 10,
  "requestTimeoutSecs": 60,
  "maxRetries": 1,
  "flattenOutput": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

One dataset item per product (or per variant at variant granularity). Store rollups carry recordType 'store-summary'.

# 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",
        "grind.co.uk",
        "bruvi.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("eiv/shopify-product-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": [
        "allbirds.com",
        "grind.co.uk",
        "bruvi.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("eiv/shopify-product-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": [
    "allbirds.com",
    "grind.co.uk",
    "bruvi.com"
  ]
}' |
apify call eiv/shopify-product-scraper --silent --output-dataset

```

## MCP server setup

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