Shopify Product Scraper — GTIN & Collections avatar

Shopify Product Scraper — GTIN & Collections

Pricing

from $1.40 / 1,000 products

Go to Apify Store
Shopify Product Scraper — GTIN & Collections

Shopify Product Scraper — GTIN & Collections

Scrape every product and variant from any Shopify store, with the GTIN barcodes, subscription plans and wholesale price breaks that /products.json does not contain.

Pricing

from $1.40 / 1,000 products

Rating

0.0

(0)

Developer

Spool

Spool

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

6 days ago

Last modified

Share

Shopify Product Scraper — GTIN, Variants & Collections

Every product and variant from any Shopify store — with the GTIN barcodes, subscription plans and wholesale prices that /products.json does not contain.

rothys.com 964 variants 81% with barcode newest 100%, oldest 49%
finisterre.com 92 variants 100% with barcode £50 → $66.83
deathwishcoffee.com 62 variants 6 subscription products, real subscriber price

The field almost no Shopify scraper returns

Every Shopify store serves its catalog at /products.json — no key, no login, permitted by robots.txt. Nearly every Shopify scraper reads that one endpoint and returns what it finds.

It does not contain the barcode.

GTIN / UPC / EAN is what connects a Shopify product to the rest of commerce — to Amazon, to Google Shopping, to a distributor's price list, to your own ERP. Without it you have a pile of product titles and the matching is manual.

It lives one layer down, on /products/{handle}.js. Measured across eight stores, it came back on 97–100% of variants for six of them. The other two are the merchant's doing, not the endpoint's: Rothy's is 81% — its newest products are fully barcoded, its oldest half are not — and Death Wish Coffee is 26%:

rothys.com 81% organicbasics.com 97%
allbirds.com 98% gymshark.com 100%
finisterre.com 100% kith.com 100%
tentree.com 100% deathwishcoffee.com 26%

That endpoint costs one request per product, which is why most tools skip it. It is the entire point of this one.


Nine other fields that endpoint has and /products.json does not

what it tells you
barcodeGTIN/UPC/EAN — match to Amazon, Google Shopping, retail
selling_plan_groupsSubscription offers, with the real subscriber price
quantity_price_breaksWholesale volume tiers
quantity_ruleOrder minimums, maximums and pack increments
inventory_managementWhether stock is tracked at all
weight, media, price_variesShipping weight, video/3D media, price ranges

inventory_management deserves a note. Every scraper reports available, but when a merchant does not track inventory, available is a default rather than a fact. inventoryTracked tells you which kind of "in stock" you are looking at. Nobody else reports the difference.

Subscription pricing is read from the variant's own plan allocation, not guessed from the plan's name — so subscriptionDiscountPercent is the actual saving.


What the merchant says is selling

/collections.json is the store's own merchandising, and it is public. A product sitting in the merchant's best-sellers collection is a demand signal the store is publishing itself — not an estimate.

Reading all of it is wasteful: Rothy's has 196 collections, Allbirds 1,000, and most are colour and size groupings. So collections are ranked, not matched — best sellers first, then new arrivals, then trending, then sale, each capped so one kind cannot crowd out another. Internal plumbing (

Discount Eligible Products Excluding Markdown
, test and hidden collections) is dropped.

On Allbirds that turns 1,000 collections into 23 useful reads:

6 best-seller lists · 4 new-arrivals · 3 trending · 10 sale

You get isBestSeller, isNewArrival and inSaleCollection per product. inSaleCollection is deliberately separate from onSale: a product can sit in the sale collection at full price, or be discounted without being merchandised as sale, and those are different facts.


Prices you can actually compare

products.json carries prices as bare numbers with no currency anywhere in it. A UK store showing 50 and a US store showing 50 are £50 and $50.

So the currency is read from the store profile and every price is reported twice:

finisterre.com GBP £50 → $66.83
rothys.com USD $65 → $65

When no rate is available the USD fields are null rather than a guess.

There is a second trap here worth naming, because it is easy to get wrong: /products.json gives prices as decimal strings, while /products/{handle}.js gives them as integer cents. Mixing the two silently divides every enriched price by a hundred. Each source is normalised on the way in.


