# Shopify Variant Scraper: Price, SKU & Stock (`axiorasolutions/shopify-variant-scraper`) Actor

Scrape any public Shopify store into one row per product variant: SKU, variant title, options, price, compare-at price, discount percent, availability, barcode, weight, images, vendor, product type, tags and description. Batch many stores, filter by collection, vendor, price or sale status.

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

## Pricing

from $0.44 / 1,000 product variants

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Scraper — Product Variants, Prices, SKUs & Stock

Shopify scraper that turns any public store into **one row per product variant** — its own SKU, options, price, compare-at price, discount and availability — because that is the level real pricing and inventory work happens at. It reads the store's **own public product feed**, so there is **no API key, no app install and no store password** needed. Leave the prefilled `allbirds.com` and `kith.com` in place and click **Start** to see it work.

### What you get

- 🧬 **One row per product variant** — `sku`, `barcode`, `option1/2/3`, `optionNames`, `variantTitle` and `position`, so a shoe with eight sizes in three colours becomes 24 rows.
- 💱 **Pricing fields you can act on** — `price`, `compareAtPrice`, `isOnSale`, `discountPercent`, `productMinPrice` and `productMaxPrice`, all computed from the merchant's own data.
- 📦 **`available`** — the true purchasable boolean from the feed, with no invented inventory quantities.
- 🏷️ **Product context on every row** — `productTitle`, `productHandle`, `productUrl`, `vendor`, `productType`, `tags`, `publishedAt` and `ageDays`.
- 🔁 **Change detection built in** — a stable `variantUid` plus a `productHash` covering title, price, compare-at price and availability.
- 🛟 **Honest errors, not blank runs** — a blocked or non-Shopify store becomes an `ok: false` row with a specific reason.

### Quick start

1. Add one or more store domains to **Shopify stores** — a bare domain like `allbirds.com`, a full URL or a `myshopify.com` address all work.
2. Optional: narrow the scope with **Collection handles** or **Product handles**, and cap the run with **Max products per store** and **Max variants for the whole run**.
3. Paste the minimal input below (or keep the prefilled stores) and click **Start**.
4. Review the **Variants**, **Pricing & discounts** and **Catalogue export** dataset tabs, then export or connect through the API.

```json
{
  "stores": ["allbirds.com"],
  "maxProductsPerStore": 50
}
```

### Example output

One dataset row — a single variant of one product:

```json
{
  "ok": true,
  "errorCode": null,
  "variantUid": "allbirds.com:4578213:31889234",
  "store": "allbirds.com",
  "storeName": "Allbirds",
  "currency": "USD",
  "collection": "mens-shoes",
  "productId": "4578213",
  "productHandle": "wool-runner-go",
  "productTitle": "Wool Runner Go",
  "productUrl": "https://allbirds.com/products/wool-runner-go",
  "productType": "Shoes",
  "vendor": "Allbirds",
  "tags": ["new", "mens"],
  "publishedAt": "2026-09-12T10:00:00.000Z",
  "ageDays": 20,
  "variantId": "31889234",
  "variantTitle": "US 10 / Natural Black",
  "sku": "WRG-10-NB",
  "barcode": "0190000123456",
  "option1": "US 10",
  "option2": "Natural Black",
  "option3": null,
  "optionNames": { "Size": 1, "Color": 2 },
  "position": 7,
  "price": 125,
  "compareAtPrice": 145,
  "isOnSale": true,
  "discountPercent": 13.8,
  "available": true,
  "requiresShipping": true,
  "taxable": true,
  "grams": 600,
  "featuredImageUrl": "https://cdn.shopify.com/s/files/1/0001/wrg.jpg",
  "imageCount": 6,
  "productMinPrice": 125,
  "productMaxPrice": 145,
  "variantCount": 24,
  "productHash": "ba71f4c2e90d5538",
  "scrapedAt": "2026-10-02T12:00:00.000Z"
}
```

### What this Shopify scraper returns

- 🧬 **Variant-level rows** — `sku`, `barcode`, `option1/2/3`, `price`, `compareAtPrice`, `discountPercent`, `available`, `grams`, `weight`, variant image and position. Plus `optionNames`, so you always know whether `option1` means Size or Colour **for that specific product**.
- 🏷️ **Discount maths done for you** — `isOnSale` and `discountPercent` are computed from the merchant's own compare-at price, not estimated.
- 💱 **Real currency, or honest null** — Shopify's public product feed does not include a currency, so the Actor reads it from the storefront. If the store does not expose it, `currency` is `null`. **Prices are never relabelled with a guessed currency.**
- 📦 **No invented stock numbers** — Shopify's public feed publishes a purchasable boolean and nothing else. You get `available`. No fabricated `inventory_quantity`.
- 🔀 **Three ways in** — whole catalogue, specific **collection handles**, or an exact list of **product handles** when you track a known SKU set.
- 🎯 **Filters that cut your bill** — in-stock only, on-sale only, price range, vendor, product type, tags and `publishedAfter` all run before rows are written, so filtered variants are never charged.
- 🔁 **Change detection** — `variantUid` is stable and `productHash` covers title, price, compare-at price and availability. Diff two scheduled runs and you have a price-and-stockout feed.
- 🛟 **One blocked store never fails the run** — a password-protected store, a non-Shopify site or a 403 becomes an `ok: false` row with a specific reason.

