Data Tooly logo Data Tooly
Guide · E-commerce Intelligence

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.

Published · Updated · By Omar Eldeeb

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. 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 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 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 turns that into a notification.

The holiday sales calendar 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.

Do it without code

The free e-commerce price tracker builder 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.

Open the free E-commerce price tracker tool →

Written by Omar Eldeeb, who also builds the tools and Apify actors mentioned here. Links to apify.com carry an affiliate parameter — see the disclosure.