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

Paste any Shopify store URL, custom domains included, and get every product: title, vendor, type, tags, min and max price, compare-at price and sale flag, stock flag, each variant with SKU, options and price, images and the description as plain text. Many stores per run. No API key.

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

## Pricing

from $0.90 / 1,000 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?

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

This Shopify scraper downloads every product from any Shopify store, including stores on their own domain, through the store's public `products.json` feed. Each product comes with its price range, compare-at price and sale flag, stock flag, every variant with SKU, options and price, all image URLs and the description as plain text. Give it one store or a hundred in the same run. No Shopify account, app or API key needed.

- **Any Shopify store.** A domain like `kith.com`, a full URL, a custom domain or a `.myshopify.com` address all work.
- **Headless stores too.** Some big brands run a custom front end on top of Shopify, so `products.json` on their main domain answers 403 or 404. The Actor finds the store's `.myshopify.com` address on the home page and reads the catalogue from there. In our tests this worked for Gymshark and Fashion Nova.
- **Whole catalogue or chosen collections.** Leave collections empty for everything, or give handles like `sale` or a collection URL to scrape only those.
- **One row per product or per variant.** Per variant gives you one spreadsheet row per size and colour with its own SKU, price and stock flag.
- **Sites that are not Shopify do not break the run.** They are skipped, cost nothing, and are listed with the reason in the `SUMMARY` record.

### What data you get

| Field | What it holds |
|---|---|
| `storeDomain`, `storeName`, `currency` | The store, its name and the currency its prices are in |
| `productId`, `title`, `handle`, `url` | The product and its page |
| `vendor`, `productType`, `tags` | Brand, product type and every tag the store set |
| `createdAt`, `publishedAt`, `updatedAt` | When the product was created, last published and last changed |
| `priceMin`, `priceMax` | Lowest and highest variant price |
| `compareAtPrice`, `discountPercent`, `onSale` | The original price when something is discounted, the largest discount in percent, and a true/false sale flag |
| `available` | True when at least one variant can be bought |
| `variants` | Each variant: `variantId`, `title`, `sku`, `options` (for example `{"Size": "M"}`), `price`, `compareAtPrice`, `available`, `grams`, `imageUrl`, `url` |
| `options` | Option names and all their values |
| `imageUrl`, `images` | Main image and every image URL |
| `description` | Product description as plain text |
| `collection` | The collection handle, when you scraped collections |

With **One row per variant** on, the `variants` list is replaced by `variantId`, `variantTitle`, `sku`, `variantOptions`, `variantPrice`, `variantCompareAtPrice`, `variantOnSale`, `variantAvailable`, `variantGrams`, `variantImageUrl` and `variantUrl` on each row, next to the product fields.

### How to use it

1. Add one or more store URLs.
2. Set **Max products per store**. Set it to 100000 for the whole catalogue.
3. Optional: add **Collection handles**, or turn on **One row per variant**.
4. Run it and export to JSON, CSV or Excel, or call it from the API, Make, Zapier, n8n or an AI agent through the Apify MCP server.

A run with 20 products from each of two stores takes under 10 seconds on Apify. Large catalogues come 250 products per request, so 10,000 products is about 40 requests.

#### Example input

```json
{
  "storeUrls": ["https://kith.com", "allbirds.com"],
  "maxProductsPerStore": 20,
  "outputPerVariant": false
}
```

#### Example output (one product, variants and tags shortened)

```json
{
  "storeDomain": "allbirds.com",
  "storeName": "Allbirds",
  "currency": "USD",
  "productId": 6858599596112,
  "title": "Women's Tree Dasher Relay - Blizzard/Hazy Cobalt (Clarity Blue Sole)",
  "handle": "womens-tree-dasher-relay-blizzard-hazy-cobalt",
  "url": "https://allbirds.com/products/womens-tree-dasher-relay-blizzard-hazy-cobalt",
  "vendor": "Allbirds",
  "productType": "Shoes",
  "tags": ["allbirds::edition => limited", "allbirds::material => tree", "FINAL SALE", "shoprunner"],
  "createdAt": "2023-07-15T21:42:13-07:00",
  "publishedAt": "2026-09-25T16:49:33-07:00",
  "updatedAt": "2026-10-29T19:19:01-07:00",
  "priceMin": 67,
  "priceMax": 67,
  "compareAtPrice": 135,
  "discountPercent": 50,
  "onSale": true,
  "available": false,
  "variantCount": 13,
  "options": [{ "name": "Size", "values": ["5", "5.5", "6", "6.5", "7", "7.5", "8", "8.5", "9", "9.5", "10", "10.5", "11"] }],
  "variants": [
    {
      "variantId": 40234510123088,
      "title": "5",
      "sku": "A10692W050",
      "options": { "Size": "5" },
      "price": 67,
      "compareAtPrice": 135,
      "available": false,
      "grams": 638,
      "requiresShipping": true,
      "imageUrl": null,
      "url": "https://allbirds.com/products/womens-tree-dasher-relay-blizzard-hazy-cobalt?variant=40234510123088"
    }
  ],
  "imageUrl": "https://cdn.shopify.com/s/files/1/1104/4168/files/A10691_24Q1_Tree-Dasher-Relay-Blizzard-Hazy-Cobalt-Clarity-Blue_PDP_LEFT.png?v=1776183508",
  "images": ["https://cdn.shopify.com/s/files/1/1104/4168/files/A10691_24Q1_Tree-Dasher-Relay-Blizzard-Hazy-Cobalt-Clarity-Blue_PDP_LEFT.png?v=1776183508"],
  "description": "Made from leftover stock yarns, each pair features slight variations in color for a one-of-a-kind design. ...",
  "collection": null,
  "source": "allbirds.com",
  "scrapedAt": "2026-10-30T02:19:02.962Z"
}
```