Built for a merchant feed

Set requireBarcode and every row carries a GTIN. feedReady tells you whether a row has everything Google Merchant Center requires — id, title, link, image, brand, price, availability and GTIN — so you know what needs work before upload rather than discovering it at import.

{ "storeUrls": ["rothys.com"], "requireBarcode": true }

Quick start

{ "storeUrls": ["rothys.com", "deathwishcoffee.com"] }

Domains, full URLs and product links all work. www. is stripped, and www.x.com and x.com are merged so you are never billed twice for one store.


What you get back

One row per variant by default — the shape a price monitor or a feed needs, because barcode, price and stock are all per-variant. Switch rowsPer to product to get variants nested instead.

{
"store": "deathwishcoffee.com",
"storeName": "Death Wish Coffee Company",
"currency": "USD",
"title": "Coconut Caramel Coffee",
"vendor": "Death Wish Coffee",
"url": "https://deathwishcoffee.com/products/coconut-caramel-coffee",
"variantTitle": "1 bag",
"sku": "DW-CC-12",
"barcode": "810063344313",
"price": 13.99,
"priceUsd": 13.99,
"compareAtPrice": null,
"onSale": false,
"available": true,
"inventoryTracked": true,
"subscriptionAvailable": true,
"subscriptionPlans": ["Deliver every 2 weeks", "Deliver every 30 days"],
"subscriptionPriceMin": 12.59,
"subscriptionDiscountPercent": 10,
"collections": ["labor-day-sale-25-off", "new-arrivals"],
"isBestSeller": false,
"isNewArrival": true,
"inSaleCollection": true,
"feedReady": true
}

Eight dataset views ship with it: Products, Merchant feed, Pricing, Subscriptions, Wholesale, Stock, Merchandising and Unavailable.


Recipes — copy, paste, run

Build a Google Merchant feed from a store

{ "storeUrls": ["rothys.com"], "requireBarcode": true }

Find every subscription product and what subscribing saves

{ "storeUrls": ["deathwishcoffee.com"], "requireSubscription": true }

Everything on sale under $50, across several competitors

{
"storeUrls": ["rival-one.com", "rival-two.com"],
"onlyOnSale": true,
"maxPrice": 50
}

Wholesale and B2B offers

{ "storeUrls": ["..."], "rowsPer": "variant" }

Then read the Wholesale view — wholesaleBreaks carries the volume tiers.

A fast, cheap catalog dump with no barcodes

{ "storeUrls": ["gymshark.com"], "enrich": false, "collectionsMode": "none" }

Every option

OptionDefaultWhat it does
storeUrls—Required. Domains, URLs or product links, mixed freely
enrichtrueRead barcodes, subscriptions and wholesale. One extra request per product. Turn off for a fast catalog dump
rowsPervariantvariant for feeds and price monitoring, product for catalog analysis
maxProductsPerStore1000How deep to read. The main lever on run time and cost
collectionsModekeykey reads only signal collections, all up to 40, none skips them
requireBarcodefalseKeep only rows with a GTIN — the feed-building filter
onlyInStockfalseKeep only available variants
onlyOnSalefalseKeep only discounted variants
requireSubscriptionfalseKeep only subscription products
minPrice / maxPrice—Compared in USD, so it means the same on every store
vendors / productTypes / tags—Filter by brand, type or tag
includeDescriptionfalseAdds the full description. Large
maxConcurrency5Parallel enrichment requests. Lower it if a store returns 429

What it covers, honestly

About five stores in six work. Tested against 18 real stores: 15 served the catalog, 3 did not. Those three return not_a_shopify_store — they have disabled the endpoint or are not on Shopify. A miss is always reported, never silent.

Barcode coverage is the merchant's choice, not ours. Six of eight stores returned barcodes on 97–100% of variants. Rothy's returned 81%, because its older products were never given one; Death Wish Coffee 26%, because they simply have not entered them. enriched tells you whether the field was read; a null barcode means the merchant left it empty.

available depends on whether the merchant tracks stock. inventoryTracked says which, rather than reporting a confident boolean that means nothing.

Currency is what the store serves you. Shopify switches storefront by geography, so a store may answer in USD when read from outside its home market. Figures are reported exactly as served rather than normalised into something tidier but invented.

