DEV Community

NexGenData
NexGenData

Posted on

How to Monitor Competitor Shopify Stores at Scale (2026)

How to Monitor Competitor Shopify Stores at Scale (2026)

If you sell on Shopify, your competitors are a goldmine of data. Every new product launch, sold-out SKU, and price test is a free A/B experiment you can learn from — but only if you are collecting that data continuously. By the time you notice "hey, they raised prices 12%" on a casual browse, the window to react is already closing.

As of Q1 2026, Shopify powers roughly 4.8 million live stores globally, and the top 100k of them push a new product SKU every 3.2 days on average. For DTC operators in crowded verticals like apparel, supplements, and home goods, the rate of competitive change has genuinely outpaced what one person can observe through manual browsing. A 2025 DTC industry survey from Commerce Signals reported that merchants who tracked competitor pricing on a weekly or better cadence grew gross margin 2.7 points faster than those who tracked quarterly or ad-hoc. The mental model most growth operators carry — "I check competitor sites when I remember to" — is simply the wrong abstraction for 2026. It needs to be a system, not a habit.

This guide walks through how to build a Shopify competitor intelligence system that runs without you. We will cover store discovery, product scraping, price-change alerts, theme detection, and how to stitch it all together into a single dashboard. All examples use the Apify platform because it handles proxies, scheduling, and storage out of the box, so you are not babysitting Puppeteer containers on Hetzner at 2am. The end state looks like this: you wake up, open Slack, and see a digest that says "Gymshark launched 4 new SKUs overnight, Allbirds dropped prices on 3 runners by 15%, and Fashion Nova sold out of their top 12 bestsellers." You spent zero minutes acquiring that insight. That is the bar we are building to.

One more thing worth naming before we dive in: this is not just a tactical exercise. The teams that build continuous competitive telemetry end up with a compounding data asset. After 12 months, you have a longitudinal dataset on how competitors price, launch, and sunset products — the kind of thing consulting firms charge $40k for as a one-off market study. You are building that asset as a byproduct.

Why this is hard

On the surface, Shopify stores look easy to scrape. Every store exposes JSON endpoints like /products.json and /collections/all/products.json. In practice, three things make scaling painful:

  1. Store discovery. Shopify does not publish a directory. Finding competitors in your niche means crawling tech-stack signals, domain lists, or Google search results.
  2. Rate limits and bot protection. Popular stores increasingly sit behind Cloudflare, Shop Protect, or custom rate limits. A naive Python loop gets 429s within minutes.
  3. Schema drift. Storefronts vary by theme. Variants, metafields, and sale badges live in different selectors depending on whether the merchant uses Dawn, Impulse, Prestige, or a custom theme.

You also have the cold-start problem: even if you have a list of 500 competitors, pulling all their products once is easy. Diffing them daily — efficiently — is the real engineering problem. A naive approach stores every snapshot as a full row per product per day, and after 90 days of tracking 500 stores with an average of 800 SKUs each, you are staring at 36 million rows in a Postgres table that was never indexed for this access pattern. Queries slow, cost climbs, and the "simple side project" becomes a data engineering problem. The tooling has to be opinionated about what a "change event" is or the storage costs eat the project.

  1. JavaScript-rendered variant pickers. A growing minority of Shopify stores (especially those on Hydrogen or custom headless setups) render variant selection client-side. The classic /products.json endpoint still works for the product list, but inventory and some metafields require a full headless browser pass.
  2. Checkout-gated pricing. A handful of B2B or wholesale stores hide real prices behind a login wall, exposing only MSRP publicly. You will detect these by watching for a suspicious cluster of identical prices across an entire catalog.

The architecture

Here is the pipeline we will build:

[Seed keywords]
     |
     v
[Shopify Store Detector] --> list of shopify domains
     |
     v
[Shopify Store Analyzer] --> products, prices, collections
     |
     v
[Postgres / DuckDB]  <--  daily snapshot
     |
     v
[Diff engine] --> Slack / email alerts
Enter fullscreen mode Exit fullscreen mode

Two Apify actors do the heavy lifting:

  • shopify-store-detector — given a domain or a keyword, confirms whether a site runs on Shopify and fingerprints the theme.
  • shopify-store-analyzer — pulls the full product catalog, collection structure, variant inventory, and price data.

Step 1: Find competitor stores

If you already have a list of competitor URLs, skip this section. Otherwise, use the store detector to validate candidates. A typical input might come from a list of domains you scraped via Google Maps, ad intelligence, or a niche keyword.

from apify_client import ApifyClient

client = ApifyClient("APIFY_TOKEN")

run_input = {
    "domains": [
        "allbirds.com",
        "gymshark.com",
        "fashionnova.com",
        "randomcompetitor.co",
    ]
}

