Shopify Price Monitor - Competitor Price & Stock Tracker avatar

Shopify Price Monitor - Competitor Price & Stock Tracker

Pricing

Pay per usage

Go to Apify Store
Shopify Price Monitor - Competitor Price & Stock Tracker

Shopify Price Monitor - Competitor Price & Stock Tracker

Track competitor prices across any number of Shopify stores. Get a daily report of price drops, price increases, new products and out-of-stock items - only what actually changed. The first run saves a baseline; the second run onward reports changes.

Pricing

Pay per usage

Rating

0.0

(0)

Developer

Kashis Rolleston

Kashis Rolleston

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Categories

Share

Shopify Multi-Store Price Monitor

Watch as many Shopify stores as you like and get back only what changed since the last run — price drops, price rises, new products, sold-out variants, sales starting and ending.

Point it at a list of competitor stores, put it on a daily schedule, and read a one-page report each morning instead of clicking through 50 catalogs.


Set it up in three steps

1. Run it once. Paste in the store URLs you want to watch and hit Start. This first run reads each catalog and saves it. It reports no changes, because there is nothing yet to compare against — that is expected, not a fault.

2. Put it on a schedule. Open the Schedules tab and set it to run daily. This is the step that matters: a monitor that only ever runs once can never report anything. From the second run onward, every run reports what moved since the previous one.

3. Have it come to you. In the Integrations tab, connect Slack or a webhook so the report arrives on its own instead of waiting for you to check.

That's it. After that it runs itself, and you only hear from it when something changes.

Where to find things afterwards

Apify's own labels are aimed at developers. Here's the translation:

You'll seeIt means
RunsEvery time it checked your stores. Click one to see that day's findings.
Storage → Key-value store → reportThe report. This is the page you actually want to read — click the eye icon to open it.
Storage → DatasetThe same changes as raw rows, for exporting to Excel or Google Sheets.
OutputA quick table of what changed, without leaving the run.
LogThe blow-by-blow of it working. Useful if a store failed.
SchedulesWhen it runs automatically.
IntegrationsWhere to send the report — Slack, email, your own system.

If you only remember one: the report lives under Storage → Key-value store.


What it reports

EventFires when
PRICE_DROP / PRICE_INCREASEa variant's price moved
SALE_STARTED / SALE_ENDED / SALE_DEEPENEDthe compare-at price appeared, went away, or the discount got bigger
BACK_IN_STOCK / OUT_OF_STOCKa variant's availability flipped
NEW_PRODUCT / REMOVED_PRODUCTa product was added to or pulled from the catalog
NEW_VARIANT / REMOVED_VARIANTa size/colour was added or dropped from an existing product
TITLE_CHANGEDa product was renamed or repositioned

Changes are tracked per variant, because that is where price and stock actually live. A store putting one size on sale is a real event that product-level monitoring misses entirely.

Output

  • Dataset — one row per change, ready for Sheets, Excel, or a webhook.
  • report (HTML) — a formatted, shareable page with thumbnails and links.
  • changes.csv — the same data as a spreadsheet.
  • OUTPUT — run summary: stores checked, totals by event type, warnings.

Getting the report delivered to you

By default the report waits in the run's storage for you to open. To have it come to you instead, schedule the Actor to run daily, then add a delivery in the Actor's Integrations tab:

Where you want itHow
SlackBuilt in. Authorise the workspace, pick a channel, trigger on run succeeded. Easiest option by a distance.
Your own systemAdd a webhook — Apify POSTs the run and dataset details to any URL when the run finishes.
EmailConnect Zapier or Make to Gmail and trigger on a finished run.
A spreadsheetExport the dataset to Google Sheets on a schedule, or pull it from the dataset API.

Delivery deliberately lives at the platform layer rather than inside this Actor. Building email in would mean handing over SMTP credentials to a scraper, and the integrations above already do it better.

Input

