Shopify Price Monitor - Competitor Price & Stock Tracker
Pricing
Pay per usage
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
Maintained by CommunityActor 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 see | It means |
|---|---|
| Runs | Every time it checked your stores. Click one to see that day's findings. |
Storage → Key-value store → report | The report. This is the page you actually want to read — click the eye icon to open it. |
| Storage → Dataset | The same changes as raw rows, for exporting to Excel or Google Sheets. |
| Output | A quick table of what changed, without leaving the run. |
| Log | The blow-by-blow of it working. Useful if a store failed. |
| Schedules | When it runs automatically. |
| Integrations | Where 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
| Event | Fires when |
|---|---|
PRICE_DROP / PRICE_INCREASE | a variant's price moved |
SALE_STARTED / SALE_ENDED / SALE_DEEPENED | the compare-at price appeared, went away, or the discount got bigger |
BACK_IN_STOCK / OUT_OF_STOCK | a variant's availability flipped |
NEW_PRODUCT / REMOVED_PRODUCT | a product was added to or pulled from the catalog |
NEW_VARIANT / REMOVED_VARIANT | a size/colour was added or dropped from an existing product |
TITLE_CHANGED | a 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 it | How |
|---|---|
| Slack | Built in. Authorise the workspace, pick a channel, trigger on run succeeded. Easiest option by a distance. |
| Your own system | Add a webhook — Apify POSTs the run and dataset details to any URL when the run finishes. |
| Connect Zapier or Make to Gmail and trigger on a finished run. | |
| A spreadsheet | Export 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
| Field | Default | Notes |
|---|---|---|
storeUrls | — | Any form works: shop.com, https://www.shop.com/collections/all, shop.myshopify.com |
mode | monitor | monitor = changes only. snapshot = full catalog export every run. |
trackedEvents | most | Which event types to report |
minPriceChangePct / minPriceChangeAbs | 0 | Suppress rounding noise and FX jitter |
keywordFilter / excludeKeywords | — | Watch only part of a catalog |
includeUnavailable | true | Set false to track only buyable stock |
concurrency | 5 | Stores fetched at once |
maxPagesPerStore | 40 | 40 pages × 250 = 10,000 products |
autoTune | true | See below |
resetBaseline | false | Throw 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:
| Proxy | Result |
|---|---|
| No proxy / single IP | Small 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 |
| Residential | Gets 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.txtapify 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.