Shopify Product Scraper — GTIN & Collections
Pricing
from $1.40 / 1,000 products
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
6 days ago
Last modified
Categories
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.83deathwishcoffee.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 | |
|---|---|
barcode | GTIN/UPC/EAN — match to Amazon, Google Shopping, retail |
selling_plan_groups | Subscription offers, with the real subscriber price |
quantity_price_breaks | Wholesale volume tiers |
quantity_rule | Order minimums, maximums and pack increments |
inventory_management | Whether stock is tracked at all |
weight, media, price_varies | Shipping 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 MarkdownOn 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.83rothys.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
| Option | Default | What it does |
|---|---|---|
storeUrls | — | Required. Domains, URLs or product links, mixed freely |
enrich | true | Read barcodes, subscriptions and wholesale. One extra request per product. Turn off for a fast catalog dump |
rowsPer | variant | variant for feeds and price monitoring, product for catalog analysis |
maxProductsPerStore | 1000 | How deep to read. The main lever on run time and cost |
collectionsMode | key | key reads only signal collections, all up to 40, none skips them |
requireBarcode | false | Keep only rows with a GTIN — the feed-building filter |
onlyInStock | false | Keep only available variants |
onlyOnSale | false | Keep only discounted variants |
requireSubscription | false | Keep 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 |
includeDescription | false | Adds the full description. Large |
maxConcurrency | 5 | Parallel 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".
Is this legal?
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
error | Meaning |
|---|---|
not_a_shopify_store | No public catalog at /products.json |
catalog_blocked | The store returned 403 or 429 |
catalog_empty | Catalog exists but has no products |
all_products_filtered_out | Read fine, but your filters matched nothing |
invalid_input | Couldn'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
- Open the Actor and paste store domains into Shopify stores.
- Leave enrich on for barcodes. Set requireBarcode if you are building a merchant feed, or requireSubscription to find subscription products.
- Click Start. 800 products take under a minute.
- Open the Merchant feed, Subscriptions or Wholesale view, or export as CSV, Excel or JSON.
- 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.
| Plan | per 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
- Shopify Stores API — Velocity, Pricing & Brand Model — one row per store: launch velocity measured properly, price bands in USD, discount depth, brand model, and the apps the storefront runs
- Shopify Scraper — Stores & Products — both of the above in one run — the store summary plus the full catalog
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.