# Scrape Product Variants: Sizes, Colors and Price per SKU

> How to scrape product variants (size, color, pack) with each variant's SKU, price and stock: the Shopify .js endpoint, a Python API run, and the traps.

- URL: https://datatooly.xyz/guides/scrape-product-variants-sizes-colors/
- Author: Omar Eldeeb (https://datatooly.xyz/about/)
- Published: 2026-10-20
- Updated: 2026-10-20
- Free tool: [E-commerce price tracker — scrape product prices, stock and variants](https://datatooly.xyz/ecommerce-price-tracker/)
- Tags: ecommerce, python, webscraping, datascience

To scrape product variants, read every variant of a product, not just the price on the page. Each variant (a size, color or pack size) has its own SKU, option values, price and stock. On Shopify, add `.js` to a product URL to get them as JSON. For Shopify, Walmart, Zalando, Target and other stores, an Apify actor returns them as a `variants[]` array on each product row.

This guide shows both methods with real output from 2026-10-20, what a variant row contains, which sites give per-variant prices, and the traps that make variant data wrong without an error.

## Why the product price isn't enough

A product page shows one selected variant. The price at the top is that variant's price, and the stock flag is that variant's stock. Here's the same Walmart listing in one of our runs:

| Variant | Price | In stock |
|---|---|---|
| 5 Pack (the URL's own variant) | $19.40 | no |
| 2 Pack | $7.76 | yes |
| Single | $3.88 | yes |

If you had stored only the product-level fields, you'd have recorded "$19.40, out of stock" for a cable you could buy that day. Variant-level data fixes three common problems:

- **Price ranges.** On one Zalando sneaker page in an October 17 run, most sizes cost €39.95 but size 36 cost €64.95.
- **Partial stock.** A Columbia rain jacket had 271 color and size combinations, and 207 of them were out of stock, while the product as a whole said "in stock".
- **Matching.** Retailers sell a pack size or color under its own SKU or GTIN, so matching across stores works only at the variant level.

## Method 1: Shopify's `.js` endpoint (no tools)

Every Shopify store serves a product's variants at the product URL plus `.js`. No key or login is needed:

```python
import requests

url = "https://www.skullcandy.com/products/crusher-540-active"
p = requests.get(url + ".js", headers={"User-Agent": "Mozilla/5.0"}, timeout=30).json()

print(p["title"], "| options:", [o["name"] for o in p["options"]],
      "| price_min", p["price_min"] / 100, "price_max", p["price_max"] / 100)
for v in p["variants"]:
    print(f"  {v['id']}  {v['sku']:<11} {v['title']:<32} {v['price'] / 100:>7.2f}  "
          f"was={v['compare_at_price'] and v['compare_at_price'] / 100}  available={v['available']}")
```

Output on 2026-10-20:

```text
Crusher® 540 Active | options: ['Color'] | price_min 209.99 price_max 229.99
  50564609998905  S6EVW-U054  Skullcandy X Baker Skateboards    229.99  was=None  available=True
  45146487652409  S6EVW-T013  Smoke                             209.99  was=None  available=True
  45138862538809  S6EVW-T016  Forest                            209.99  was=None  available=True
  44402118393913  S6EVW-T005  Soft                              209.99  was=None  available=True
  44494413594681  S6EVW-T992  Coal                              209.99  was=None  available=True
  44402118524985  S6EVW-T993  Concrete                          209.99  was=None  available=True
```

Two details matter:

- In `.js`, `price` and `compare_at_price` are **integers in cents**. Divide by 100.
- The `.json` version (`/products/handle.json`) returns the price as a string like `"229.99"` and has **no `available` field**. Use `.js` when you need stock.

To list every product of a store before you read variants, see [Shopify products.json pagination](https://datatooly.xyz/guides/shopify-products-json-pagination/). This method only works on Shopify. Other platforms put variants in JSON-LD, in embedded page state, or in a separate API, and each needs its own parser.

## Method 2: variants from any supported store, through the API

[E-commerce Product Scraper Pro](https://apify.com/constructive_calm/ecommerce-product-scraper-pro?fpr=v77kxu) on Apify reads variants on Shopify, WooCommerce, Walmart, Target, Zalando and the other stores in its supported-sites table, and puts them in a `variants[]` array on each product row. `includeVariants` is on by default. This script runs it on three stores and writes one CSV row per variant:

```python
import csv
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("constructive_calm/ecommerce-product-scraper-pro").call(run_input={
    "detailsUrls": [
        {"url": "https://www.skullcandy.com/products/crusher-540-active"},
        {"url": "https://www.zalando.de/puma-club-ii-era-unisex-sneaker-low-whiteblackvapor-gray-pu115o0sb-a11.html"},
        {"url": "https://www.walmart.com/ip/5-pack-onn-3ft-USB-C-USB-C-Braided-Charging-Cable-Fast-Charging-USB-IF-Certified-White-Compatible-iPhone-17-16-15-Series-Samsung-Galaxy-Note-Pixel-De/20232964243"},
    ],
    "includeVariants": True,          # the default; shown for clarity
    "maxProductResults": 3,
})

rows = []
for p in client.dataset(run["defaultDatasetId"]).iterate_items():
    if p.get("recordType") != "product":
        continue
    variants = p.get("variants") or []
    prices = [v["price"] for v in variants if v.get("price") is not None]
    sold_out = [v for v in variants if v.get("inStock") is False]
    print(f"{p['site']}: {p['name'][:45]} | page price {p['price']} {p['currency']} | "
          f"{len(variants)} variants, price range {min(prices, default=None)}-{max(prices, default=None)}, "
          f"{len(sold_out)} sold out")
    for v in variants:  # one flat row per variant
        rows.append({
            "site": p["site"], "product": p["name"], "variant_sku": v.get("sku"),
            "options": "; ".join(f"{k}={val}" for k, val in (v.get("options") or {}).items()),
            "price": v.get("price"), "currency": v.get("currency") or p.get("currency"),
            "was_price": v.get("originalPrice"), "in_stock": v.get("inStock"), "url": v.get("url"),
        })

with open("variants.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.DictWriter(f, fieldnames=list(rows[0]))
    w.writeheader()
    w.writerows(rows)
print(len(rows), "variant rows written to variants.csv")
```

Output on 2026-10-20 (`pip install apify-client`, and `APIFY_TOKEN` set to your Apify API token):

```text
skullcandy.com: Crusher® 540 Active | page price 229.99 USD | 6 variants, price range 209.99-229.99, 0 sold out
zalando-de: CLUB UNISEX - Low-Top-Sneaker - white/black/v | page price 39.95 EUR | 19 variants, price range 39.95-39.95, 4 sold out
walmart-us: (5 pack) onn 3ft Braided USB-C Charging Cable | page price 19.4 USD | 3 variants, price range 3.88-19.4, 1 sold out
28 variant rows written to variants.csv
```

The first lines of `variants.csv`:

```csv
site,product,variant_sku,options,price,currency,was_price,in_stock,url
skullcandy.com,Crusher® 540 Active,S6EVW-U054,Color=Skullcandy X Baker Skateboards,229.99,USD,,True,https://www.skullcandy.com/products/crusher-540-active?variant=50564609998905
skullcandy.com,Crusher® 540 Active,S6EVW-T013,Color=Smoke,209.99,USD,,True,https://www.skullcandy.com/products/crusher-540-active?variant=45146487652409
```

All three pages were fetched over datacenter connections, so the run had no traffic fee. It billed one start event and three product events. Variants don't add events.

## What a variant contains

Each entry in `variants[]` has the same keys:

| Key | Meaning |
|---|---|
| `sku` | The variant's own identifier: a store SKU, a Walmart item id, or an Amazon ASIN |
| `name` | The variant's title, e.g. "Smoke" or "5 Pack" |
| `options` | The option values as an object, e.g. `{"Color": "Red Lily", "Size": "L"}` |
| `price`, `currency`, `originalPrice` | The variant's price, its ISO currency and its was-price, when the page states them |
| `inStock`, `availability` | Stock flag and the raw availability (`InStock`, `OutOfStock`) |
| `gtin`, `image`, `url` | Barcode, variant image and a URL that selects the variant |

The product row around it has `variantCount`, the product-level `price` and `inStock` (the selected variant's), and `changedVariants` in monitor mode.

## Which sites give per-variant prices

The pattern below comes from the product rows in about 260 of our cloud test runs between 2026-10-17 and 2026-10-20. It's a measure of what those pages contained, not a guarantee for every product:

| Site | Variant entries | With a price | With stock |
|---|---|---|---|
| Shopify stores (Death Wish Coffee, Skullcandy, Beardbrand) | 2,572 | all | 2,454 |
| Columbia (Salesforce Commerce) | 2,803 | all | all |
| Zalando | 1,239 | all | all |
| Walmart | 1,212 | 1,200 | all |
| Bloomingdale's | 835 | all | all |
| John Lewis | 145 | all | all |
| Target | 43 (11 of 70 products had variants) | all | all |
| Amazon (.com, .co.uk, .de) | 2,651 | **none** | **none** |
| A WooCommerce store (Nalgene) | 571 | 22 | 23 |

Amazon is the exception. Its product pages list each variant's ASIN and option values, but not their prices. The row's own `price` is the price of the ASIN in your URL. To price a sibling, put its `variants[].url` (a `/dp/ASIN` link) into `detailsUrls` and scrape it as a product. The [Amazon price scraper guide](https://datatooly.xyz/guides/amazon-price-scraper-asin/) covers that by ASIN and marketplace.

Grocery sites in our runs (Waitrose, Aldi UK) returned no variants: each pack size is its own product page.

## Traps that give wrong variant data without an error

1. **Reporting one price per product.** Store the range (min and max over variants) or a price per variant. "From $209.99" and "$229.99" are both true on the Skullcandy page above.
2. **Reading `inStock` on the product as "every size is available".** The product flag is the selected variant's. The Columbia jacket said in stock with 207 of 271 combinations sold out.
3. **Losing the currency.** Variant `currency` can be empty when the page omits it (always on Amazon). Fall back to the product's `currency`, as the script does. If prices come back in the wrong currency, set `proxyCountry` to the store's country.
4. **Comparing variants by name.** "Black" on one store is "Jet Black" on another. Match on `gtin` when both sides have it, or on your own mapping of option values.
5. **Huge rows.** A product with 271 variants is one row with a long array. Flatten it, as the script does, before you load it into a spreadsheet.
6. **Expecting stable variant prices.** The Zalando size-36 price of €64.95 on October 17 was €39.95 on October 20, like the other sizes. Variant prices move on their own, which is exactly why you track them.

## Holiday season: watch sizes, not just prices

During Black Friday (Friday, November 27, 2026) and Cyber Monday (November 30), a "deal" can cover only some sizes or colors, and the popular ones can sell out first. Neither can be seen from the product-level price. Two settings help:

- Schedule the same input daily from early November with `monitorMode: true`. A variant whose price, was-price or currency changes marks the product `changed` and lists it in `changedVariants`, so you can see a discount that applies to one color only.
- Track `inStock` per variant for the sizes you care about. The [back-in-stock alert guide](https://datatooly.xyz/guides/back-in-stock-alert-scraper/) turns that into a notification.

The [holiday sales calendar](https://datatooly.xyz/guides/holiday-sales-calendar-price-tracking/) lists the 2026 and 2027 sale dates and how early to start a baseline for each.

## Cost

Pay per event, from the actor's pricing table: one product row is one "product scraped" event, $0.0048 on Apify's FREE plan tier down to $0.0008 on GOLD, plus $0.005 per run start. Variants are inside the row, so they don't add events. Sites that need residential proxies or a browser (see the table on the actor page) add a per-MB or per-render fee. Apify's free plan includes $5 of usage every month, with no credit card required. There's no free trial inside the actor.

## Related guides

- [Shopify products.json pagination](https://datatooly.xyz/guides/shopify-products-json-pagination/): list every product of a Shopify store.
- [Scrape product reviews from Walmart, Target and Amazon](https://datatooly.xyz/guides/scrape-product-reviews-walmart-target-amazon/): the review fields, including which variant each reviewer bought.
- [Back-in-stock alerts](https://datatooly.xyz/guides/back-in-stock-alert-scraper/).

## Do it without code

The free [e-commerce price tracker builder](https://datatooly.xyz/ecommerce-price-tracker/) has a variants option and produces a ready-to-run input for the actor, with a fixed example of the output shape. It doesn't fetch prices in your browser.

*Disclosure: I built the actor and this site. The Shopify `.js` method above needs neither.*

Sources: live responses from `skullcandy.com` (`.js` and `.json` endpoints) and an actor run on 2026-10-20; per-site variant counts from our own cloud test runs, 2026-10-17 to 2026-10-20.

## Quick answers

### How do I get all variants of a Shopify product?

Add `.js` to the product URL, for example `https://store.com/products/handle.js`. The JSON has a `variants` array with each variant's id, SKU, title, option values, `price` in cents and an `available` flag. The `.json` version gives the price as a string and no availability flag.

### Why is the price on a scraped product different from the variant I want?

A product page shows one selected variant, usually the default or the first one in stock. The page-level price is that variant's price. Other sizes or pack sizes can cost more or less, so read the price from each variant, not from the product.

### Can I get the price of every Amazon variant from one product page?

Not reliably. In our October 2026 test runs, Amazon product pages listed each variant's ASIN and options (size, color, count) but no per-variant price. To price a variant, scrape its own `/dp/ASIN` page.

### Do variants cost extra when scraping with the Apify actor?

No. Variants are stored inside the product row, and the actor charges per product row delivered, so a product with 1 or 271 variants is one product event.

## Related guides

- [Retail Sales Calendar 2026-2027: When to Track Prices](https://datatooly.xyz/guides/holiday-sales-calendar-price-tracking/)
- [Amazon Price History Without Keepa: Build Your Own](https://datatooly.xyz/guides/amazon-price-history-without-keepa/)
- [Amazon Price Scraper: Prices by ASIN on Any Marketplace](https://datatooly.xyz/guides/amazon-price-scraper-asin/)
- [Back in Stock Alert Scraper: Products, Sizes, Colours](https://datatooly.xyz/guides/back-in-stock-alert-scraper/)
- [Back to School Price Tracker: Tax Holidays & Laptops](https://datatooly.xyz/guides/back-to-school-price-tracker/)
- [Compare Black Friday Prices Across Stores (Google Shopping)](https://datatooly.xyz/guides/black-friday-google-shopping-price-comparison/)
- [Black Friday Price Tracker: Watch 2026 Prices Daily](https://datatooly.xyz/guides/black-friday-price-tracker/)
- [Boxing Day Sales Tracker UK 2026: Prices in GBP](https://datatooly.xyz/guides/boxing-day-sales-price-tracker-uk/)
- [Christmas Price Tracker: When to Buy Gifts Cheapest](https://datatooly.xyz/guides/christmas-price-tracker/)
- [Competitor Price Monitoring: Scraper Setup and Cost](https://datatooly.xyz/guides/competitor-price-monitoring-scraper/)
- [Cyber Monday Price Tracker: Hourly Checks for Nov 30](https://datatooly.xyz/guides/cyber-monday-deal-tracker/)
- [Grocery Price Tracker: Build a Basket Index in Python](https://datatooly.xyz/guides/grocery-price-tracker-scraper/)
- [MAP Monitoring: Find Minimum Advertised Price Violations](https://datatooly.xyz/guides/map-price-monitoring/)
- [Price Drop Alerts to Slack or Email with a Scraper](https://datatooly.xyz/guides/price-drop-alerts-email-slack/)
- [Prime Day Price Tracker: ASINs, Big Deal Days, 2027](https://datatooly.xyz/guides/prime-day-price-tracker/)
- [Ramadan Sale Price Tracker 2027: Noon & Amazon UAE](https://datatooly.xyz/guides/ramadan-eid-sales-price-tracking/)
- [Scrape Product Prices with Python from JSON-LD](https://datatooly.xyz/guides/scrape-product-prices-json-ld-python/)
- [Scrape Product Reviews from Walmart, Target and Amazon](https://datatooly.xyz/guides/scrape-product-reviews-walmart-target-amazon/)
- [Fake Black Friday Deals: Check the Real Discount](https://datatooly.xyz/guides/spot-fake-black-friday-discounts/)
- [Apify E-commerce Scraping Tool Alternative: Same Input](https://datatooly.xyz/guides/switch-from-apify-ecommerce-scraping-tool/)
- [When Do Black Friday Prices Drop? The 2026 Timeline](https://datatooly.xyz/guides/thanksgiving-black-friday-price-drop-timeline/)
- [Walmart and Target Price Scraper: Search and Product Pages](https://datatooly.xyz/guides/walmart-target-price-scraper/)
- [White Friday & 11.11 Tracker: Noon and Amazon UAE Prices](https://datatooly.xyz/guides/white-friday-11-11-noon-amazon-uae-prices/)
- [Shopify products.json Pagination: 250 Limit and the 25,000 Cap](https://datatooly.xyz/guides/shopify-products-json-pagination/)
- [AI Share of Voice: Measure It in ChatGPT and Perplexity](https://datatooly.xyz/guides/ai-share-of-voice-chatgpt-perplexity/)