FieldDefaultNotes
storeUrlsAny form works: shop.com, https://www.shop.com/collections/all, shop.myshopify.com
modemonitormonitor = changes only. snapshot = full catalog export every run.
trackedEventsmostWhich event types to report
minPriceChangePct / minPriceChangeAbs0Suppress rounding noise and FX jitter
keywordFilter / excludeKeywordsWatch only part of a catalog
includeUnavailabletrueSet false to track only buyable stock
concurrency5Stores fetched at once
maxPagesPerStore4040 pages × 250 = 10,000 products
autoTunetrueSee below
resetBaselinefalseThrow away stored snapshots and start over

Tip: filtering out a store's own test products

Plenty of shops leave internal test or sample products published in their feed — titles like "Test Classic Percale Duvet Cover" show up in real catalogs. Those are genuinely in the store's data, but they are noise in a change report. Drop them with:

"excludeKeywords": ["test", "sample", "do not order"]

The same field is the way to ignore any product line you don't compete on.

The first run reports nothing

That is correct behaviour, not a bug. The first run has nothing to compare against, so it saves a baseline. The second run is the one that reports changes. Schedule it daily and you get a useful report from day two.

Auto-tuning

The actor keeps a small learned profile per store and reuses it next run:

  • page size — some stores/CDNs reject limit=250; once a working size is found it is reused instead of rediscovered every run.
  • catalog depth — a 300-product store needs 2 pages, not 40. Remembering the real depth removes a wasted request per store per run.
  • pacing — a store that throttled us gets a delay next time, and that delay decays away once it stops throttling, so one bad afternoon doesn't slow the store down permanently.
  • currency — detected once, then cached.

Turn autoTune off to fetch every store with stock settings.

The partial-fetch guard

The one failure mode that quietly ruins a monitor is a partial fetch. If a store times out halfway through pagination and the half-catalog is saved as the new baseline, the next run reports thousands of phantom REMOVED_PRODUCT events, and the run after that reports them all as new.

So when a previously healthy catalog appears to lose more than half its variants, the run is treated as suspect: the old baseline is kept, no changes are reported for that store, and a warning goes in the report. A real clearance event shows up on the following run once the catalog reading is stable.

Rate limiting — read this

Shopify's /products.json is a public platform endpoint, but many larger brands put a CDN in front of it that rate-limits per IP. In testing, roughly two thirds of large DTC brands returned 429 from a single residential IP, while smaller stores returned 200 at every page size. The limit is on IP reputation and request frequency, not page size — a blocked store stays blocked even at limit=5.

Verified on the platform:

ProxyResult
No proxy / single IPSmall stores fine; most large brands 429
Datacenter (default)Small and mid-size stores fine — e.g. 5,839 variants pulled from a large linens brand
ResidentialGets through Cloudflare-protected brands — e.g. 2,509 variants from a major shoe brand that 429s on datacenter

You do not have to manage this. With escalateBlockedStores on (the default), a store that gets blocked is retried once through a residential IP automatically, and the tuner remembers — next run it goes straight there instead of wasting the doomed cheap attempt. Only the stores that actually need it cost residential bandwidth, and if a store later recovers on the cheap tier it drops back down.

Verified end to end: a run of three stores where two were blocked went from 1 of 3 succeeding to 3 of 3, with no input changes.

Two details that make it work:

  • Each store gets its own proxy session, so one store's rate limit can't poison the rest of the run.
  • Sessions are salted per run. Session stickiness is wanted within a run (every page of one catalog from one IP looks like a normal shopper) but not between runs — keying on the domain alone pins a store to one IP forever, so a single burned IP would block that store permanently.

If many stores throttle at once, lower concurrency — the actor suggests a value in the run warnings when it detects this.

A 429 aborts that store immediately rather than retrying into a wall, so one blocked store costs seconds, not minutes, and never blocks the others.

Why this stays working

/products.json is a Shopify platform endpoint, not something each merchant designs. One implementation covers every store on the platform, and a shop redesigning its theme cannot break it. That is what makes this cheap to run and cheap to maintain compared to a scraper that parses HTML.

Running it locally

pip install -r requirements.txt
apify run

Or on Apify:

$apify push

State lives in a named key-value store (shopify-monitor-state) so baselines survive between runs. The default store is wiped each run — a monitor keeping its baseline there would have no memory and report every product as new, forever.