Running on Apify adds scheduling, webhooks, monitoring, API and SDK access, and one-click export to JSON, CSV, Excel, Google Sheets and 20+ integrations.

### How to use it

1. Add store domains to **Shopify stores**.
2. Leave **Collection handles** and **Product handles** empty for the full catalogue, or fill one of them to narrow the scope.
3. Set **Max products per store** and **Max variants for the whole run**.
4. Add filters if you want a smaller, cheaper dataset — **Discounted variants only** plus a **Minimum price** is the classic competitor-promo query.
5. Click **Start**, then use the **Variants**, **Pricing & discounts** and **Catalogue export** dataset tabs.

#### Where do I find a collection or product handle?

They are the last path segment of the store URL. `https://allbirds.com/collections/mens-shoes` → handle `mens-shoes`. `https://allbirds.com/products/wool-runner-go` → handle `wool-runner-go`.

### How much does it cost

Pricing is **pay per event** with one event:

| Event | What triggers it | Billed |
|---|---|---|
| Product variant | One variant row written to the dataset | per variant |
| Actor start | Once per run, platform fee | per run |

**Billing is per variant, not per product**, and the README says so up front because it is the one thing that will surprise you: a 200-product store with an average of 12 variants is ~2,400 billed rows, not 200. Use **Max variants for the whole run** to cap it exactly, and the filters to pay only for the rows you actually want.

Variants removed by filters are **not** billed. Stores that cannot be read are **not** billed. Compute, bandwidth and storage are included; there is no separate platform-usage charge on top.

Set **Max cost per run** in the run options for a hard ceiling. Higher Apify plans get progressively lower per-variant pricing through Apify Store tier discounts.

Evaluating? Set **Max products per store** to `5` on one store.

### Example input

```json
{
  "stores": ["allbirds.com", "https://rothys.com"],
  "collections": ["mens-shoes"],
  "maxProductsPerStore": 100,
  "maxVariantsTotal": 2000,
  "includeDescription": false,
  "onSaleOnly": true,
  "minPrice": 50,
  "tags": ["new"],
  "publishedAfter": "90 days"
}
```

### When a store refuses the feed

```json
{
  "ok": false,
  "errorCode": "UNSUPPORTED_SOURCE",
  "requestedInput": "example-store.com",
  "error": {
    "code": "UNSUPPORTED_SOURCE",
    "message": "example-store.com returned 404 for its product feed. The store may be private, password-protected or on a different platform.",
    "httpStatus": 404,
    "hint": "This source or URL shape is not supported by this Actor. See the README for supported sources."
  }
}
```

### Use cases

- **Competitor price monitoring** — schedule daily, diff `productHash` per `variantUid`, alert on changes.
- **Promotion tracking** — `onSaleOnly` plus `discountPercent` tells you exactly what a competitor is discounting and by how much.
- **Stockout and assortment analysis** — `available` per variant reveals which sizes and colours actually sell out.
- **New-arrival feeds** — `publishedAfter: "7 days"` on a schedule.
- **Dropshipping and reselling** — `Catalogue export` is a flat, import-ready product feed with SKUs and barcodes.
- **Brand compliance** — scan many retailers for a `vendor` and compare advertised prices against MAP.
- **Lead qualification** — pair with the **Domain Contact Enricher** to find which prospects run Shopify, then pull their catalogue size and price band.

### Related Actors by Axiora Solutions

| Actor | Use it for |
|---|---|
| **Domain Contact Enricher** | Detect which of your prospects run Shopify, and get their contact details |
| **Sitemap & Indexability Audit** | Technical SEO health of the same storefronts |
| **News & RSS Feed Scraper** | Brand and product news for the stores you track |

### Frequently asked questions

#### Which Shopify stores work?

Any store that leaves its standard public product feed enabled, which is the default. Stores that are password-protected, that have disabled the feed, or that sit behind an aggressive bot shield will return a clear `UNSUPPORTED_SOURCE` or `ACCESS_DENIED` row rather than silently returning nothing.

#### Why is `currency` sometimes null?

Because Shopify's public product feed genuinely does not contain it. The Actor reads the currency from the storefront page instead. When a store does not expose it there either, the field stays `null` — the alternative would be labelling prices with a currency we cannot verify, and a wrong currency in a pricing dataset is worse than a missing one.