Prices and stock are a snapshot. They are true at the moment of the run.

Enrichment costs one request per product. A 1,000-product store is ~1,010 requests. That is the trade for barcodes; enrich: false avoids it entirely.

Direct requests first; Apify Proxy only for what a store refuses. Shopify throttles the platform's shared addresses by address, not by rate — Allbirds enriched 30% of its catalog directly from the platform and 96% once the refused products were retried through Apify Proxy, while the same requests pass 100% from an ordinary home connection. So every product is tried directly, and only the ones the store turned down go through the proxy, one fresh session each. It is included in the price; the log says how many it recovered, and anything still missing is reported as missing rather than passed off as "no barcode".


Everything read here is a public endpoint Shopify serves by design, requires no authentication, and is permitted by robots.txt: /products.json, /products/{handle}.js, /collections.json and /meta.json.

What this Actor deliberately does not do: Shopify's /cart/add.js will reveal a variant's exact remaining stock if you request an absurd quantity, and tools advertising "real inventory counts" appear to use it. Shopify's default robots.txt disallows /cart on every store we checked, and /search with it. Neither is touched here. Exact stock counts are not worth ignoring a merchant's robots.txt for.

Everything returned is catalog data — products, prices, barcodes, availability. No customer data, no personal information.


When a store can't be read

errorMeaning
not_a_shopify_storeNo public catalog at /products.json
catalog_blockedThe store returned 403 or 429
catalog_emptyCatalog exists but has no products
all_products_filtered_outRead fine, but your filters matched nothing
invalid_inputCouldn't read a domain from that value

A breakdown is saved to the key-value store as RUN_SUMMARY.


FAQ

How do I get GTIN or UPC codes from a Shopify store? Turn on enrich (it is on by default). Barcodes come from /products/{handle}.js, which /products.json does not include.

How is this different from other Shopify product scrapers? Most read one endpoint and return titles, prices and images. This adds the barcode, subscription plans with real subscriber prices, wholesale price breaks, order minimums, whether stock is actually tracked, and which of the merchant's own collections each product sits in.

Can I scrape all Shopify stores at once? No. You supply the store list. This reads the stores you name, thoroughly.

Does it work on any Shopify store? Roughly five in six. Some merchants disable the public catalog, and those are reported rather than guessed at.

Will it get me blocked? It makes plain HTTPS requests at a concurrency you control. Lower maxConcurrency if a store returns 429.


How to use it

  1. Open the Actor and paste store domains into Shopify stores.
  2. Leave enrich on for barcodes. Set requireBarcode if you are building a merchant feed, or requireSubscription to find subscription products.
  3. Click Start. 800 products take under a minute.
  4. Open the Merchant feed, Subscriptions or Wholesale view, or export as CSV, Excel or JSON.
  5. To keep a feed fresh, Schedule a daily run; to push rows into Google Sheets, a database or a webhook, use the integrations tab.

How much does it cost?

Pay per event — you are charged per product, not per row. A product with 20 size/colour variants is 20 rows and one charge.

Planper product, enriched (barcode, subscriptions, wholesale)per product, catalog only (enrich: false)
Free$0.002$0.0005
Bronze$0.0018$0.00045
Silver$0.0016$0.0004
Gold$0.0014$0.00035

Actor start is $0.002. Stores that could not be read cost nothing. If a store throttles the per-product requests, the products that could not be enriched are billed at the catalog-only price, not the enriched one — you pay for barcodes only when you get them.

Worked examples

  • Rothy's, 40 products → 827 variant rows with barcodes → 40 × $0.002 = $0.08
  • A 1,000-product catalog with GTINs → $2.00 (Gold plan: $1.40)
  • The same catalog with enrich: false → $0.50
  • The free plan's $5 credit covers about 2,500 enriched products

For comparison, tools that bill per dataset row would charge that Rothy's run 827 times.


More Shopify tools from spool

All three read the same public endpoints, respect robots.txt, and report a miss rather than guessing.


Support

Open an issue on the Issues tab and you will get a reply, usually the same day. Requests for extra fields are welcome — tell me what you need.