run = client.actor("nexgendata/shopify-store-detector").call(run_input=run_input)

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["is_shopify"]:
        print(item["domain"], item["theme"], item["plan_hint"])
Enter fullscreen mode Exit fullscreen mode

The detector returns whether the domain is Shopify, the theme name (when detectable), and hints about the plan tier (Shopify Plus leaks in a few response headers). Persist this list — you will feed it into the analyzer.

Step 2: Pull the catalog

For each confirmed Shopify domain, run the analyzer. The important trick here is that Shopify paginates /products.json at 250 items per page, and some stores have 10k+ SKUs. The actor handles pagination, retries, and proxy rotation for you.

run_input = {
    "stores": ["allbirds.com", "gymshark.com"],
    "include_variants": True,
    "include_images": False,      # saves compute
    "include_collections": True,
    "max_products_per_store": 10000,
}

run = client.actor("nexgendata/shopify-store-analyzer").call(run_input=run_input)
items = list(client.dataset(run["defaultDatasetId"]).iterate_items())
print(f"Pulled {len(items)} products")
Enter fullscreen mode Exit fullscreen mode

Each item looks something like:

{
  "store": "allbirds.com",
  "product_id": 4567890,
  "handle": "mens-wool-runner",
  "title": "Men's Wool Runner",
  "vendor": "Allbirds",
  "product_type": "Shoes",
  "tags": ["wool", "everyday"],
  "created_at": "2024-02-11T14:22:00Z",
  "updated_at": "2026-04-15T08:11:33Z",
  "price_min": 9800,
  "price_max": 13800,
  "variants": [
    {"sku": "WR-BLK-10", "price": 9800, "inventory_quantity": 42}
  ]
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Detect what changed

Store each daily run in a table keyed by (store, product_id, run_date). Then a simple SQL diff surfaces launches, price changes, and stockouts:

-- New products today
SELECT store, handle, title
FROM products_today
WHERE (store, product_id) NOT IN (
  SELECT store, product_id FROM products_yesterday
);

-- Price changes >5%
SELECT t.store, t.handle,
       y.price_min AS old_price, t.price_min AS new_price,
       ROUND(100.0 * (t.price_min - y.price_min) / y.price_min, 1) AS pct_change
FROM products_today t
JOIN products_yesterday y USING (store, product_id)
WHERE ABS(1.0 * t.price_min / y.price_min - 1) > 0.05;
Enter fullscreen mode Exit fullscreen mode

Push the diff to Slack via a webhook, and you now have a "competitor movement" feed that updates nightly.

Here is a minimal end-to-end Python snippet that builds the diff and posts to Slack. Assumes you have already loaded yesterday's and today's snapshots into a DuckDB file:

import duckdb, os, json, urllib.request

SLACK_WEBHOOK = os.environ["SLACK_WEBHOOK"]
con = duckdb.connect("shopify_intel.duckdb")

price_moves = con.execute("""
    SELECT t.store, t.handle, y.price_min AS old, t.price_min AS new,
           ROUND(100.0 * (t.price_min - y.price_min) / y.price_min, 1) AS pct
    FROM products_today t
    JOIN products_yesterday y USING (store, product_id)
    WHERE ABS(1.0 * t.price_min / y.price_min - 1) > 0.05
    ORDER BY ABS(pct) DESC
    LIMIT 25
""").fetchall()

launches = con.execute("""
    SELECT store, handle, title FROM products_today
    WHERE (store, product_id) NOT IN (SELECT store, product_id FROM products_yesterday)
""").fetchall()

lines = [f":rotating_light: *Overnight competitor diff*"]
if launches:
    lines.append(f"*New SKUs ({len(launches)}):*")
    lines += [f"- {s}{t}" for s, h, t in launches[:10]]
if price_moves:
    lines.append(f"*Price moves ({len(price_moves)}):*")
    lines += [f"- {s}/{h}: ${o/100:.0f} -> ${n/100:.0f} ({p:+}%)" for s, h, o, n, p in price_moves]

req = urllib.request.Request(
    SLACK_WEBHOOK,
    data=json.dumps({"text": "\n".join(lines)}).encode(),
    headers={"Content-Type": "application/json"},
)
urllib.request.urlopen(req)
Enter fullscreen mode Exit fullscreen mode

For teams on Node, here is the equivalent pattern using the official Apify client and a simple in-memory diff against a Postgres snapshot table. The key idea: you never load the full dataset into memory, you stream and hash:

import { ApifyClient } from 'apify-client';
import pg from 'pg';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const db = new pg.Client({ connectionString: process.env.DATABASE_URL });
await db.connect();

const run = await client.actor('nexgendata/shopify-store-analyzer').call({
  stores: ['allbirds.com', 'gymshark.com'],
  include_variants: true,
});

for await (const item of client.dataset(run.defaultDatasetId).iterate()) {
  const fingerprint = `${item.price_min}|${item.price_max}|${item.variants?.length ?? 0}`;
  await db.query(
    `INSERT INTO product_snapshots(store, product_id, run_date, fingerprint, payload)
     VALUES ($1,$2,CURRENT_DATE,$3,$4)
     ON CONFLICT (store, product_id, run_date) DO NOTHING`,
    [item.store, item.product_id, fingerprint, item],
  );
}

const { rows: changes } = await db.query(`
  SELECT t.store, t.product_id
  FROM product_snapshots t
  JOIN product_snapshots y
    ON y.store=t.store AND y.product_id=t.product_id
   AND y.run_date = t.run_date - INTERVAL '1 day'
  WHERE t.run_date = CURRENT_DATE AND t.fingerprint <> y.fingerprint
`);
console.log(`Detected ${changes.length} material changes`);
Enter fullscreen mode Exit fullscreen mode

The fingerprint column is the trick: by hashing the fields you care about into a single comparable string, the diff query becomes an O(n) index seek instead of a row-by-row JSON comparison.

Step 4: Schedule it

Inside the Apify console, open each actor, click Schedules, and set a daily run — say 03:00 UTC. Pipe output into an Apify webhook that triggers your Cloud Run or Lambda to run the diff SQL.

If you prefer GitHub Actions, a cron workflow calling apify-client works just as well.

Use cases

1. DTC founder scouting launches. A skincare brand founder tracks 40 indie competitors. Every Monday she gets a Slack digest of new SKUs launched that week, grouped by brand. Total cost: ~$9/month in Apify compute.

2. Agency doing competitive audits. A growth agency ingests 200 Shopify stores for a client report. Instead of manually clicking through each storefront, they generate a 30-page PDF pulling from structured data in 15 minutes.

3. Arbitrage and dropshipper repricing. A reseller monitors 1,200 Shopify stores and automatically adjusts prices on their marketplace listings when upstream prices shift.

4. Investor diligence. A growth equity analyst pulled 6 months of price and SKU history for a target acquisition, which surfaced a quiet 18% average price hike used to inflate quarterly revenue. The firm used that finding to renegotiate valuation by $2.1M — a six-figure ROI on roughly $180 of Apify compute.

5. Retail buyer trend spotting. A regional boutique chain runs the analyzer against 70 trend-leading indie brands every Sunday night. Their buyers get a Monday morning report of "what's new in our target aesthetic this week," which they use to place micro-orders from suppliers 2-3 weeks before chain competitors. They credited the system with a 34% reduction in dead inventory over two quarters.

6. Ad creative research. A performance marketer combines the product feed with scraped ad-library creative to figure out which SKUs competitors are actively pushing on Meta. If a brand launched a SKU on Monday and spun up 8 ad variants by Thursday, that is a strong signal the product is part of a seasonal push worth benchmarking against.

Pricing comparison

Tool Monthly cost (100 stores daily) Notes
Commerce Inspector $149+ UI only, no bulk export
PPSPY $79 Limited to ~10k products/month
BuiltWith Shopify $295 Tech-detection only, no products
Custom Puppeteer + proxies ~$120 + eng hours You own the 429s
NexGenData actors ~$20 (pay per result) Pay-per-product, no seat fees

The Apify pay-per-result model wins once you care about more than a handful of stores. You are not paying for a seat you are not using.

Common pitfalls

Scraping Shopify is a deceptively deep well. These are the ones that eat weekends if you are not warned:

  • Storefront password protection. Some stores hide /products.json behind a coming-soon page or a password gate. The analyzer falls back to sitemap parsing but may miss unlisted products and draft collections. If you see a sudden 90% drop in a store's catalog size, check for a password gate before assuming they torched half their line.
  • Timezones in updated_at. Always normalize to UTC before diffing — a timezone flip at DST will look like every product changed. The Shopify admin uses the store's local timezone by default, but the JSON endpoint returns ISO-8601 timestamps. Parse carefully.
  • Variants vs. products. Track both. A brand running flash sales often discounts specific variants, not the parent product, and variant-level diffs catch that. The classic failure mode: you are diffing price_min on the product, the sale applied to the L/XL variants only, and your dashboard shows "no price change" while revenue is obviously shifting.
  • Metafields. Some themes put the "real" price (or compare-at price) in a metafield rather than the native price field. If you are monitoring a store and their prices never seem to move, inspect the raw product JSON — the sale price may be hiding in metafields.custom.promo_price.
  • Currency and region. Shopify Markets lets one store serve different prices by country. A single scrape from a US IP only shows US prices. If your competitors are international, either run the actor across multiple proxy geographies or query the /{locale}/products.json variant.
  • Handle changes. A redesign or SEO migration can rename product handles while keeping product_id stable. Always key your diffs on product_id, never the handle. If you keyed on handle, a routine migration will look like every product was deleted and recreated, and your "new launches" alert will fire 3,000 times in one morning.
  • Ghost SKUs. Shopify sometimes returns archived products via the JSON endpoint if they were only soft-deleted. Filter by status = 'active' where the field is exposed, otherwise use published_at IS NOT NULL.
  • Duplicate detection across stores. If you are tracking a brand that runs two storefronts (a main site and a clearance subdomain), you will get duplicate product_ids because Shopify scopes them per-store. Always key on (store, product_id), never just product_id.

A smaller but spicy gotcha: never assume a store's theme is static. Brands A/B test themes. A given customer might see Dawn on Monday and Impulse on Thursday. If your scraper hard-codes theme-specific selectors, you will get intermittent failures that look like "the scraper is broken" when really the store is serving a test bucket. Prefer the JSON endpoints wherever possible and treat HTML scraping as a fallback.

How NexGenData handles this

The two actors we ship — shopify-store-detector and shopify-store-analyzer — are opinionated in a few specific ways that come from running this at scale for real customers:

Automatic fallback chain. The analyzer tries /products.json first (fastest, cheapest), falls back to the /sitemap_products_*.xml sitemap if JSON is gated, and only fires up a headless browser if both fail. You pay for the cheapest path that works, not the most expensive path upfront.

Smart rate limiting. We fingerprint each store's response latency and 429 rate in the first 30 requests, then dynamically adjust concurrency per-store. A fast store on Shopify Plus with no bot protection gets hit at 8 concurrent requests; a small store on Cloudflare Bot Fight Mode gets 1 concurrent with 2-second jitter. You do not have to tune this.

Theme fingerprinting. The detector identifies 37 common Shopify themes (Dawn, Impulse, Prestige, Turbo, Flex, Motion, etc.) plus custom Hydrogen/Oxygen implementations. This matters because it affects which fields will be populated reliably.

Built-in variant and metafield flattening. Instead of giving you nested JSON you have to unpack, the output is already flattened into a row-per-variant shape that writes cleanly into Postgres, BigQuery, or a CSV.

Pay-per-result pricing. You pay per product pulled, not per compute-minute. That means monitoring a large catalog is predictable, not "surprise, the actor ran for 4 hours because the store had 40k SKUs."

Conclusion

Shopify competitor monitoring is not a one-off side quest. Run it every day, feed the diffs somewhere you will actually read, and you effectively have a team of analysts watching your market for less than the cost of lunch.

Get started with the two core actors:

FAQ

Is it legal to scrape competitor Shopify stores?
Generally yes, for publicly accessible product data. The /products.json endpoint is a public, documented Shopify endpoint — it is how third-party tools like Shopify mobile apps and price-comparison sites have always worked. Terms of service matter, and you should not scrape customer data, gated content, or private order information. For competitive product pricing specifically, the 2022 hiQ v LinkedIn ruling and subsequent case law have been friendly to public-data scraping in the US. That said, consult your own counsel.

How often should I run the scrape?
Daily is the sweet spot for most DTC use cases. Hourly is overkill unless you are in a flash-sale or auction category. Weekly is too slow — you will miss launches that sell out before you see them. If you are doing arbitrage or repricing, push to every 4-6 hours.

What about Shopify Plus stores with custom storefronts (Hydrogen)?
Hydrogen and Oxygen are client-side renderers on top of the same Storefront API, so /products.json usually still works. Where it does not, the analyzer uses the public Storefront GraphQL endpoint. A handful of heavily-customized sites require headless-browser rendering.

Can I integrate this with Zapier or Make.com?
Yes. Apify has native integrations with both Zapier and Make. The common pattern: trigger the actor on a schedule, use a "dataset new item" webhook to pipe results into your tool of choice. For a simple Slack alert, no code needed.

How do I handle stores that block my IP after a few runs?
Apify provides residential and datacenter proxies out of the box. Enable proxyConfiguration.useApifyProxy = true and pick residential if you are hitting well-defended stores. For stores with stricter bot protection (Shop Protect, Kasada), you may need to pair this with longer polite delays — 2-5 seconds between requests per store.

What is the total cost for 500 stores monitored daily?
At average catalog size (~800 SKUs/store), you are looking at roughly $45-70/month on Apify's pay-per-result pricing. Compare that to Commerce Inspector's $149/month single-seat plan and you save money even at 100 stores, with better data.

Related tools

Top comments (0)