DEV Community

Tidy Tools
Tidy Tools

Posted on

Track Shopify competitor prices daily with products.json (no API key)

If you sell on Shopify, your competitors probably do too. And every Shopify storefront has a public, documented-by-accident API that most people never look at:

https://www.allbirds.com/products.json?limit=250&page=1
Enter fullscreen mode Exit fullscreen mode

Open that in a browser. It is the store's entire catalog as JSON: every product, every variant, SKUs, prices, compare-at prices and whether each size is in stock. No login, no API key, no HTML parsing. Shopify serves it to every visitor (unless the store turns it off).

That makes Shopify price monitoring much simpler than scraping product pages. This post shows how to turn that feed into a clean export and a daily price-drop alert, and where its limits are.

What the feed gives you (and what it does not)

Data Endpoint Limit
Products and variants /products.json?limit=250&page=N 250 per request; at most 100 pages, so 25,000 products per catalog or collection
One collection /collections/<handle>/products.json same
Collections /collections.json 250 per request
Store facts /meta.json name, myshopify domain, default currency, country, product count

Three things it does not give you:

  1. Currency. products.json has prices but no currency field. You have to read it from the store page or meta.json, and stores using Shopify Markets convert prices by visitor country.
  2. Stock quantities. You get available: true/false per variant, never "3 left".
  3. Anything on headless storefronts. Stores built on Hydrogen (fashionnova.com in my tests) answer 404.

One clean row per product

I wrapped all of this in an Apify Actor, Shopify Product Scraper & Shopify Price Monitor, so you can paste store URLs, domains, collection URLs or product URLs and get a flat table. It also checks robots.txt first and waits one second between requests per store by default.

{
  "stores": ["https://www.allbirds.com", "gymshark.com", "https://kith.com/collections/mens-footwear"],
  "maxProductsPerStore": 300,
  "onlyOnSale": true
}
Enter fullscreen mode Exit fullscreen mode

A real row (30 September 2026), shortened:

{
  "storeName": "Allbirds",
  "title": "Women's Allbirds Flip Flop - Dusty Pink",
  "url": "https://www.allbirds.com/products/womens-allbirds-flip-flop-dusty-pink",
  "vendor": "Allbirds",
  "productType": "Shoes",
  "price": 25,
  "priceMax": 25,
  "compareAtPrice": 50,
  "onSale": true,
  "discountPercent": 50,
  "currency": "USD",
  "currencySource": "store page",
  "available": true,
  "variantsCount": 7,
  "variantsAvailable": 1,
  "skus": ["A12513W050", "A12513W060", "A12513W070", "A12513W080", "A12513W090", "A12513W100", "A12513W110"],
  "options": [{ "name": "Size", "values": ["5", "6", "7", "8", "9", "10", "11"] }],
  "publishedAt": "2026-09-25T23:58:13.000Z",
  "charged": true
}
Enter fullscreen mode Exit fullscreen mode

That single row already answers a competitive question: a new product, published five days earlier, at half price, with only one of seven sizes left. discountPercent and variantsAvailable are computed for you; the raw variants array (SKU, price, compare-at, availability, weight per size) is in the row too. Set rowPer to variant if you want one spreadsheet line per size or colour; you still pay once per product.

Filters run before charging: keywords, excluded words, vendors, product types, tags, price range, in stock only, on sale only, published since a date, or only certain collections.

The real test run

Seven inputs, 300 products per store: allbirds.com, gymshark.com, a Kith collection, colourpop.com, one Tentree product URL, fashionnova.com (headless, no feed) and example.com (not Shopify).

Result: 1,201 products from 5 stores in 12.6 seconds, plus 2 free error rows. Cost on the Free plan: 1,201 × $0.001 = $1.20.

The two failures come back as rows that explain themselves, and are not charged:

{ "input": "https://www.fashionnova.com", "success": false, "errorType": "not_found",
  "error": "products.json could not be read: HTTP 404 (not found) (the store has turned off its public product feed, or it is a headless storefront) (not charged)", "charged": false }