#### Example variant row (One row per variant on, shortened)

```json
{
  "storeDomain": "www.gymshark.com",
  "storeName": "Gymshark US",
  "productId": 6806880518346,
  "title": "Gymshark Power Washed Track Pants Short - Pitch Grey/Wash",
  "priceMin": 56,
  "priceMax": 56,
  "variantId": 39799176954058,
  "variantTitle": "Large",
  "sku": "B7B7S-GFPV-L",
  "variantOptions": { "Size": "Large" },
  "variantPrice": 56,
  "variantCompareAtPrice": null,
  "variantOnSale": false,
  "variantAvailable": true,
  "variantUrl": "https://www.gymshark.com/products/gymshark-power-washed-track-pants-short-pants-grey-aw26?variant=39799176954058"
}
```

### How much does it cost?

You pay per item saved: $0.0009 per product, plus $0.0005 once per run. 1,000 products cost about $0.90 and 10,000 cost about $9.00. Apify's free plan credit of $5 a month covers about 5,500 products.

With **One row per variant** on, each variant row is one item. A store where products have 6 sizes on average gives 6 rows per product, so check the variant count before you turn it on for a big catalogue.

Stores that fail (not on Shopify, `products.json` switched off, a wrong collection handle, a domain that does not exist) cost nothing. They are listed with the reason in the `SUMMARY` record of the run's key-value store.

### Use cases

- **Competitor price tracking:** run it daily on competitor stores and compare `priceMin`, `compareAtPrice` and `onSale` over time.
- **Stock monitoring:** watch `available` and each variant's `available` flag to see what sells out and when it comes back.
- **Catalogue research:** see a brand's full range, vendors, product types and tags, and how often they add products (`createdAt`).
- **Dropshipping and wholesale research:** collect product titles, images, SKUs and prices from many stores in one run.
- **Product feeds and AI datasets:** plain text descriptions and image URLs ready for search, comparison sites or RAG.

### FAQ

**How do I know if a site runs on Shopify?**
You don't need to. Give the URL, and if it is not a Shopify store it is skipped with the reason "not a Shopify store" in `SUMMARY`. Other stores in the same run keep going.

**Why did a Shopify store fail with "products.json is disabled or blocked"?**
A few stores switch the public feed off or block it with a firewall, and some headless stores hide their `.myshopify.com` address. Try the **RESIDENTIAL** proxy group first. If it still fails, the store does not publish its catalogue this way.

**Is there a limit on catalogue size?**
Shopify serves at most 25,000 products from one `products.json` feed. When a store has more, the Actor goes through every collection after that and adds the products it has not seen yet. Products that are in no collection at all can't be reached past that point.

**Do I get exact stock counts?**
No. Shopify's public feed only says whether each variant can be bought (`available`), not how many are left.

**Are prices in my currency?**
Prices are as the store's feed gives them, and `currency` is the store's main currency from its public `meta.json`. The Actor does not pick a shopping country, so country-specific prices are not covered.

**Do I need a proxy?**
Apify's datacenter proxy is on by default and was enough for every store in our tests. If a store keeps answering 403 or 429 in the log, switch to the RESIDENTIAL group. The Actor retries every failed request up to five times with a new IP.

**Is this legal?**
It reads the product feed that Shopify stores publish to every visitor. You are responsible for how you use the data.

### Other Actors by this developer

- [Website Tech Stack Detector](https://apify.com/fernandosord/website-tech-stack-detector): find out which sites run on Shopify and what else they use.
- [Website Change Monitor](https://apify.com/fernandosord/website-change-monitor): get told when a page changes, such as a pricing page.
- [Domain Profile: WHOIS and DNS](https://apify.com/fernandosord/domain-profile-whois-dns): owner, registrar, age and DNS records for any domain.
- [Google Sheets Export](https://apify.com/fernandosord/google-sheets-export): send any dataset straight to a Google Sheet.
- [Website Screenshot and PDF](https://apify.com/fernandosord/website-screenshot-pdf): full-page screenshots of product pages.

# Actor input Schema

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

One or more Shopify stores: a domain (kith.com), a full URL (https://www.allbirds.com), a custom domain or a .myshopify.com address. A collection URL (https://kith.com/collections/footwear) scrapes only that collection.

## `collectionHandles` (type: `array`):

Optional. Only scrape these collections, on every store in the list. Use the handle from the collection URL, for example sale for /collections/sale. Leave empty for the whole catalogue.

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

The Actor stops at this many products for each store. Set it high (for example 100000) to get the whole catalogue. Variants of a product never count against this number.

## `outputPerVariant` (type: `boolean`):

Off: one item per product with its variants in a list. On: one item per variant (size, colour and so on) with the product fields repeated, which is easier to use in a spreadsheet. Each row is a dataset item and is billed as one.

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

Apify datacenter proxy is on by default and works for most stores. If a store keeps answering 403 or 429, choose the RESIDENTIAL group.

## Actor input object example

```json
{
  "storeUrls": [
    "https://kith.com",
    "allbirds.com"
  ],
  "maxProductsPerStore": 20,
  "outputPerVariant": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

One item per product, or one per variant when that option is on.

# 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://kith.com",
        "allbirds.com"
    ],
    "maxProductsPerStore": 20,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("fernandosord/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 = {
    "storeUrls": [
        "https://kith.com",
        "allbirds.com",
    ],
    "maxProductsPerStore": 20,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("fernandosord/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 '{
  "storeUrls": [
    "https://kith.com",
    "allbirds.com"
  ],
  "maxProductsPerStore": 20,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call fernandosord/shopify-products-scraper --silent --output-dataset

```

## MCP server setup

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