Shopify Store Scraper — Product Catalogue & Store API avatar

Shopify Store Scraper — Product Catalogue & Store API

Pricing

from $1.00 / 1,000 products

Go to Apify Store
Shopify Store Scraper — Product Catalogue & Store API

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

Plainfetch Studio

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

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/priceMax for 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-summary item — 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:

FieldTypeDescription
titlestringThe product's title.
storestringThe store's origin, e.g. "https://www.allbirds.com".
vendorstring | nullThe brand/manufacturer, as set by the store.
productTypestring | nullThe store's own product-type label, e.g. "Shoes".
tagsstring[]Normalized to an array either way the store serves them.
priceMin / priceMaxnumber | nullLowest/highest variant price.
availablebooleantrue if at least one variant is in stock.
variantsCountintegerHow many variants the product has (always present).
variantsarray (optional)Full variant list — size/color, price, stock, SKU. Omit with an input flag for a lighter dataset.
imagesstring[]Image URLs, straight from the store.
urlstringThe product page, https://host/products/<handle>.

One store-summary item per store, built once its catalogue has been read:

FieldTypeDescription
namestring | nullThe store's display name, from its /meta.json.
currencystring | nullThe store's currency, e.g. "USD".
productsScrapedintegerHow many products were actually scraped for this store.
vendorsstring[]Every distinct vendor seen among the scraped products.
productTypesstring[]Every distinct product type seen.
priceMin/priceMaxnumber | nullThe store's overall price range across scraped products.
collectionsarray (optional)Title, handle and product count of each collection. Present only when includeCollections is on — and then always present, even when empty.
collectionsErrorobject | nullShips beside collections: null when the list was read, otherwise why the list is empty (the read failed, or it was skipped).
completebooleantrue only if the catalogue was read to its natural end — an empty page, or a page with no new products. See below.
incompleteReasonstring | nullWhy complete is false: "max-products-reached", "spending-cap-reached", or "interrupted". null when complete.
errorobject | nullSet 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}
— the 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.

ScrapedCost
1 store, 800 products800 × $0.001 + $0.01 = $0.81
10 stores, 200 products each2,000 × $0.001 + 10 × $0.01 = $2.10
5,000 products across 25 stores5,000 × $0.001 + 25 × $0.01 = $5.25

Real example: the run above (

{"storeUrls": ["https://www.allbirds.com"], "maxProductsPerStore": 20}
) delivered 20 products and 1 store summary, for $0.03.

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, so store.com, https://store.com/ and https://store.com/collections/all are one store, read and billed once, not three times.
  • maxProductsPerStore (optional, default 1000, 1 to 25,000) — how many products to scrape per store, newest-first page by newest-first page.
  • includeVariants (optional, default true) — include each product's full variant list. Turn off for a lighter dataset — variantsCount and the price range are kept either way.
  • includeCollections (optional, default false) — also read /collections.json and list the store's collections in its summary. When on, the summary always carries both collections and collectionsError, so an empty list is never ambiguous.
  • requestDelayMs (optional, default 250) — 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"
, so you always know it isn't the whole store.

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.