#### Can I get inventory quantities?

No, and no scraper can from the public feed. Shopify publishes a purchasable boolean (`available`) and nothing more. Any tool that shows you a public store's stock *count* is either inferring it or making it up. Track `available` over scheduled runs to detect stockouts reliably.

#### Why do I get so many rows per product?

Because pricing and stock are per variant. This is the product's core design, and the pricing section above spells out the cost implication. If you only need one row per product, filter downstream on `position: 1`, or set a `maxVariantsTotal` that matches your budget.

#### Is there a product limit per store?

Shopify's public feed pages 250 products at a time and the Actor follows up to 200 pages for a full catalogue (20 per collection), so roughly 50,000 products per store is reachable. **Max products per store** is what actually bounds a run.

#### How do I detect price changes?

Schedule the Actor, keep `variantUid` as your key, and compare `productHash` between runs. A changed hash means the title, price, compare-at price or availability moved. That is far cheaper than storing and diffing every field.

#### Does it need a proxy?

Usually not. Enable datacenter proxy rotation if you see `ACCESS_DENIED` on a store that works in your browser. Residential groups also work but Apify bills them per gigabyte, so reach for datacenter first.

#### Is scraping Shopify product feeds legal?

The Actor requests a public endpoint that merchants publish so their catalogue can be syndicated, and it identifies itself. Product and price data is business information, not personal data. You remain responsible for how you use it, including any contractual obligations you have with the brands involved.

#### Something looks wrong — how do I report it?

Open the **Issues** tab on this Actor page with the store domain and the field you expected.

***

Runnable examples and how-to guides for these Actors: [github.com/batow133/axiora-apify-actors](https://github.com/batow133/axiora-apify-actors)

# Actor input Schema

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

One store per entry. A bare domain, a full URL or a myshopify.com address all work: allbirds.com, https://kith.com, examplestore.myshopify.com.

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

Restrict to specific collections by handle, for example mens-shoes or new-arrivals. The handle is the last path segment of a /collections/... URL. Leave empty to read the whole catalogue.

## `productHandles` (type: `array`):

Fetch only these specific products by handle, the last path segment of a /products/... URL. Fastest option when you track a known set of SKUs.

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

Stop after this many products from each store. One product usually expands to several variant rows.

## `maxVariantsTotal` (type: `integer`):

Hard ceiling on billed rows across every store. The run stops cleanly when it is reached.

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

Add the product description as clean plain text. Off by default because descriptions dominate export size.

## `descriptionMaxChars` (type: `integer`):

Truncate description text at this length.

## `availableOnly` (type: `boolean`):

Keep only variants the store currently reports as purchasable. Leave off to see sold-out variants too, which is what you want for stockout tracking.

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

Keep only variants where the compare-at price is higher than the current price, meaning the store is advertising a discount.

## `minPrice` (type: `number`):

Keep only variants priced at or above this value, in the store's own currency. Leave empty for no lower bound.

## `maxPrice` (type: `number`):

Keep only variants priced at or below this value, in the store's own currency.

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

Keep only products from these vendors, case-insensitive substring match.

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

Keep only these Shopify product types, case-insensitive substring match.

## `tags` (type: `array`):

Keep only products carrying at least one of these tags, case-insensitive.

## `publishedAfter` (type: `string`):

Keep only products published on or after this date. Accepts 2026-01-31 or a relative value such as 30 days. Good for catching new arrivals.

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

Give up on a single store request after this many seconds.

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

Optional. Some stores put a bot shield in front of their product feed and answer 403. Datacenter proxy rotation usually clears that. Residential groups also work but Apify bills them per gigabyte.

## Actor input object example

```json
{
  "stores": [
    "allbirds.com",
    "https://rothys.com",
    "examplestore.myshopify.com"
  ],
  "collections": [
    "mens-shoes",
    "sale"
  ],
  "productHandles": [
    "wool-runner-go"
  ],
  "maxProductsPerStore": 250,
  "maxVariantsTotal": 5000,
  "includeDescription": false,
  "descriptionMaxChars": 2000,
  "availableOnly": false,
  "onSaleOnly": false,
  "minPrice": 20,
  "maxPrice": 500,
  "vendors": [
    "Nike"
  ],
  "productTypes": [
    "Sneakers"
  ],
  "tags": [
    "new",
    "sale"
  ],
  "publishedAfter": "30 days",
  "requestTimeoutSecs": 30,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `variants` (type: `string`):

Every variant collected, with SKU, options, pricing, compare-at price, discount, availability and product context.

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

Per-store currency, product and variant counts, filter effects, billing and network totals.

# 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",
        "kith.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("axiorasolutions/shopify-variant-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",
        "kith.com",
    ] }

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

```

## MCP server setup

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