Shopify Store Scraper: Products, Variants & Inventory avatar

Shopify Store Scraper: Products, Variants & Inventory

Pricing

from $2.00 / 1,000 record scrapeds

Go to Apify Store
Shopify Store Scraper: Products, Variants & Inventory

Shopify Store Scraper: Products, Variants & Inventory

Scrape any Shopify store's complete catalogue through the public /products.json endpoint: titles, variants, prices, SKUs, images, tags and availability. Millions of stores, no key, no browser.

Pricing

from $2.00 / 1,000 record scrapeds

Rating

0.0

(0)

Developer

Arman Hossain

Arman Hossain

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

4 hours ago

Last modified

Share

Shopify Store Scraper: Products with variants, price ranges and stock, from any Shopify storefront

Shopify Store Scraper reads any Shopify storefront's complete catalogue through the public /products.json endpoint, titles, variants, prices, compare-at prices, SKUs, images, tags, product types and stock flags.

Every Shopify store publishes this data. It is the same catalogue the storefront's own theme renders from, so there is no browser, no proxy, no login and no credentials to manage. A 291-product catalogue comes back in seconds.

Agent skill: SKILL.md

https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/shopify-store-scraper.md

What you get

Output fieldMeaning
store, storeNameDomain you passed, and the shop name from the store's own record
productId, title, handleShopify product ID, display title, and the URL slug
vendor, productType, tagsBrand, Shopify's product type, and the full tag list as an array
priceMin, priceMax, currencyCheapest and dearest variant, in the store's own currency
availableTrue when at least one variant is purchasable
variantCount, variantsVariant count, and the nested list with price, compareAtPrice, sku, available, options
images, featuredImageEvery product image URL, and the first one
bodyPlainProduct description as clean plain text, HTML stripped, entities decoded
publishedAt, updatedAt, urlPosting timestamps and the direct product URL
scrapedAtRun timestamp

A RUN_SUMMARY record in the key-value store holds per-run counts, the filters used, and any store that refused.

Common use cases

  • Competitor price and assortment monitoring. Daily snapshot per SKU, diff on price and available.
  • Dropshipping and sourcing research. Filter thousands of catalogues by productType, vendor and price band.
  • New-product launch tracking. Watch publishedAt across a set of brands.
  • Lead qualification. Run detectOnly over a prospect list to find which domains are Shopify and how big their catalogue is.

Quick start

Two stores, first 250 products each:

{
"stores": ["allbirds.com", "kith.com"]
}

Price monitoring, one row per size/colour, ready to join on SKU:

{
"stores": ["allbirds.com"],
"outputMode": "variant",
"collectionHandles": ["mens"],
"maxProductsPerStore": 0
}

Qualify a prospect list without paying to scrape it:

{
"stores": ["allbirds.com", "gymshark.com", "example.com"],
"detectOnly": true
}

Input

FieldTypeDefaultNotes
storesarray-Required. Bare domains, full URLs, or .myshopify.com hosts. Everything after the host is stripped, so pasting a collection URL is fine.
collectionHandlesarray[]Restrict to collections. The handle is the slug after /collections/. Full URLs accepted. Empty = whole catalogue.
maxProductsPerStoreinteger250Cap on products read per store. 0 = everything Shopify will serve.
outputModestringproductproduct = one row per product, variants nested. variant = one flat row per variant.
includeVariantsbooleantrueEmbed the variant list in product rows. Ignored in variant mode.
minPricenumber0Drop cheaper items, in the store's own currency. No FX conversion.
detectOnlybooleanfalseReport Shopify-or-not per domain and stop. Two requests per domain, no product rows.

Which combinations make sense

  • outputMode: "variant" with maxProductsPerStore, the cap counts products, not rows. A 250-product apparel catalogue is routinely 3,000+ variant rows, and each one is a billed event.
  • collectionHandles plus maxProductsPerStore, the cap is per store, not per collection, and collections are read in the order given.
  • includeVariants: false in product mode, the fastest, narrowest shape. You keep priceMin, priceMax, variantCount and available, which is enough for assortment tracking.
  • detectOnly ignores every other filter.

Output example

A real product record from a live run against allbirds.com (variant list abridged to two of thirteen):

{
"store": "allbirds.com",
"storeName": "Allbirds",
"productId": 7292464955472,
"title": "Men's Cruiser - Shadow Blue (Natural White Sole)",
"handle": "mens-cruiser-shadow-blue-natural-white-sole",
"vendor": "Allbirds",
"productType": "Shoes",
"tags": ["DNAM BRANDS", "EC STOCK"],
"currency": "USD",
"url": "https://allbirds.com/products/mens-cruiser-shadow-blue-natural-white-sole",
"variantCount": 13,
"priceMin": 105,
"priceMax": 105,
"available": true,
"variants": [
{
"id": 41990816759888,
"title": "8",
"sku": "A12856M080",
"price": 105,
"compareAtPrice": null,
"available": true,
"position": 1,
"options": ["8"],
"grams": 1000,
"requiresShipping": true,
"taxable": true,
"updatedAt": "2026-08-06T04:38:01-07:00"
},
{
"id": 41990816923728,
"title": "10.5",
"sku": "A12856M105",
"price": 105,
"compareAtPrice": null,
"available": false,
"position": 6,
"options": ["10.5"],
"grams": 1000,
"requiresShipping": true,
"taxable": true,
"updatedAt": "2026-08-06T04:38:01-07:00"
}
],
"images": ["https://cdn.shopify.com/s/files/1/1104/4168/files/All-birds_0010.png?v=1783535519"],
"featuredImage": "https://cdn.shopify.com/s/files/1/1104/4168/files/All-birds_0010.png?v=1783535519",
"bodyPlain": "Inspired by a classic court style, this signature shoe delivers the right balance of style and comfort…",
"publishedAt": "2026-07-08T11:32:23-07:00",
"updatedAt": "2026-08-06T04:32:26-07:00",
"scrapedAt": "2026-08-06T11:37:56.404Z"
}

