Shopify Products Scraper — Barcode GTIN, Variants & Prices
Pricing
$1.00 / 1,000 per product returneds
Shopify Products Scraper — Barcode GTIN, Variants & Prices
Full product catalogs from any Shopify store: one row per product with every variant's barcode (GTIN/EAN/UPC), SKU, price, currency, availability and stock where exposed. Custom domains and headless stores resolved automatically. Measured barcode fill: 94.1% of variants across 6 stores.
Pricing
$1.00 / 1,000 per product returneds
Rating
0.0
(0)
Developer
Scrapers Delight
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
6 hours ago
Last modified
Categories
Share
Full product catalogs from any Shopify store. One row per product with title, vendor,
productType, tags, priceMin/priceMax, compareAtPrice, onSale, currency, availability,
images, options, description and a complete variants array — and on every variant
the field the catalog feed itself never carries: the barcode (GTIN/EAN/UPC), plus
sku, price, currency, weight and (where the store exposes it) live
inventoryQuantity. Works on custom domains, *.myshopify.com domains and headless
storefronts — paste gymshark.com and the Actor recovers gymsharkusa.myshopify.com
from the homepage HTML by itself. No login. No API key. No browser.
Measured on 90 products / 629 variants across 6 live stores (2026-08-15): barcode on 94.1% of variants, SKU 100%, price 100%, currency 100%. On the shipped default input (300 products from allbirds.com + colourpop.com): barcode on 99.8% of variants, currency 100%, images 100%.
{"storeUrls": ["allbirds.com", "colourpop.com"],"enrichVariants": true,"maxItems": 300,"maxItemsPerStore": 150,"maxConcurrency": 5,"proxyConfiguration": { "useApifyProxy": true }}
Click Try for free and hit Start — that block is literally the input the Actor ships with. A platform run on those shipped defaults returned 300 products (150 per store) in 55 seconds for $0.30, with barcode on 99.8% of variants, currency 100%, inventory quantity 100%, images 100%.
The wedge: the barcode is not in the feed
Every Liquid Shopify storefront serves a public catalog feed at
/products.json?limit=250&page=N — and most Shopify scrapers stop there. That feed has
no barcode key at all. The GTIN/EAN/UPC only exists on the per-product endpoint
(/products/{handle}.json), one request per product, which is exactly the request this
Actor spends when enrichVariants is on (the default). The same request also brings
per-variant price_currency, weight + weight_unit, inventory_policy and — on
stores that expose it — inventory_quantity, including negative values on oversold
variants (a real -5 appears in the sample row below).
Why the barcode matters: it is the join key. A GTIN matches a Shopify variant to the same product on Amazon, Google Shopping, eBay, a POS system or a wholesale price list — matching by title never survives "Men's Strider - Medium Grey (Blizzard Sole)". Barcode- level rows are what price-intel, marketplace-arbitrage and catalog-matching buyers actually need, and generic "scrape any Shopify store" actors that read only the feed cannot deliver them.
Measured fill, because it is store-dependent and you should see the honest number before
buying: across the 6-store sample, 85.6% of products carried a barcode on at least one
variant and 94.1% of all variants carried one — but the spread matters: fashion and
beauty stores measured at or near 100%, while one coffee store in the sample published no
barcodes at all (0/3 variants on its sampled products). If your target store does not put
GTINs into Shopify, no scraper can conjure them; the variantsWithBarcode field on every
row tells you exactly what you got.
Read this before you buy rows
- Barcode fill is a property of the store, not of this Actor. 94.1% of variants
across the 6-store benchmark — but a store that never entered GTINs yields zero
(measured: 0% on one coffee store, 100% on Allbirds). Run 150 rows first, read
variantsWithBarcode, then scale. - The public feed hard-caps at 25,000 products per store. Measured on a 100k-product
catalog:
page=100is the last valid page atlimit=250;page=101and beyond return HTTP 400. A store bigger than 25,000 products cannot be fully enumerated through the public feed by this or any other feed-based scraper, and the Actor logs a warning when it hits the wall instead of pretending it got everything. - Prices and currency follow the exit IP's country (Shopify Markets). The same
kith.com catalog priced in CAD from a Canadian home IP and in USD through a US exit.
Every variant carries its own
currencyfield so you always know what you got, andproxyConfigurationaccepts a country pin if you need one consistent market. inventoryQuantityis only published by some stores — 33.4% of variants across the 6-store sample, 100% on the two default stores. When it isnullthe store hides stock counts;available(true/false) still fills from the feed on every variant.- Password-protected and pre-launch stores return nothing. The Actor reports the store in the log, skips it, and bills nothing for it — but it cannot get behind Shopify's password page, by design.
onlyWithBarcodeneeds the enrichment toggle. Barcodes only exist on the per-product endpoint, so if you switch enrichment off and the barcode filter on, the Actor logs a warning and switches enrichment back on rather than failing the run.
What you get
One row per product. Prices are numbers in major units (91 = $91.00), dates are the
store's own ISO-8601 timestamps, scrapedAt is a full UTC timestamp.
| Group | Fields | Example |
|---|---|---|
| Identity | store, fetchHost, myshopifyDomain, productId, handle, title, url | allbirds.com · 7199699927120 · mens-strider-medium-grey |
| Catalog | vendor, productType, tags[], options[], createdAt, publishedAt, updatedAt | Allbirds · Shoes · Size 8–14 |
| Pricing | priceMin, priceMax, compareAtPriceMin, compareAtPriceMax, onSale, currency | 91 · was 130 · true · USD |
| Stock | available, variantCount, variantsWithBarcode | true · 13 · 13 |
| Barcodes | barcodes[] (unique GTINs across variants) | ["196942200889", …] |
| Variants | variants[]: variantId, title, sku, barcode, price, compareAtPrice, currency, available, position, option1..3, grams, weight, weightUnit, requiresShipping, taxable, inventoryQuantity, inventoryPolicy, inventoryManagement, createdAt, updatedAt | 13 variants, every one with barcode + stock |
| Media | featuredImage, images[], imageCount | 5 CDN URLs |
| Content | descriptionText (clean text), descriptionHtml (original) | the store's own copy |
| Provenance | enriched, enrichSource, scrapedAt | true · product-json |
The dataset ships with three saved table views: Products (overview), Barcode matching (GTIN columns for catalog joins) and Price intel (price/was/on-sale/stock).
Field fill — measured, not estimated
Cross-store benchmark: 90 products / 629 variants across 6 stores (Allbirds, ColourPop, Kith, Gymshark, Death Wish Coffee, Fashion Nova — fashion, beauty, coffee; custom domains and recovered headless stores), fetched through the Apify proxy, 2026-08-15. Sorted by fill; the store-dependent fields are bolded.
| Field | Fill | Notes |
|---|---|---|
title / handle / url / productId | 100% | |
sku (variant) | 100% | |
price (variant) | 100% | |
currency (variant) | 100% | from price_currency, needs enrichVariants |
weight / grams (variant) | 100% | |
images / descriptionHtml / vendor / productType / publishedAt | 100% | |
tags | 98.9% | |
barcode (per variant) | 94.1% | the wedge — and store-dependent: 100% on some stores, 0% on one |
barcode (≥1 variant per product) | 85.6% | variantsWithBarcode tells you per row |
compareAtPrice | 33.5% | only exists while a variant is discounted — a real absence, not a gap |
inventoryQuantity | 33.4% | only stores that expose stock counts; null = hidden, available still fills |
option2 / option3 (variant) | 0% on one-option catalogs | filled only when the store uses 2–3 product options (e.g. size + colour). Measured 0/1,675 variants on the shipped defaults — Allbirds sells by size only, ColourPop by shade only. option1 is 100%. |
myshopifyDomain | 0% on custom-domain input | filled only when the catalog is read from a *.myshopify.com host — you pasted one, or a headless storefront was recovered (gymshark.com → gymsharkusa.myshopify.com). A store served on its own domain returns null here: measured 0/300 on the shipped defaults. fetchHost always tells you which host actually served the rows. |
The shipped default run (300 products, allbirds.com + colourpop.com, 1,675 variants, re-audited on the platform dataset 2026-08-19): barcode 99.8% of variants (1,671/1,675) · barcode ≥1 variant 98.7% of products (296/300) · currency 100% · inventoryQuantity 100% · sku 100% · vendor / productType / tags / images / description / options 100% · compareAtPrice 65.4% of variants and 31.3% of products · in stock 100% of products have at least one purchasable variant · 300/300 unique product ids · 5.6 variants per product on average. Every run prints its own measured fill in the log, so you can check any store the same way.
Reliability contract
- A run always returns rows. Send
{}, send{"helloWorld": 123}, send a list of unparseable tokens — the Actor logs a warning and falls back to the documented sample (allbirds.com,colourpop.com) instead of failing. Measured:{}→ 300 rows in 48 s,{"helloWorld":123}→ 300 rows in 86 s. - A run never times out. The Actor reads the run's own wall-clock deadline and stops
collecting with ~30 s in reserve, flushes every row it already has, and finishes
SUCCEEDED with a
Partial catalog: N products delivered…status message. Measured on an unbounded input against a 120 s budget, three times: 533 / 500 / 285 rows at 91 / 90 / 90 s, no timeout. Raise the run timeout to go deeper — nothing else changes. - Delivered rows are billed rows, and only once. Rows are charged on push, in batches, behind the run's own max-total-charge headroom: a run that hits its charge cap stops on a whole row and says so, and no row is ever delivered unbilled or billed twice. Every run asserts that the product ids it pushed are unique before it reports success.
- A filtered or duplicate product costs nothing.
onlyAvailableis applied from the catalog feed before any enrichment request is spent;onlyWithBarcodedrops rows before they are pushed. Neither is billed.
How to run it
Any store, any mix of domain styles
{ "storeUrls": ["allbirds.com", "fnova.myshopify.com", "kith", "https://colourpop.com/collections/best-sellers"] }
Custom domains, myshopify domains, bare handles and full pasted URLs (the path is
ignored) all work. Stores are processed in order; requests within a store run at
maxConcurrency (default 5).
Headless storefronts — automatic recovery
Gymshark 403s /products.json on its own domain; Fashion Nova 404s it. Both still leak
their *.myshopify.com handle in the homepage HTML, and the Actor recovers it by itself
(verified live: www.gymshark.com → gymsharkusa.myshopify.com, www.fashionnova.com →
fnova.myshopify.com). You paste the brand's domain; the Actor finds the catalog.
Feed-only mode — half the requests, no barcodes
{ "storeUrls": ["allbirds.com"], "enrichVariants": false, "maxItems": 1000 }
Skips the per-product request: roughly twice as fast, same price per product, but
barcode, currency, weight, inventoryQuantity stay null on every row. Use it
when you only need titles, prices and availability.
Barcode-only output — for catalog matching
{ "storeUrls": ["allbirds.com"], "onlyWithBarcode": true, "maxItems": 500 }
Drops products with no GTIN on any variant before billing — you pay only for rows you can join on.
Scheduling and integrations
Save the input as a Task (Console → Save as Task), attach a Schedule — daily price/stock monitoring of a competitor set is the classic use. The dataset is available over the REST API and the standard integrations (Zapier, Make, n8n, webhooks, MCP). Starting a run from the API:
curl -X POST "https://api.apify.com/v2/acts/scrapersdelight~shopify-products-scraper/runs?token=YOUR_TOKEN" \-H 'Content-Type: application/json' \-d '{"storeUrls":["allbirds.com"],"maxItems":200}'
Sample row
A real row from an actual run, captured 2026-08-15 (arrays truncated here for length —
variants has all 13 entries in the dataset, images all 5, tags all 21).
{"store": "allbirds.com","fetchHost": "allbirds.com","myshopifyDomain": null,"productId": 7199699927120,"handle": "mens-strider-medium-grey","title": "Men's Strider - Medium Grey (Blizzard Sole)","url": "https://allbirds.com/products/mens-strider-medium-grey","vendor": "Allbirds","productType": "Shoes","tags": ["allbirds::edition => classic", "allbirds::gender => mens", "allbirds::hue => grey"],"priceMin": 91,"priceMax": 91,"compareAtPriceMin": 130,"compareAtPriceMax": 130,"currency": "USD","onSale": true,"available": true,"variantCount": 13,"variantsWithBarcode": 13,"barcodes": ["196942200889", "196942200995", "196942201084"],"variants": [{"variantId": 41243839496272,"title": "8","sku": "A11718M080","barcode": "196942200889","price": 91,"compareAtPrice": 130,"currency": "USD","available": false,"position": 1,"option1": "8", "option2": null, "option3": null,"grams": 903, "weight": 1.9911, "weightUnit": "lb","requiresShipping": true, "taxable": true,"inventoryQuantity": -5,"inventoryPolicy": "deny","inventoryManagement": "shopify","createdAt": "2025-07-23T14:43:12-07:00","updatedAt": "2026-08-14T21:14:02-07:00"}],"options": [{ "name": "Size", "values": ["8", "8.5", "9", "9.5", "10", "10.5", "11", "…"] }],"featuredImage": "https://cdn.shopify.com/s/files/1/1104/4168/files/A11729_25Q3_Strider-Medium-Grey-Blizzard-Sole_PDP_LEFT.png?v=1754951480","imageCount": 5,"descriptionText": "Made to match the pace of life in motion, the Strider offers comfort and versatility…","createdAt": "2025-07-23T14:43:12-07:00","publishedAt": "2026-08-11T15:42:35-07:00","updatedAt": "2026-08-14T21:14:02-07:00","enriched": true,"enrichSource": "product-json","scrapedAt": "2026-08-15T07:36:41.118Z"}
Fields people misread:
inventoryQuantity: -5is real — the store oversold that variant (backorders).nullmeans the store hides stock counts;0means genuinely none left.compareAtPrice: nullis normal — it only exists while a variant is discounted.onSaleis derived from it, so 31% on-sale means 31% of that catalog was discounted.availableat the product level means at least one variant is purchasable; checkvariants[].availableper size/color.priceMin/priceMaxcollapse the variant spread; single-price products have both equal.
Input
| Field | Type | Default | What it does |
|---|---|---|---|
storeUrls | string list | ["allbirds.com", "colourpop.com"] | Stores to scrape — custom domain, myshopify domain, bare handle or pasted URL. |
enrichVariants | boolean | true | One extra request per product for barcode/GTIN, currency, weight, stock. The wedge — leave on. |
onlyWithBarcode | boolean | false | Only products with ≥1 GTIN. Dropped rows are never billed. Needs enrichment on. |
onlyAvailable | boolean | false | Only in-stock products, filtered from the feed before any enrichment is spent. |
maxItems | integer | 300 | Your hard cost cap, across all stores. 300 = $0.30, 1,000 = $1. |
maxItemsPerStore | integer | 150 (prefill) | Per-store cap so one huge catalog cannot eat the run. 0 = off. |
maxConcurrency | integer | 5 | Parallel requests within a store. Max 10. |
proxyConfiguration | proxy | Apify datacenter | Shopify is not walled — the cheap rung is the default on purpose. Pin a country to pin the currency. |
Pricing
$0.001 per product returned — $1 per 1,000. Charged on the product-scraped event.
No start fee, no monthly fee from this Actor — several incumbents charge an
actor-start event on top of their per-item rate; this one does not.
| Run | Products | Cost |
|---|---|---|
| The shipped default (2 stores sampled) | 300 | $0.30 |
| One mid-size store, complete | 1,000 | $1.00 |
| Competitor set, 10 stores × 500 | 5,000 | $5.00 |
| The biggest single catalog the feed can serve | 25,000 | $25.00 |
- You are charged for rows delivered. Products removed by
onlyWithBarcode/onlyAvailable, duplicate product ids, and unreachable stores are never billed. - Rows are charged as they are pushed (
Actor.pushData(items, 'product-scraped')) — if you hit a budget cap you get whole, billed rows and a clear warning, never a half-billed dataset. - Enrichment does not change the price: barcode rows cost the same $0.001. It buys our requests, not your money.
Transport — measured, and why the cheap proxy is the default
Ladder, run against the same 8-request set (catalog page + per-product endpoint across 4 stores) on 2026-08-15:
| Rung | Result |
|---|---|
| Direct, no proxy | 8/8 |
| Apify proxy, datacenter (auto) | 8/8 |
| Apify RESIDENTIAL | 8/8 |
| Apify RESIDENTIAL + pinned session | 5/8 — the 3 misses were proxy-side 590 UPSTREAM504 on one flaky exit, not Shopify blocks |
Sustained load through the Apify datacenter rung: 30/30 = 100.0% across 5 stores at
concurrency 5. Shopify's storefront endpoints are not bot-walled, so the default is the
cheap datacenter rung and you are not paying residential rates you do not need. The
occasional transient (430 security block, proxy 59x) is retried on a fresh exit IP
automatically, up to 4 attempts with backoff.
robots.txt on Shopify stores speaks, and this Actor honours it: the standard file
disallows /cart, /checkout, /account, /search, /recommendations/products and
sort/filter query permutations — none of which this Actor touches (this is also why
currency comes from the per-product endpoint and not the common /cart.js trick, which
lives under a disallowed path). The routes it does read — /products.json and
/products/{handle}.json|.js — appear in no Disallow line on any store checked;
ColourPop's file explicitly Allows /products/. Concurrency defaults to a modest 5
per store.
Duplicates — measured
| Walk | Rows | Unique | Duplicates |
|---|---|---|---|
| kith.com, pages 1–6 contiguous | 1,500 | 1,500 | 0.0% |
| colourpop.com, pages 1–4 contiguous | 1,000 | 1,000 | 0.0% |
The feed paginates cleanly — no promoted/pinned cards, no page-boundary drift in the
measurement. The Actor still enforces a run-wide dedupe on fetchHost:productId before
billing (the same catalog can legitimately be reachable through two of your input
domains — the custom domain and its myshopify handle), and asserts uniqueness across
everything it pushed before the run may succeed; a violation fails the run rather than
handing you a dirty dataset.
When a run fails
This Actor fails loudly rather than handing back a quiet empty dataset:
- 0 rows → the run throws, and the message names each store's specific failure (password-protected, not Shopify, catalog route blocked) and whether your filters removed everything. Nothing is billed.
- A store fails, others work → the failed store is reported in the log with its reason and skipped; the run continues and only delivered rows are billed.
- Barcode enrichment degrades (>30% misses on a store) → a loud warning names the
store, so silent
nullbarcodes cannot masquerade as "that store has no GTINs". - The 25,000-product feed cap → a warning tells you the catalog kept going; the rows you got are complete and correctly billed.
- Budget cap hit → whole rows, charged exactly, plus a warning — never a half-billed dataset.
Who buys this
- Price-intelligence and repricing teams — competitor price,
compareAtPrice,onSaleand stock across any set of Shopify stores, on a schedule, joined to your own catalog by GTIN. - Marketplace arbitrage / dropship researchers — the barcode is the join key to
Amazon/eBay/Google Shopping listings;
onlyWithBarcodegives you only the sellable, matchable rows. - Catalog and PIM teams — enrich a distributor feed with images, descriptions, option structures and GTINs from brand storefronts.
- Ecommerce competitive intel — assortment size, price architecture, discount depth
(
compareAtPricespread), launch cadence (publishedAt) per competitor. - Agencies and app developers auditing merchant catalogs — barcode coverage, duplicate SKUs and untagged products, straight from the storefront.
FAQ
Does this need an account, a login or an API key? No. Both endpoints are public storefront routes every Liquid Shopify store serves. There is no login, no cookie, no CAPTCHA solving, no browser.
Do I really get barcodes/GTINs?
Where the store entered them, yes: 94.1% of 629 variants across the 6-store benchmark,
99.8% on the shipped default run. Where a store never entered GTINs, no scraper can
invent them — variantsWithBarcode on every row tells you exactly what you got, and
onlyWithBarcode makes you pay only for matchable rows.
Which stores work? Any Shopify store whose storefront is public: custom domains, myshopify domains, and headless builds whose homepage leaks the myshopify handle (Gymshark and Fashion Nova both verified). Password-protected stores are reported and skipped, never billed.
Can I get a store's entire catalog? Up to 25,000 products (100 pages × 250), which covers the overwhelming majority of stores completely. Beyond that the public feed itself returns HTTP 400 — a platform cap, not an Actor limit — and the run warns instead of pretending.
Why do prices show a currency I did not expect?
Shopify Markets localizes price and currency by exit-IP country. Every variant carries
currency, and pinning a proxy country pins the market.
Are stock numbers live?
inventoryQuantity is what the store publishes at scrape time, on the ~⅓ of stores that
expose counts (100% of the two default stores). available fills on every variant
regardless. Negative values are real oversells.
Two runs — will I get duplicates?
Within a run, never: dedupe on fetchHost:productId is asserted before the run may
succeed. Across runs, rows carry stable productId/variantId keys to merge on.
Do I get charged for filtered or failed rows?
No. onlyWithBarcode, onlyAvailable, duplicates, and stores that could not be read are
all dropped before billing.
Will a run ever succeed with zero rows? No. Zero rows throws, with per-store reasons in the message. Nothing is billed on a thrown run.
Is scraping a store's catalog legal? The data is the store's public storefront output. You are responsible for complying with each store's terms of service and applicable law in how you collect and use it — especially if you resell the data.
Honest limits
- No reviews, no collections tree, no customer data. This Actor does one thing — the product catalog with variant-level barcodes — completely. Review scraping is a different function (and a different consent surface) on purpose.
- The 25,000-product cap per store is Shopify's, not ours (measured: page 101 = HTTP 400). Mega-catalogs need a different enumeration strategy than the public feed.
- Barcode fill is the store's data hygiene, not a constant: 0% to 100% by store. Measure with 150 rows before you buy 15,000.
inventoryQuantityis absent on ~⅔ of stores (33.4% of benchmark variants carried it).availableis the universal stock signal.- Draft/unpublished products never appear — the public feed only serves what the
store publishes.
statusfields from the Admin API are not accessible without the merchant's own credentials, and this Actor uses none. - Currency follows the exit IP unless you pin a proxy country. The
currencyfield makes every row self-describing either way.
Legal & fair use
This Actor reads public, unauthenticated storefront endpoints (/products.json,
/products/{handle}.json, /products/{handle}.js) and, only when a headless store
requires it, the public homepage HTML. It does not log in, does not touch carts,
checkouts, accounts, search or any path a standard Shopify robots.txt disallows, and
collects no personal data — rows describe products, not people. Store and brand names
belong to their owners; this Actor is not affiliated with, endorsed by, or connected to
Shopify Inc. or any store it reads. You are responsible for complying with each
store's terms and with how you use the data.
Feedback
Found a store shape this misses or want a field added? Open an issue on the Issues tab — and if the Actor earns it, a review on the Reviews tab helps other buyers find it.