Shopify Scraper — Stores & Products
Pricing
from $1.40 / 1,000 products
Shopify Scraper — Stores & Products
Shopify store scraper and Shopify products scraper in one run: every product and variant with GTIN barcodes, plus store-level launch velocity, price positioning, discounting, brand model and the apps the store runs.
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
Point it at a Shopify store. Get every product and variant with GTIN barcodes, and a read on the business itself — how fast it ships, where it prices, how hard it discounts, and which apps it pays for.
store deathwishcoffee.com 0.03 new/day median $40 dormant · 8 apps · Dawn themeproduct Coconut Caramel Coffee / 1 bag 810063344313 $13.99 subscribe & save 10%product Valhalla Java / 5 lb bag 810063342159 $89.99 in stock, tracked
Most Shopify scrapers give you rows of products. This gives you the catalog and what the catalog says about the merchant, from the same read.
Two records, one run
Set output and you get either or both:
The store record — one row per store. Launch velocity measured properly, price bands in USD, discount depth, stock pressure, brand model, catalog scale, and the apps and theme the storefront loads.
The product records — one row per variant (or per product), each with the GTIN barcode, price, stock, subscription plan and collection membership.
Use store alone for a fast, cheap scan of a hundred competitors. Use
products for a pure catalog extract. Use both when you want the business and
its catalog together.
The barcode
/products.json — the endpoint nearly every Shopify scraper reads — 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 product titles and manual matching.
It lives one layer down, at /products/{handle}.js, which costs one request per
product. 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%
The same endpoint carries four more things /products.json lacks:
subscription plans with the real subscriber price, wholesale volume
breaks, order minimums, and inventory_management — which decides
whether "in stock" means anything, because when a merchant does not track
inventory available is a default rather than a fact. inventoryTracked tells
you which kind you are looking at.
Velocity that is actually right
The store record reports how fast the store ships new product. Getting that right took three attempts, because the obvious methods are all wrong.
published_at looks like a launch date and is not — it is rewritten in bulk
whenever a store replatforms. Allbirds spans 7.6 years by created_at and 83
days by published_at. Deriving a rate from it is wrong in both directions at
once:
from published_at measured properlyallbirds.com 3.54/day 0.00/day overstated without boundgymshark.com 12.39/day 0.00/day overstated without boundfinisterre.com 0.28/day 1.86/day 6.6x too lowrothys.com 0.22/day 0.81/day 3.7x too low
So both rates are reported: perDay counts products genuinely created in the
last 90 days, publishPerDay counts what was published. The gap is the
finding — Gymshark has created nothing since May while publishing sixteen a day,
which is re-merchandising, not dying, and reads as republishing.
And it says when it does not know. On a store too large to read in full,
velocityConfidence comes back low and the number is a lower bound rather
than a wrong answer. It fires only when it should: Organic Basics reads 1.76/day
from 250 products against 1.77/day from its full 1,816.
Which apps the store pays for
The catalog says what a store sells. The storefront says how it operates.
deathwishcoffee.com 8 apps Klaviyo · Postscript · Rebuy · Smile.io · Afterpay Dawnrothys.com 2 apps Intelligems · Yotpo DNA
Over 50 apps recognised across email/SMS, reviews, support, subscriptions, CRO,
loyalty, search, analytics, post-purchase, payments and affiliate. Anything
third-party that cannot be named is still reported in otherThirdPartyHosts, so
a store running something unusual shows up rather than looking simpler than it
is. themeIsStock flags the free Shopify themes — a store still on stock Dawn
has not invested in design.
What the merchant says is selling
/collections.json is the store's own merchandising, and it is public. A
product in the merchant's best-sellers collection is a demand signal the
store is publishing itself.
Reading all of it is wasteful — Rothy's has 196 collections, Allbirds 1,000, mostly colour and size groupings. So collections are ranked, not matched: best sellers first, then new arrivals, trending, sale, each capped so one kind cannot crowd out another, with internal plumbing (
Discount Eligible Products Excluding MarkdownPrices you can compare
products.json carries prices as bare numbers with no currency in it. A UK
store showing 50 and a US store showing 50 are £50 and $50. Currency is read
from the store profile and every price reported twice, in store currency and
USD. When no rate is available the USD fields are null, never a guess.
A trap worth naming: /products.json gives decimal strings while
/products/{handle}.js gives integer cents. Mixing them divides enriched
prices by a hundred. Each source is normalised on the way in.
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 merge so you are never billed twice for one store.
Recipes
Scan 100 competitors fast — one row each, no per-product cost
{ "storeUrls": ["..."], "output": "store" }
Build a Google Merchant feed
{ "storeUrls": ["rothys.com"], "output": "products", "requireBarcode": true }
feedReady says whether a row has everything Merchant Center needs — id, title,
link, image, brand, price, availability and GTIN.
Every subscription product and what subscribing saves
{ "storeUrls": ["deathwishcoffee.com"], "requireSubscription": true }
Everything discounted under $50 across rivals
{ "storeUrls": ["a.com", "b.com"], "onlyOnSale": true, "maxPrice": 50 }
Every option
| Option | Default | What it does |
|---|---|---|
storeUrls | — | Required. Domains, URLs or product links |
output | both | both, products, or store for a fast one-row scan |
enrich | true | Barcodes, subscriptions, wholesale. One request per product |
rowsPer | variant | variant for feeds and price monitoring, product for analysis |
detectApps | true | Apps and theme on the store record. One request per store |
maxProductsPerStore | 1000 | Read depth — the main lever on time and cost |
collectionsMode | key | key reads signal collections only, all, or none |
requireBarcode | false | Keep only rows with a GTIN |
onlyInStock / onlyOnSale | false | Stock and discount filters |
requireSubscription | false | Subscription products only |
minPrice / maxPrice | — | Compared in USD, so consistent across stores |
vendors / productTypes / tags | — | Filter by brand, type or tag |
includeDescription | false | Adds full descriptions. Large |
maxConcurrency | 5 | Parallel requests. Lower it if a store returns 429 |
What it covers, honestly
About five stores in six work. Of 18 real stores tested, 15 served the
catalog. The rest return not_a_shopify_store. A miss is always reported.
Barcode coverage is the merchant's choice. Six of eight stores returned
97–100%. Rothy's returned 81% — its older products were never given one — and
Death Wish Coffee 26%, because they have not entered them. A null barcode means
the merchant left it empty; if the store throttled the run instead, the log says
so and RUN_SUMMARY counts the rows affected.
catalogSize is exact only when catalogComplete is true. Otherwise it is
null rather than a number that would be wrong.
App detection reads what the storefront loads. A retailer that self-hosts its scripts shows fewer apps than it runs — Gymshark detects only one — so a low count on a big brand means "not visible", not "not installed".
Currency is what the store serves you. Shopify switches storefront by geography.
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".
Prices and stock are a snapshot, true at the moment of the run.
Is this legal?
Everything read is a public endpoint Shopify serves by design, requiring no
authentication and permitted by robots.txt: /products.json,
/products/{handle}.js, /collections.json and /meta.json.
What this Actor deliberately does not do: /cart/add.js reveals 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 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. 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 scrape a Shopify store? Pass the domain. It reads the public catalog every Shopify store serves and returns the products plus a summary of the store itself.
Is this a Shopify store scraper or a Shopify products scraper?
Both, from one read. As a Shopify store scraper it returns one summary row per
store — launch velocity, pricing, discounting, brand model and installed apps.
As a Shopify products scraper it returns every product and variant with its GTIN
barcode, price, stock and collections. Set output to get either on its own.
How do I get GTIN or UPC codes from Shopify?
Leave enrich on. Barcodes come from /products/{handle}.js, which
/products.json does not include.
How is this different from other Shopify scrapers? Most read one endpoint and return titles, prices and images. This adds barcodes, subscription plans with real subscriber prices, wholesale breaks, whether stock is actually tracked, the merchant's own best-seller lists, the apps the store runs, and a correct launch-velocity measurement.
Can I scrape every Shopify store at once? No. You supply the store list. It reads the stores you name, thoroughly.
Will it get me blocked?
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.
- Choose What to return —
both,productsfor a catalog extract, orstorefor a fast one-row-per-store scan. - Click Start. Two stores with 800 products take about 45 seconds.
- Open the Stores view for the business read, or Merchant feed / Pricing / Subscriptions for the products. Filter on
recordTypewhen exporting. - Schedule it to track competitors over time, or call it through the API and integrations.
How much does it cost?
Pay per event. Two things are charged: one store summary per store, and one charge per product — a product with 20 variants is 20 rows and one charge.
| Plan | store summary | product, enriched | product, catalog only |
|---|---|---|---|
| Free | $0.02 | $0.002 | $0.0005 |
| Bronze | $0.018 | $0.0018 | $0.00045 |
| Silver | $0.016 | $0.0016 | $0.0004 |
| Gold | $0.014 | $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
output: "store"on 100 competitors → 100 × $0.02 = $2.00, no per-product cost at all- 3 stores, 2,000 products, both records → 3 × $0.02 + 2,000 × $0.002 = $4.06
- Rothy's + Death Wish at the defaults (812 products, 2 summaries) → $1.66
- The free plan's $5 credit covers about 2,400 enriched products
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 Product Scraper — GTIN & Collections — every product and variant with GTIN barcodes, subscription plans, wholesale breaks and best-seller membership
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.