# Shopify Products Scraper: Prices, Variants and Stock (`variegated_binturong/shopify-products-scraper`) Actor

Get every product variant of a list of Shopify stores from their public products.json: price, compare-at price and discount, availability, SKU, vendor, type, tags, image, link and dates, plus a price summary per store. Stores that are not Shopify or are closed get one clear error row.

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

## Pricing

$1.00 / 1,000 product rows

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 Products Scraper: Prices, Variants and Stock

Paste a list of Shopify stores and get every product variant they sell, one row per variant: product, variant, type, brand, tags, price, compare-at (struck-through) price and discount, availability, SKU, image, link and dates. Each store also gets a summary: how many products, minimum, average and maximum price, and how many are on sale.

The data comes from `/products.json`, the public product feed every Shopify storefront exposes. No login, no cookies, no browser, nothing bypassed: a store that is not on Shopify, is password protected or blocks the feed gets one clear error row.

### What it is for

- **Competitor price monitoring**: schedule it daily and track prices, discounts and stock of the stores you compete with.
- **Brands and distributors**: check how resellers price your products (compare-at prices, discounts) and which variants they list.
- **Product and market research**: full catalogues with types, vendors and tags, to see assortments and price ranges across a niche.
- **Agencies and B2B sales to e-commerce**: size up a list of prospect stores (catalogue size, price range, share on sale) before you pitch.
- **Catalogue exports**: a clean table of your own store, with links and images, for audits and feeds.

### Input

| Field | What it does | Default |
|---|---|---|
| `stores` | One store per line: a domain or any URL of the store. A `/collections/<name>` URL reads only that collection | required |
| `keywords` | Keep variants with at least one of these words in the title, variant, type, vendor, tags or SKU | all products |
| `onlyAvailable` | Skip variants that cannot be bought | false |
| `onlyOnSale` | Keep only variants whose compare-at price is higher than the price | false |
| `oneRowPerProduct` | One row per product (cheapest variant, plus `variantsCount` and `priceMax`) | false |
| `includeDescription` | Add the product description as plain text | false |
| `maxProductsPerStore` | Stop reading a store after this many products | 10000 |
| `includeErrorRows` | Add one row for each store that cannot be read | true |
| `concurrency`, `timeoutSecs`, `maxRetries` | Speed and resilience | 3, 30, 3 |
| `maxStores`, `maxResults` | Safety caps (0: no limit on rows) | 1000, 0 |

Example:

```json
{
  "stores": ["shop.example.com", "https://another-store.example/collections/sale"],
  "onlyAvailable": true,
  "keywords": ["linen", "summer"]
}
```

### Output

One row per variant:

```json
{
  "type": "variant",
  "store": "shop.example.com",
  "storeName": "Example Shop",
  "product": "Linen Shirt",
  "variant": "S / White",
  "productType": "Shirts",
  "vendor": "Example Brand",
  "tags": ["linen", "summer", "sale"],
  "price": 75,
  "compareAtPrice": 100,
  "discountPercent": 25,
  "onSale": true,
  "currency": "EUR",
  "available": true,
  "sku": "LS-S-W",
  "options": { "Size": "S", "Color": "White" },
  "image": "https://cdn.shopify.com/s/files/example/white.jpg",
  "url": "https://shop.example.com/products/linen-shirt?variant=11",
  "handle": "linen-shirt",
  "productId": "1",
  "variantId": "11",
  "publishedAt": "2026-10-01T08:00:00.000Z",
  "createdAt": "2026-09-01T08:00:00.000Z",
  "updatedAt": "2026-10-22T08:00:00.000Z",
  "productUpdatedAt": "2026-10-20T08:00:00.000Z",
  "description": null,
  "collection": null,
  "input": "shop.example.com",
  "error": null,
  "scrapedAt": "2026-10-27T08:00:00.000Z"
}
```

- `price` and `compareAtPrice` are numbers in the store's currency. `compareAtPrice` is filled only when it is higher than the price (the struck-through price shoppers see); `onSale` and `discountPercent` follow from it.
- `currency` and `storeName` come from the store's public `/meta.json`; they are null when the store does not publish it.
- `available` is whether the store lets shoppers buy the variant. The public feed does not give stock quantities.
- `updatedAt` is the variant's last update (the product's when the variant has none).
- Products without variants give one row with null price.

