If you own a Shopify store, the admin has a product CSV export. If you want the catalog of a competitor, a supplier or a store you are about to migrate, you do not have that option. What most Shopify stores do have is a public products.json feed: the same JSON catalog Shopify serves to every visitor, 250 products per page.
Paging through it yourself is doable for one store, but handling robots.txt, pacing, collections, headless storefronts that do not serve the feed, and turning nested JSON into a clean table quickly adds up. The Shopify Store Scraper does that and returns one row per product with prices, compare-at prices, discount, stock, variants and images.
This guide walks through checking a store, choosing what to export, running the export and getting the result into CSV, JSON or pandas.
Step by step
Check that the store serves products.json
Open https://the-store-domain/products.json in a browser. If you see JSON with a products array, the store works. Headless storefronts built on Hydrogen/Oxygen or a custom frontend do not serve it, and a few large brands block automated requests; the actor returns a free row explaining why in those cases, so you can also just try it.
Add the store and choose the mode
Put the store domain or any URL from the store in
storeUrls; the actor takes the host, so a collection or product URL works too, as do myshopify.com domains. Keepmodeas products for one row per product. Use both if you also want a single store summary row with product counts, price stats, theme and detected apps.Decide on scope and row size
maxProductsPerStoredefaults to 1,000 and goes up to 100,000; raise it for large catalogs. Usecollectionswith handles such as mens-shoes to export only certain collections.includeVariantsis on by default and adds every variant with SKU, price, compare-at price, availability, options and weight. Turn onincludeBodyHtmlonly if you need product descriptions, because it makes rows much bigger (the price stays the same).{ "storeUrls": ["allbirds.com"], "mode": "products", "collections": ["mens"], "maxProductsPerStore": 5000, "includeVariants": true, "includeBodyHtml": true }Download the dataset as CSV or JSON
When the run finishes, its dataset can be downloaded from Apify Console or the API in CSV, JSON and other formats. Use the
fieldsparameter to choose a tidy set of columns for a spreadsheet, and request JSON when you want the nested variants and images kept intact.# Export the dataset of a finished run as CSV or JSON DATASET_ID="your-dataset-id" curl -s -H "Authorization: Bearer $APIFY_TOKEN" \ "https://api.apify.com/v2/datasets/$DATASET_ID/items?format=csv&clean=true&fields=store,title,vendor,productType,price,priceMax,compareAtPriceMin,discountPct,currency,available,variantCount,updatedAt,url" \ -o products.csv curl -s -H "Authorization: Bearer $APIFY_TOKEN" \ "https://api.apify.com/v2/datasets/$DATASET_ID/items?format=json&clean=true" \ -o products.jsonLoad it into pandas for a quick summary
For analysis, read the product rows into a DataFrame, write the columns you need to CSV and summarize per store. The dataset can mix product and store rows in both mode, so filter on
type.import os import pandas as pd from apify_client import ApifyClient client = ApifyClient(os.environ["APIFY_TOKEN"]) run = client.actor("actor-forge/shopify-store-scraper").call(run_input={ "storeUrls": ["allbirds.com", "tentree.com"], "mode": "products", "maxProductsPerStore": 1000, }) rows = list(client.dataset(run["defaultDatasetId"]).iterate_items()) products = pd.DataFrame([r for r in rows if r.get("type") == "product"]) cols = ["store", "title", "vendor", "productType", "price", "priceMax", "compareAtPriceMin", "discountPct", "currency", "available", "variantCount", "updatedAt", "url"] products[cols].to_csv("shopify_products.csv", index=False) # Quick per-store summary summary = products.groupby("store").agg( products=("title", "count"), medianPrice=("price", "median"), onSaleShare=("onSale", "mean"), inStockShare=("available", "mean"), ) print(summary)Keep the export fresh with updatedSince
For an ongoing price and stock feed, schedule the same input daily and set
updatedSinceto the previous day's date (YYYY-MM-DD) or an ISO timestamp. Only products whose updated_at is on or after that moment become rows, and skipped products are never charged, so you can merge each day's changes into your master export.
Sample input
{
"storeUrls": [
"allbirds.com"
],
"mode": "products",
"maxProductsPerStore": 1000,
"includeVariants": true,
"includeBodyHtml": false
}Sample output
Product rows come out like this, with price, compare-at price, discount and stock in the first columns:
| store | title | vendor | productType | price | compareAtPriceMin | discountPct | available | variantCount |
|---|---|---|---|---|---|---|---|---|
| www.allbirds.com | Women's Allbirds Flip Flop - Dusty Pink | Allbirds | Shoes | 25 | 50 | 50 | true | 7 |
| www.allbirds.com | Men's Canvas Runner NZ - Deep Navy Stripes (Blizzard Sole) | Allbirds | Shoes | 100 | true | 13 | ||
| www.tentree.com | Sasquatch Peak Hat | tentree | Accessories | 31.5 | 45 | 30 | true | 1 |
| www.deathwishcoffee.com | White Chocolate Pistachio Ground Bundle | Death Wish Coffee | Bundle | 48.99 | 53.99 | 9.3 | true | 1 |
Run it from code
cURL
curl -X POST "https://api.apify.com/v2/acts/actor-forge~shopify-store-scraper/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"storeUrls": ["allbirds.com"], "mode": "products", "maxProductsPerStore": 1000, "includeVariants": true, "includeBodyHtml": false}'JavaScript (Node.js)
// Node.js 18+ (built-in fetch). Set APIFY_TOKEN in your environment.
const input = {
"storeUrls": [
"allbirds.com"
],
"mode": "products",
"maxProductsPerStore": 1000,
"includeVariants": true,
"includeBodyHtml": false
};
const res = await fetch(
"https://api.apify.com/v2/acts/actor-forge~shopify-store-scraper/run-sync-get-dataset-items",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.APIFY_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify(input),
}
);
const items = await res.json();
console.log(items.length, items[0]);Python
# pip install apify-client (set APIFY_TOKEN in your environment)
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run_input = {
"storeUrls": [
"allbirds.com"
],
"mode": "products",
"maxProductsPerStore": 1000,
"includeVariants": True,
"includeBodyHtml": False
}
run = client.actor("actor-forge/shopify-store-scraper").call(run_input=run_input)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
print(item)The run-sync-get-dataset-items endpoint waits for the run (up to 5 minutes) and returns the dataset rows directly. For longer runs, start the run asynchronously and read the dataset when it finishes, as the Python example does. Replace APIFY_TOKEN with your own token from the Apify console.
What it costs
Product rows cost $0.0003 each, so exporting a 1,000-product store is $0.30 and three such stores are $0.90. In both mode, each store row adds $0.005, so the same three stores with summaries cost $0.915.
| Charged for | When | Price | Per 1,000 | Worked example |
|---|---|---|---|---|
| Product result | Charged per product row delivered (with variants and images) | $0.0003 | $0.30 | A 1,000-product store = $0.30; with updatedSince, unchanged products are skipped and cost nothing |
| Store analyzed | Charged per store row in store/both mode (meta, price stats, top vendors/types, collections, theme, apps) | $0.005 | $5 | 100 competitor stores = $0.50; blocked, headless, robots-disallowed and non-Shopify sites are free rows |
| Actor Start (platform start fee) | Charged when the Actor starts running. Number of events charged depends on Actor memory (one event per GB, minimum one event). | $0.00005 | — | One run at 256 MB = $0.0000125 |
Ready to try it? Shopify Store Scraper — Products, Prices, Store Info runs on Apify, pay per result.
Run on Apify → Read the full actor pageTips and pitfalls
- products.json only lists published products. Drafts, archived items and inventory quantities are not public, and prices are the store's base-currency prices, not market-specific or B2B prices.
- In both mode, check
catalogCompleteon the store row: it tells you whether the stats cover the whole catalog or were cut off bymaxProductsPerStore. - The actor obeys each store's robots.txt and Crawl-delay and runs about 1.3 requests per second per store, so very large catalogs take a while. Several stores run in parallel, up to 3 at a time.
- A 401, 403, 429 or bot challenge stops that store immediately with no retries. Those rows are free, but the store's products will be missing from the export.
FAQ
Can I export products from a Shopify store I don't own?
Yes, as long as the store serves Shopify's public products.json feed and its robots.txt allows it. The actor reads only that public catalog data and no customer or contact data.
Does the export include product images?
Yes. Each product row includes every image URL plus a featured image. The files themselves are not downloaded; you get the URLs.
Why does my CSV have fewer products than the store's website shows?
Common reasons are the maxProductsPerStore cap (default 1,000), a collections filter, an updatedSince filter, or products that are not published. Raise the cap and clear the filters for a full export.
Can I export several stores in one run?
Yes. Add them all to storeUrls; duplicates are removed and each row carries a store column, so one CSV can hold several catalogs.
Actors used in this guide
Shopify Store Scraper — Products, Prices, Store Info
Scrape any Shopify store's products from its public products.json: title, vendor, type, tags, price range, compare-at price, stock, variants and…
$0.30 per 1,000 · product resultRelated guides
How to Monitor Competitor Ads in Google Ads Transparency Center
List your competitors' domains or advertiser names, run the ActorForge Ads Transparency actor daily with only-new mode, and route the new ads into…
How to Audit Every URL in Your Sitemap and Find Orphan Pages
Run the SEO Audit Crawler with useSitemap on and maxPages above your site's size; it audits every page it can reach plus every sitemap URL, and…