L logiover
ecommerce · May 24, 2026 · 5 min read

How to Scrape Shopify Product Catalogs and Prices in 2026

A practical guide to extracting full product catalogs, variants and pricing from any Shopify store using the public /products.json endpoint — no browser, no API key.

Every Shopify store on the planet exposes its entire public product catalog through a single, undocumented-but-stable JSON endpoint. No login, no API token, no browser automation, no anti-bot wall. If you do competitive intelligence, price tracking, or dropshipping research, this is the cheapest, fastest data source in e-commerce — once you know the endpoint exists and how to traverse it. This guide covers exactly that, plus how to turn the raw JSON into a clean catalog you can diff over time.

The endpoint that makes this trivial

Shopify powers millions of stores on one platform, and that platform ships a consistent public route on every store:

https://store-domain.com/products.json?limit=250&page=1

That’s it. Hit it and you get back a JSON array of products — the same data the storefront renders, structured and paginated. Because it’s a direct JSON fetch, there’s no browser to launch, which is the entire performance story: a headless-browser scraper spends 90% of its compute rendering pages you don’t need. Hitting /products.json with a raw HTTP client is an order of magnitude cheaper and faster.

Two endpoint details matter:

  • limit caps at 250 products per page. Above that you paginate with page=2, page=3, until you get an empty array.
  • Some stores disable it. A small fraction of merchants block /products.json or proxy it through a custom route. For those, you fall back to the collection or sitemap routes — but the vast majority of Shopify stores leave it wide open.

There’s also a collection-scoped variant — /collections/<handle>/products.json — if you only want one category rather than the whole catalog.

What each product record contains

The payload is rich. Per product you get:

  • Identity — product ID, title, handle (the URL slug), vendor, product_type, tags.
  • Variants — an array, each with its own variant ID, title (e.g. “Large / Blue”), price, compare_at_price (the strikethrough price), sku, and available flag.
  • Pricing range — derived min/max across variants, plus currency.
  • Media — image URLs and alt text.
  • Timestampscreated_at, updated_at, published_at.

The updated_at field is the secret weapon for incremental scraping: instead of re-pulling the whole catalog every run, you fetch only products updated since your last run, which keeps cost and noise down.

Run the Shopify Competitor Spy — pulls full product catalogs, variants, price ranges, inventory status and images from any Shopify store via direct JSON. No browser, low compute cost, built for bulk store extraction.

Why no-browser matters for cost

This actor’s whole design thesis is no headless browser. Here’s why that’s not just a nice-to-have:

  • A browser-based scraper allocates ~1–2 GB of memory per worker and burns CPU rendering React, fonts, and images you’ll never use. A raw HTTP fetch of /products.json needs a fraction of that.
  • On Apify, compute is metered. Less memory and less time per request means dramatically lower compute-unit (CU) cost per product — the difference between cents and dollars per thousand products.
  • No browser means no headless-Chrome fingerprinting surface, so you rarely need residential proxies for the catalog endpoint. Datacenter IPs work fine for most stores.

The trade-off: you only get what /products.json exposes. It won’t render dynamic “you may also like” widgets or app-injected content. For catalog and pricing intelligence, that’s exactly what you want anyway.

Schema design for downstream use

Flatten the nested variant structure into rows you can diff:

{
  "store_domain": "examplebrand.com",
  "product_id": 7345123456789,
  "title": "Merino Crew Sock",
  "handle": "merino-crew-sock",
  "vendor": "ExampleBrand",
  "product_type": "Socks",
  "variant_id": 41234567890123,
  "variant_title": "Large / Charcoal",
  "sku": "MCS-L-CHR",
  "price": "18.00",
  "compare_at_price": "24.00",
  "currency": "USD",
  "available": true,
  "image_url": "https://cdn.shopify.com/s/files/1/.../merino-crew.jpg",
  "created_at": "2026-01-12T08:00:00Z",
  "updated_at": "2026-05-21T14:30:00Z",
  "scraped_at": "2026-05-24T12:00:00Z"
}

Choices worth making early:

  • Explode to one row per variant. Price and availability live at the variant level, not the product level. Keeping it nested makes price-diffing painful.
  • Always store compare_at_price. The gap between price and compare_at_price is the discount depth — a core competitive signal.
  • Keep available and updated_at. Together they let you detect both stockouts and price changes between runs.
  • Stamp scraped_at on every row. Price tracking is meaningless without knowing when a price was observed.

Typical use cases

  • Competitive intelligence — monitor a competitor set’s full catalogs, catch new-product launches the day they go live, watch their discount cadence.
  • Price tracking — diff price and compare_at_price across daily runs to map promotions and price-elasticity.
  • Dropshipping research — bulk-scan hundreds of stores in a niche to find winning products, vendors, and price points.
  • Market and niche analysis — map an entire vertical’s assortment and pricing to spot gaps.
  • Price-comparison engines — feed structured Shopify catalog data into a comparison dashboard.
  • AI / RAG pipelines — build a product knowledge base for retrieval-augmented shopping assistants.

The common thread is breadth times freshness: one store scraped once is a curiosity; 200 stores diffed daily is a competitive-intelligence system.

Cost math

Because there’s no browser and no residential proxy needed for most stores, the per-product cost is dominated by the cheap HTTP fetch. This actor prices per result (a “Business Lead” event at well under a cent each). A realistic competitive-intel setup — 50 competitor stores, ~500 products each, daily incremental refresh — is roughly 25,000 product rows on the first full pull, then far fewer per day once you switch to updated_at-incremental mode. That lands in the low tens of dollars per month at full breadth, or inside the free monthly credit for a smaller competitor set.

Building it yourself is doable (it’s a public JSON endpoint), but you’d still own: pagination loop handling, incremental updated_at logic, the fallback path for stores that block /products.json, variant flattening, and a scheduler. None of it is rocket science; all of it is upkeep.

Common pitfalls

  • Forgetting the 250 cap. ?limit=1000 is silently clamped to 250. If you don’t paginate, you’ll think a 4,000-product store only has 250 items.
  • Stores that disable the endpoint. A 404 or HTML response on /products.json means the merchant blocked it. Have a sitemap/collection fallback.
  • Treating available as exact inventory. It’s a boolean, not a count. Shopify doesn’t expose stock quantity on the public endpoint.
  • Price as a string. Shopify returns price as a string ("18.00"), not a number. Cast it before you do math.
  • Currency assumptions. Don’t assume USD. Read the store’s currency; international stores return their own.
  • Re-scraping everything. Without updated_at-incremental logic you’ll re-pull stable catalogs daily and pay for noise.

Wrapping up

The Shopify /products.json endpoint is the rare scraping target that’s both trivially accessible and genuinely valuable. The hard part isn’t access — it’s doing it at breadth, incrementally, across stores that occasionally block you, and keeping the diff clean. If you need a one-off catalog dump, write the fetch loop yourself. If you need a daily competitive-pricing feed across a market, a managed no-browser actor is the cheaper, lower-maintenance route.

Open the Shopify Competitor Spy on Apify — bulk catalog and price extraction via direct JSON, incremental refresh, flat per-variant schema. Pay per product. Start with Apify’s free monthly credit.

Related guides