With `oneRowPerProduct`, each row adds `variantsCount` and `priceMax`, and `price`, `compareAtPrice` and `sku` are those of the cheapest variant.

A store that cannot be read gives one error row:

```json
{ "type": "error", "store": "not-a-shop.example", "input": "not-a-shop.example", "collection": null, "error": "products.json not found (HTTP 404): not a Shopify store, or the collection does not exist", "scrapedAt": "2026-10-27T08:00:00.000Z" }
```

Other errors you can see: `the store is password protected: nothing was scraped`, `access denied (HTTP 403): the store blocks its public product feed, nothing was bypassed`, `not a Shopify store: /products.json returned an HTML page`, `rate limited by the store (HTTP 429)`, `request failed: timeout`.

#### Summary per store

The `SUMMARY` record in the key-value store has run totals and one entry per store:

```json
{
  "store": "shop.example.com",
  "storeName": "Example Shop",
  "currency": "EUR",
  "collection": null,
  "pages": 2,
  "productsScanned": 312,
  "products": 312,
  "rows": 1048,
  "availableRows": 951,
  "onSaleRows": 120,
  "onSaleProducts": 37,
  "minPrice": 9.9,
  "avgPrice": 64.37,
  "maxPrice": 289,
  "error": null,
  "stoppedEarly": null
}
```

`products`, prices and on-sale counts refer to the rows delivered, so they follow your filters. `productsScanned` counts every product read.

### Pricing

Pay per result: **$0.001 per row delivered** to the dataset ($1 per 1,000 rows), plus the small Apify platform usage of the run. Variants removed by your filters are not delivered and not charged. Use `oneRowPerProduct` for one row per product. Each error row counts as one result: turn off `includeErrorRows` to get errors only in the summary.

### Notes

- Only public data: the `/products.json` and `/meta.json` endpoints of the storefront. No login, no cookies, no proxies, no CAPTCHA solving. Password-protected or blocked stores are reported, not bypassed.
- No personal data: rows contain product data only. From `/meta.json` only the store name and currency are kept.
- Pages of one store are read one after the other with a short pause, 250 products per page, and Retry-After is honoured, to stay gentle with each store.
- Some stores turn the public feed off or limit how many pages it returns: the summary tells you when a store stopped early.
- Not affiliated with Shopify.

# Actor input Schema

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

One store per line: a domain (shop.example.com) or any URL of the store. A collection URL (.../collections/sale) limits the run to that collection.

## `keywords` (type: `array`):

Keep only variants with at least one of these words in the product title, variant, type, vendor, tags or SKU (whole words; case and accents are ignored). Empty: every product.

## `onlyAvailable` (type: `boolean`):

Skip variants the store marks as not available.

## `onlyOnSale` (type: `boolean`):

Keep only variants with a compare-at price higher than the price.

## `oneRowPerProduct` (type: `boolean`):

One row per product instead of one per variant: price, compare-at price and SKU of the cheapest variant, plus variantsCount and priceMax. Fewer rows, lower cost.

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

Add the description as plain text (up to 5,000 characters).

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

Stop reading a store after this many products.

## `includeErrorRows` (type: `boolean`):

One row with a plain error (not a Shopify store, password protected, blocked...). Each error row counts as one result. Off: errors are listed only in the run summary.

## `concurrency` (type: `integer`):

How many stores are read at the same time. Pages of one store are read one after the other.

## `timeoutSecs` (type: `integer`):

Give up on a request after this many seconds.

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

How many times a request is retried after a timeout, a 429 or a 5xx response. Retry-After is honoured.

## `maxStores` (type: `integer`):

Safety cap on the number of input lines processed.

## `maxResults` (type: `integer`):

Stop after this many dataset rows. 0: no limit.

## Actor input object example

```json
{
  "stores": [
    "https://www.allbirds.com"
  ],
  "keywords": [],
  "onlyAvailable": false,
  "onlyOnSale": false,
  "oneRowPerProduct": false,
  "includeDescription": false,
  "maxProductsPerStore": 100,
  "includeErrorRows": true,
  "concurrency": 3,
  "timeoutSecs": 30,
  "maxRetries": 3,
  "maxStores": 1000,
  "maxResults": 0
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

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

No description

## `summary` (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.allbirds.com"
    ],
    "maxProductsPerStore": 100
};

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

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

```

## MCP server setup

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