Enter fullscreen mode Exit fullscreen mode

Turn it into a daily price monitor

A one-off export is useful once. Price monitoring needs a baseline and a diff. Turn on onlyChanges and give the watchlist a name:

{ "stores": ["https://www.tentree.com", "https://colourpop.com"], "onlyChanges": true, "monitorName": "competitors" }
Enter fullscreen mode Exit fullscreen mode
  1. Run it once. This first run returns the products and saves them as the baseline.
  2. In Apify Console go to Schedules → Create, pick this Actor and input, and run it daily.
  3. Each later run returns only new and changed products, with what changed.

A changed row looks like this (this is the shape from the unit tests; in my real test, a second Tentree run two minutes later found 0 changes and cost nothing):

{
  "title": "Women's Allbirds Flip Flop - Dusty Pink",
  "price": 20,
  "changeType": "changed",
  "changes": ["price_drop", "back_in_stock"],
  "previousPrice": 25,
  "variantChanges": [
    { "title": "5", "change": "price,available", "price": 20, "previousPrice": 25, "available": true, "previousAvailable": false }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Change types: price_drop, price_increase, back_in_stock, out_of_stock, compare_at_price, variant_added, variant_removed. Products that disappear get a free "removed" row (only when the whole catalog was read with no filters, so "not read this time" is never reported as "removed"). Connect the Slack, email or webhook integration on the Actor and the changes arrive as alerts.

Two guards that kept my test alerts honest:

  • Unchanged products are free. A daily check on a quiet store costs nothing.
  • Mass changes are held back. If more than half of a store's catalog (stores with at least 10 products) changes in one run, or the store moves to a different myshopify shop or domain, every row is still output but marked suspiciousChange: true, nothing is charged for that store, and the old baseline is kept until you accept the new one. A store-wide currency switch should not be billed as thousands of "price changes".

Bonus: which of these sites run on Shopify?

Set mode to detect and paste domains:

{ "stores": ["allbirds.com", "gymshark.com", "kith.com", "example.com", "wikipedia.org"], "mode": "detect" }
Enter fullscreen mode Exit fullscreen mode
{ "store": "https://gymshark.com", "isShopify": true,
  "detectedBy": ["meta.json", "powered-by header", "Shopify theme script", "products.json"],
  "storeName": "Gymshark US", "myshopifyDomain": "gymsharkusa.myshopify.com",
  "currency": "USD", "country": "US", "productsCount": 10088, "collectionsCount": 526 }
Enter fullscreen mode Exit fullscreen mode

Sites that are not Shopify stores are free, which makes this a cheap first filter on a lead list.

What it costs

Event Free / Starter Scale Business and higher
Product (all variants included) $1.00 / 1,000 $0.90 $0.80
Collection (collections mode) $0.30 / 1,000 $0.27 $0.24
Shopify store detected $1.00 / 1,000 $0.90 $0.80

No start fee. Not charged: filtered-out products, unchanged products in monitoring mode, removed-product rows, suspicious change sets, non-Shopify sites, password-protected stores and stores without a public feed. Max products per store (default 1,000) and Max products (default 10,000) cap the run, and it logs its worst case before starting.

Limits worth knowing

  • 25,000 products per catalog or collection is Shopify's own cap. For bigger stores, read collection by collection.
  • Prices follow the requester's country. Runs come from the US, so Shopify Markets stores usually answer in USD. currency says what the numbers are; storeCurrency shows the store's default when it differs.
  • Some stores block data-center traffic or disable the feed; you get a free row saying which.

Terms and responsible use

This reads only the public storefront endpoints Shopify serves to every visitor, checks robots.txt first, and returns product data only: no customer, order or personal contact data. Product texts and images belong to the stores, so use the data for monitoring and analysis, not to republish their catalog as your own.

Actor: https://apify.com/tidytools/shopify-store-products-scraper


Disclosure: I built this Actor and earn from its usage on Apify. The stores above are public examples from my test runs of 30 September 2026 and have no connection to me.

Top comments (0)