Almost every Shopify store publishes its whole public catalog as JSON. There's no API key, no app and no scraping of HTML. Add /products.json to the store's domain:
https://example-store.com/products.json
That returns the store's products, with titles, descriptions, prices, variants, SKUs, stock flags and image URLs. It's handy for competitor price checks, catalog audits, building a comparison site, or simply exporting your own store to a spreadsheet.
Here's how it works, how to page through big catalogs, and a few things to watch for.
What you get back
Each request returns {"products": [...]}. A trimmed product looks like this:
{
"id": 7340901859408,
"title": "Women's Flip Flop - Dusty Pink",
"handle": "womens-flip-flop-dusty-pink",
"body_html": "<p>Sun on your feet. Comfort underneath...</p>",
"vendor": "Example Brand",
"product_type": "Shoes",
"tags": ["womens", "summer"],
"published_at": "2026-09-25T16:58:13-07:00",
"updated_at": "2026-10-14T14:56:02-07:00",
"variants": [
{
"id": 42146889039952,
"title": "7",
"sku": "A12513W070",
"price": "25.00",
"compare_at_price": "50.00",
"available": false
}
],
"images": [{"src": "https://cdn.shopify.com/...", "alt": null}]
}
A few useful details:
-
priceandcompare_at_priceare strings. Acompare_at_priceabovepricemeans the item is on sale. -
availableon each variant tells you whether that size or colour is in stock. You don't get stock counts. -
handlebuilds the product URL:https://example-store.com/products/<handle>. -
images[].altis the image's alt text.nullmeans none was set, which matters for accessibility and SEO.
Paging through the whole catalog
The default page holds 30 products. Ask for up to 250 per page and step through pages until one comes back empty:
import requests, csv
store = "https://example-store.com"
rows, page = [], 1
while True:
r = requests.get(f"{store}/products.json", params={"limit": 250, "page": page}, timeout=30)
products = r.json().get("products", [])
if not products:
break
for p in products:
for v in p["variants"]:
rows.append({
"title": p["title"], "variant": v["title"], "sku": v.get("sku"),
"price": v["price"], "compare_at": v.get("compare_at_price"),
"available": v.get("available"),
"url": f"{store}/products/{p['handle']}",
})
page += 1
with open("catalog.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=rows[0].keys())
w.writeheader(); w.writerows(rows)
print(len(rows), "variants saved")
Shopify sends Access-Control-Allow-Origin: * on this endpoint, so the same request works from browser JavaScript too:
const res = await fetch("https://example-store.com/products.json?limit=250&page=1");
const { products } = await res.json();
console.log(products.length, products[0].title);
One collection only
Every collection has its own feed:
https://example-store.com/collections/<collection-handle>/products.json
Things to watch for
- Some stores turn it off or sit behind a password page. Then you'll get a 404 or HTML instead of JSON.
- Be polite. Space requests a second or two apart. Shopify rate-limits heavy traffic, and a catalog of a few thousand products only needs a handful of requests anyway.
- It's public data only. No costs, no stock counts, no unpublished products.
- Prices are in the store's currency. Multi-currency stores may show a different currency depending on where you request from.
Free tools if you'd rather not write code
We put three small browser tools on Hugging Face that use this same feed:
- Shopify product exporter: paste a store URL and download its catalog as a CSV.
- Product description checker: flags products with thin, missing or duplicated descriptions.
- Image alt-text checker: lists product images with no alt text, a quick accessibility and SEO win.
If you'd like a store's prices tracked daily, with alerts when something changes or goes on sale, email siftwright.data@gmail.com and we'll set it up for you.
Top comments (0)