In detectOnly mode you get one row per domain instead:

{
"store": "gymshark.com",
"isShopify": true,
"productsEndpointOpen": true,
"storeName": "Gymshark US",
"myshopifyDomain": "gymshark.myshopify.com",
"currency": "USD",
"country": "GB",
"publishedProductsCount": 9301,
"publishedCollectionsCount": 1723,
"detail": "shop record found",
"scrapedAt": "2026-08-06T11:39:12.008Z"
}

RUN_SUMMARY

{
"storesRequested": 4,
"storesFailed": 2,
"failures": [
{ "store": "shop.tesla.com", "page": 1, "error": "blocked (403), this storefront sits behind a bot wall (CDN or headless front end) and refuses the public endpoint" },
{ "store": "example.com", "page": 1, "error": "endpoint disabled or not a Shopify storefront (404)" }
],
"requestsMade": 8,
"productsListed": 389,
"recordsSaved": 10,
"filters": { "collectionHandles": [], "maxProductsPerStore": 5, "minPrice": 0, "outputMode": "product", "includeVariants": true, "detectOnly": false },
"finishedAt": "2026-08-06T11:38:03.780Z"
}

productsListed vs recordsSaved separates "the store is small" from "my filters were too tight".

How to tell whether a domain is Shopify

Set detectOnly: true and the Actor answers it for you: it returns the shop record with myshopify_domain, currency, country and the published product and collection counts. Domains that are not Shopify storefronts are reported as such rather than guessed at.

Two independent signals matter, and they can disagree:

  • isShopify: the domain is a Shopify storefront.
  • productsEndpointOpen: its catalogue JSON is actually readable. Some brands run a headless front end (Vercel, Next.js) or an enterprise CDN in front of Shopify; those answer /products.json with 403, 404 or 429 even though the store is Shopify.

Manually, the fastest check is curl -sI https://{domain}/products.json | grep -i powered-by, a genuine Shopify origin answers powered-by: Shopify.

Finding a collection handle

Open the collection page and read the URL: allbirds.com/collections/mensmens. You can paste the whole URL, the Actor extracts the handle. An unknown handle is not an error, Shopify serves an empty page for it, so the Actor logs a warning rather than failing the store.

API example

curl -X POST "https://api.apify.com/v2/acts/arman-bd~shopify-store-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"stores": ["allbirds.com"],
"outputMode": "variant",
"maxProductsPerStore": 50
}'

JavaScript example

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('arman-bd/shopify-store-scraper').call({
stores: ['allbirds.com', 'kith.com'],
outputMode: 'variant',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const v of items) {
console.log(`${v.store} ${v.sku}, ${v.price} ${v.currency} ${v.available ? '' : '(out of stock)'}`);
}

Limits and behaviour

  • Pagination is ?page=N&limit=250. 250 is Shopify's hard maximum for limit; asking for 300 silently returns 250. An empty products array marks the end. There is no cursor and no total count in the response.
  • 25,000 products is Shopify's ceiling. page * limit above 25,000 returns HTTP 400 ("Page * Limit exceeds the 25000 limit."), so 100 pages is the most any store will serve through this endpoint.
  • Not every store leaves it open. Of eight well-known brands probed, five answered 200 and three refused: 403 behind Akamai, 404 and 429 behind a Vercel-hosted headless front end. Refusals are per-store failures with a message that names the cause; the run continues.
  • There is no stock quantity here. The public endpoint exposes available as a boolean and nothing more, no inventory_quantity, no barcode, no inventory_management. Those exist only in the authenticated Admin API. This Actor does not invent them.
  • Prices are unconverted. Each store reports in its own currency, as published by the store itself. minPrice compares raw numbers, so mixing a EUR and a USD store in one run with a price floor will not do what you want.
  • Transient errors are retried. 429 and 5xx get three attempts with backoff, honouring Retry-After. 400/403/404 and non-JSON responses are fatal for that store and not retried.
  • Requests are paced 300 ms apart. 40 back-to-back requests drew no throttling, so this is courtesy rather than necessity.
  • Public data only. No authentication, no personal data, no access-control bypass.

FAQ

Do I need a proxy? No. Proxy configuration is not required to run this Actor.

Do I need a Shopify account or API key? No. You supply no credentials.

Why does one of my stores return nothing? Three possibilities, and RUN_SUMMARY distinguishes them: the domain is not Shopify (404/non-JSON). It is Shopify but fronted by a CDN that blocks the endpoint (403/429), or your collectionHandles / minPrice filtered everything out. Run with detectOnly: true to settle the first two in one cheap pass.

Can I get stock levels? Only available (true/false) per variant. Shopify does not publish quantities on the storefront endpoint.

Product rows or variant rows? Product rows for catalogue snapshots and assortment analysis; variant rows for price monitoring, since each size and colour has its own price, SKU and stock flag. Switch with outputMode.

Does www. matter? Sometimes. Apex and www. hosts occasionally sit behind different edges, gymshark.com answers 200 while www.gymshark.com answers 403. If one form is refused, try the other.

Can I schedule it? Yes, it is designed for scheduled runs. A daily variant-level sweep of one 250-product store is two requests.

Can I integrate it with something else? Yes, Apify API, client libraries, webhooks, scheduled runs, dataset exports (JSON/CSV/Excel) or MCP. Output is structured JSON.