Shopify Store Scraper — Product Catalogue & Store API
Pricing
from $1.00 / 1,000 products
Shopify Store Scraper — Product Catalogue & Store API
Scrape any Shopify store's full product catalogue (title, price, variants, stock, images, tags) plus a store summary (vendors, product types, price range) straight from the store's own public JSON endpoints. You only pay per product and per store summary delivered.
Pricing
from $1.00 / 1,000 products
Rating
0.0
(0)
Developer
Plainfetch Studio
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Point this at any Shopify store and get back its full product catalogue — title, price, variants, stock, images, tags — plus a store-level summary, without a browser and without touching a single private endpoint. Shopify Store Scraper is a Shopify Product API alternative built directly on the public JSON files every Shopify storefront exposes by design: give it a store URL and get one dataset item per product, plus one summary item per store, ready to filter, sort, or feed into another tool.
What can you do with it?
- Monitor a competitor's catalogue. Track a competitor's product list, prices, and stock status over time — run it on a schedule and diff the results.
- Track prices across many stores. Pull
priceMin/priceMaxfor every product across a list of stores to build a price-comparison dataset or spot a sale before it's announced. - Market research. Get a store's vendor list, product-type mix, and price range in one
store-summaryitem — no need to page through the storefront by hand. - Feed AI agents and downstream tools. Call this Actor from the Apify API, an integration, or an MCP-connected AI agent so it can look up what a store sells as one step in a larger workflow.
What data do you get?
Each store produces two kinds of dataset item, distinguished by type.
One product item per product:
| Field | Type | Description |
|---|---|---|
title | string | The product's title. |
store | string | The store's origin, e.g. "https://www.allbirds.com". |
vendor | string | null | The brand/manufacturer, as set by the store. |
productType | string | null | The store's own product-type label, e.g. "Shoes". |
tags | string[] | Normalized to an array either way the store serves them. |
priceMin / priceMax | number | null | Lowest/highest variant price. |
available | boolean | true if at least one variant is in stock. |
variantsCount | integer | How many variants the product has (always present). |
variants | array (optional) | Full variant list — size/color, price, stock, SKU. Omit with an input flag for a lighter dataset. |
images | string[] | Image URLs, straight from the store. |
url | string | The product page, https://host/products/<handle>. |
One store-summary item per store, built once its catalogue has been read:
| Field | Type | Description |
|---|---|---|
name | string | null | The store's display name, from its /meta.json. |
currency | string | null | The store's currency, e.g. "USD". |
productsScraped | integer | How many products were actually scraped for this store. |
vendors | string[] | Every distinct vendor seen among the scraped products. |
productTypes | string[] | Every distinct product type seen. |
priceMin/priceMax | number | null | The store's overall price range across scraped products. |
collections | array (optional) | Title, handle and product count of each collection. Present only when includeCollections is on — and then always present, even when empty. |
collectionsError | object | null | Ships beside collections: null when the list was read, otherwise why the list is empty (the read failed, or it was skipped). |
complete | boolean | true only if the catalogue was read to its natural end — an empty page, or a page with no new products. See below. |
incompleteReason | string | null | Why complete is false: "max-products-reached", "spending-cap-reached", or "interrupted". null when complete. |
error | object | null | Set only when incompleteReason is "interrupted" — the failure that cut the read short, or that stopped the store entirely. See Limitations. |
Every requested store gets exactly one store-summary row — never silently dropped. Even a
store skipped entirely, because your spending cap was already reached or because this Actor's own
circuit breaker had already tripped before it was ever attempted, still gets a row:
productsScraped: 0, complete: false, and incompleteReason naming why. Neither case is ever
charged.
What real output looks like
You can download the dataset in various formats such as JSON, HTML, CSV, or Excel. Here is real, unedited output from a run with
{"storeUrls": ["https://www.allbirds.com"], "maxProductsPerStore": 20}variants, images and tags arrays below are trimmed for readability (the real item
carries all 13 variants and 5 images):
[{"type": "product","store": "https://www.allbirds.com","id": 7218356060240,"title": "Men's Strider Explore - Natural Black (Dark Grey Sole)","handle": "mens-strider-explore","url": "https://www.allbirds.com/products/mens-strider-explore","vendor": "Allbirds","productType": "Shoes","tags": ["allbirds::edition => classic", "allbirds::gender => mens", "shoprunner"],"priceMin": 130,"priceMax": 130,"compareAtPriceMax": null,"currency": "USD","available": false,"variantsCount": 13,"variants": [{"id": 41334293889104,"title": "8","price": 130,"compareAtPrice": null,"available": false,"sku": "A11768M080"}],"images": ["https://cdn.shopify.com/s/files/1/1104/4168/files/A11768_25Q4_..._PDP_LEFT.png?v=1759336475"],"createdAt": "2025-09-10T12:47:47-07:00","updatedAt": "2026-09-20T02:13:47-07:00","publishedAt": "2026-08-26T10:22:14-07:00"},{"type": "store-summary","store": "https://www.allbirds.com","title": "Allbirds","name": "Allbirds","currency": "USD","country": "US","productsScraped": 20,"vendors": ["Allbirds", "re:do"],"productTypes": ["Shoes", "Socks", "return,package_protection"],"priceMin": 0.8,"priceMax": 160,"complete": false,"incompleteReason": "max-products-reached","error": null}]
The summary above has complete: false because maxProductsPerStore (20) was reached before the
store's real catalogue (294 published products) was fully read — it is still a genuinely delivered,
charged result: incompleteReason just tells you it isn't the whole store. A summary is only
complete: true once the catalogue was read all the way to an empty page.
How much does it cost?
You pay $0.001 per product delivered and $0.01 per store summary. The $0.01 store summary
is charged when two things are both true: at least one product was delivered for that store,
and the catalogue read was not cut short by an error. So a store with an empty catalogue gets
its summary for free, and a summary cut short by a block, a network error, a page too large to
read, this Actor's own page-safety limit, or the run's protection against sustained blocking is
delivered for visibility but never charged — and neither is a store that fails outright (not on
Shopify, blocked, or with products.json disabled). A summary truncated by your own
maxProductsPerStore is still an honest, delivered result and is charged, just labeled
complete: false. Once the spending limit you set for the run has been reached, nothing further is
charged at all — summaries included. Products already delivered before an interruption stay charged
either way. No start fee.
| Scraped | Cost |
|---|---|
| 1 store, 800 products | 800 × $0.001 + $0.01 = $0.81 |
| 10 stores, 200 products each | 2,000 × $0.001 + 10 × $0.01 = $2.10 |
| 5,000 products across 25 stores | 5,000 × $0.001 + 25 × $0.01 = $5.25 |
Real example: the run above (
{"storeUrls": ["https://www.allbirds.com"], "maxProductsPerStore": 20}Input
See the Input tab for the full configuration. The minimal input is one store URL:
{"storeUrls": ["https://www.allbirds.com"]}
storeUrls(required) — 1 to 500 Shopify store URLs. The bare domain also works;https://is added for you. Duplicate entries are processed once: the list is reduced to each distinct store first, sostore.com,https://store.com/andhttps://store.com/collections/allare one store, read and billed once, not three times.maxProductsPerStore(optional, default1000, 1 to 25,000) — how many products to scrape per store, newest-first page by newest-first page.includeVariants(optional, defaulttrue) — include each product's full variant list. Turn off for a lighter dataset —variantsCountand the price range are kept either way.includeCollections(optional, defaultfalse) — also read/collections.jsonand list the store's collections in its summary. When on, the summary always carries bothcollectionsandcollectionsError, so an empty list is never ambiguous.requestDelayMs(optional, default250) — courtesy pause before each request to the same store.
Limitations — read this first
This Actor requests a minimum of 1 GB of memory. A /products.json or /collections.json
page can run up to 12,000,000 characters before this Actor's own body budget cuts it off, and the
built-in per-run concurrency cap reads up to 5 stores at once — that combination needs more
headroom than a 256 MB floor reliably gives.
This Actor only works for a Shopify store that leaves its public /products.json endpoint enabled
— the vast majority do, since it's how Shopify's own storefront themes fetch product data, but a
store can disable it. A password-protected store, a store that isn't on Shopify, or one that has
turned this endpoint off produces one store-summary item with error.category: "parse" and the
message "Not a Shopify store or products.json is disabled" — and is never charged. A headless
storefront built entirely on the Storefront GraphQL API rather than Shopify's own Liquid theme may
not expose /products.json either, for the same reason. That message is only ever used for what it
says: a store whose page was simply too large to read within the request budget reports that
instead, so a healthy big catalogue is never mislabeled as a broken store.
Inventory quantities are not exposed by Shopify's public JSON — only available (in stock or
not) is. Getting an exact quantity would need the Admin API and a private access token, which this
Actor does not use.
At most 25,000 products per store (maxProductsPerStore's ceiling) and at most 250 products
per page, matching /products.json's own limits. A catalogue larger than maxProductsPerStore
still produces a charged summary — just with complete: false and
incompleteReason: "max-products-reached"Pagination is guarded against a store that makes no real progress — three layers. A product
the same store returns again on a later page is delivered and charged only once: product ids are
tracked per store and an exact repeat is dropped before it is ever pushed. A page made entirely of
products already seen is itself the natural end of that catalogue, exactly like an empty page —
this is what stops a host that ignores ?page (a CDN that strips the query string, a fixed
products.json) from serving page 1 forever. As a last-resort backstop beyond those two, no store
ever requests more than ⌈maxProductsPerStore ÷ 250⌉ + 5 pages; hitting that ceiling ends the
store complete: false, incompleteReason: "interrupted" with an explicit error, while every
product already delivered stays charged and the summary itself is not charged. None of these
guards applies across two different stores: the same product in two stores is two rows.
A block or a network error partway through a store's catalogue delivers a partial, uncharged
summary, not a silent gap: complete: false, incompleteReason: "interrupted", and error set
to what stopped it. Every product already scraped before that point stays delivered and charged.
Sites that block automated requests return error.category: "blocked" and are never charged for
the affected summary; three consecutive blocked or rate-limited responses (from any endpoint, on
any store) stop the whole run, with a clear message in the log — this Actor never attempts to get
around a block (no proxy rotation, no browser fingerprint spoofing, no CAPTCHA solving). A store
that never got to start at all, because your spending cap was already reached or the breaker had
already tripped, still leaves its own uncharged store-summary row rather than vanishing.
FAQ
Is this legal? We can't give you legal advice, but here is exactly what the Actor does: it reads
/products.json, /collections.json and /meta.json — endpoints Shopify publishes on every store
by design, the same data the storefront itself shows shoppers. It does not log in, does not get
around any block, and does not collect personal data (product data has none). You are responsible
for the store URLs you submit and for how you use the results.
Where does the data come from? Directly from each store's own domain — /products.json,
/collections.json and /meta.json — no third-party database, no cache.
How do I use this from the API or an AI agent (MCP)? Call it like any other Apify Actor: via the
Apify API, the JavaScript or Python client, an integration, or an
MCP-connected AI agent that has access to your Apify Actors. Pass the same storeUrls input and
read the results back from the default dataset.
What happens if I hit my spending cap? The run stops cleanly: it finishes the product or store
already being processed, writes everything it has, and reports chargeLimitReached: true in the run
summary. Nothing already produced is lost, and nothing beyond the cap is charged.
Changelog
0.1 — Initial release.