Shopify Price & Stock Monitor – Change Tracker
Pricing
Pay per event
Shopify Price & Stock Monitor – Change Tracker
Monitors public Shopify catalogs and outputs only what changed since the last run, per variant: price and compare-at drops/rises, back in stock, out of stock, new and removed variants, title and SKU changes. No full-catalog re-scrape needed.
Pricing
Pay per event
Rating
0.0
(0)
Developer
Changefeeds Tools
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
For merchants tracking their own store and for anyone watching competitor Shopify shops, this Actor watches public Shopify catalogs and tells you what changed since the last run — one row per variant change: price drops and rises, compare-at ("was") price changes, back in stock, out of stock, new variants, removed variants, and title or SKU changes. You get a changefeed, not a catalog dump.
Catalog scrapers return the whole catalog every time, and you have to diff it yourself. This Actor keeps the previous run's snapshot for you and returns only the differences. Schedule it daily or hourly and read the dataset, or send it to a webhook.
Use cases
Price-drop and restock alerts to Slack, without writing code. Create a task with the store list you
want to watch, add an hourly or daily Schedule in Apify Console, then attach an
Apify integration on the task's "run succeeded" event —
Slack, Zapier, Make, or Google Sheets are all built in. Each run's dataset holds only the rows that
changed since the previous run (price_changed, back_in_stock, etc.), so the integration only fires on
real changes, not a full catalog every time. Point the same integration at multiple stores by running one
task per store, or one task with all your stores in storeUrls and filter the dataset rows downstream by
store. Prefer a direct push instead of polling the dataset? Set webhookUrl and skip the integration
step — the run POSTs its own summary and up to 500 changes as soon as it finds any.
What it does
- For each store you list, it reads Shopify's public catalog endpoint,
https://<store>/products.json, 250 products per request. If that returns 404 it tries/collections/all/products.json. - It turns every product into variant rows: store, product id, handle, title, URL, variant id, variant
title, SKU, price, compare-at price, availability, and
updated_at. - It compares those rows with the snapshot saved by the previous run of the same input, and outputs one row per change.
- It saves the new snapshot for next time.
The first run for a store has nothing to compare with. It outputs every variant with
change_type: "baseline" and saves the snapshot. Changes start from the second run.
Change types
change_type | Meaning |
|---|---|
price_changed | price differs; before, after, pct_change |
compare_at_changed | compare_at_price differs, including set or cleared; pct_change when both sides have a value |
back_in_stock | available went from false to true |
out_of_stock | available went from true to false |
new_variant | a variant id not in the previous snapshot |
removed_variant | in the previous snapshot, absent from this run's complete fetch |
title_changed | product title (field: "product_title") or variant title (field: "variant_title") differs |
sku_changed | SKU differs |
baseline | first run for this store: current values, no comparison |
unchanged | only with includeUnchanged: true |
store_error | this store could not be read (see error_code) |
A variant with several changes in one run produces several rows, one per field.
Input
{"storeUrls": ["https://www.allbirds.com", "https://www.tentree.com"],"maxProductsPerStore": 5000,"trackFields": ["price", "compare_at_price", "available", "title", "sku"],"snapshotKey": "","webhookUrl": "","includeUnchanged": false}
storeUrls(required): store base URLs. Any path is ignored.allbirds.comandhttps://allbirds.com/collections/xboth mean the same store.maxProductsPerStore(default 5,000, maximum 50,000): the most products read per store per run.trackFields: which fields produce change rows. New and removed variants are always reported.snapshotKey: which saved snapshot to compare against. If you leave it empty, it is derived from the sorted store list, so a schedule with the same input always compares with its own previous run. Set it when you want to add or remove stores without starting over. Stores that are new to the key get a baseline, and the others keep comparing.webhookUrl: when a run finds changes, the run summary and the first 500 changes are POSTed here as JSON, with a 10-second timeout. A baseline-only run does not call it. If the POST fails, the failure is logged and recorded in the summary, and the run still succeeds.includeUnchanged: also output a row for every variant that did not change.
If a run finds nothing new, the dataset holds one no_changes row (not charged), so a quiet run is never mistaken for a broken one.
Sample output
A change row (dataset item). The shape is exact; the values are illustrative:
{"change_type": "price_changed","store": "www.tentree.com","product_id": "8360516550842","product_handle": "chambers-pocket-shirt-nightfall-midnight-blue","product_title": "Chambers Pocket Shirt","product_url": "https://www.tentree.com/products/chambers-pocket-shirt-nightfall-midnight-blue","variant_id": "45724486140090","variant_title": "NIGHTFALL MIDNIGHT BLUE / S","sku": "TCM6530-6325-S","price": 70.4,"compare_at_price": 88,"available": true,"updated_at": "2026-09-28T20:55:54-07:00","field": "price","before": 88,"after": 70.4,"pct_change": -20,"previous_run_at": "2026-09-28T03:56:00.489Z","run_at": "2026-09-29T03:56:00.489Z"}
Prices are numbers in the store's own currency, exactly as products.json reports them. products.json
does not say which currency that is, so the Actor does not guess.
A store that cannot be read gives one error row, and the other stores still run:
{"change_type": "store_error","store": "www.example-store.com","error_code": "blocked","error_message": "HTTP 403: the store refused the public catalog request","partial": false,"run_at": "2026-09-29T03:56:00.489Z"}
error_code is one of not_shopify, password_protected, blocked, rate_limited, http_error,
network_error. partial: true means some pages were read before the error. Changes from those pages
are still reported, but removals are not.
The run summary is saved to the default key-value store as OUTPUT:
{"run_at": "…","finished_at": "…","snapshot_key": "stores-d196c1e1e34ba01e6de5ede9cc4c094b","first_run": false,"products_checked": 600,"variants_checked": 4536,"changes_total": 12,"changes_by_type": { "price_changed": 3, "compare_at_changed": 1, "back_in_stock": 2, "out_of_stock": 4,"new_variant": 2, "removed_variant": 0, "title_changed": 0, "sku_changed": 0 },"baseline_rows": 0,"errors": [{ "store": "…", "code": "blocked", "message": "…" }],"truncated_stores": [{ "store": "…", "reason": "max_products" }],"charge_limit_reached": false,"requests_total": 5,"stores": [{ "store": "…", "status": "complete", "first_run": false, "products_fetched": 300,"products_checked": 300, "variants_checked": 1200, "changes": 12, "requests": 2, "removals_checked": true, "…": "…" }],"webhook": null}
What it costs
$1.50 per 1,000 products checked ($0.0015 per product), plus Apify's standard actor-start fee ($0.00005 per run, scaled by memory — negligible next to the per-product charge).
- A product is charged once per run when its variants are compared, however many variants it has.
- The first (baseline) run is charged the same way. Unchanged products count too, because checking them is the work.
- Changes are included and not charged extra. Neither are store errors or the webhook.
- A product that appears twice in one run is charged once.
Worked examples:
- First (baseline) run, 1 store, 800 products: 800 products × $0.0015 = $1.20, plus the start fee.
Every product comes back as
change_type: "baseline"; there's nothing to compare yet. - Steady-state daily run, 3 stores at 2,000 products each: 6,000 products checked a run × $0.0015 = $9.00/run. Scheduled once a day, that's ~$270/month (30 runs), whatever the mix of price drops, restocks or quiet days — you're paying to check, not per change found.
- Hourly monitoring, 1 store, 500 products: 500 × $0.0015 = $0.75/run × 24 runs/day × 30 days = ~$540/month. Drop to a 4×-daily schedule for the same coverage window at ~$90/month.
Read products_checked in the run summary for your actual per-run count.
Set a maximum cost per run in Apify, and the Actor will respect it. When the budget is used up, it
stops fetching and saves what it has. It marks the affected stores as truncated
(reason: "charge_limit") and sets charge_limit_reached: true in the summary. It never charges past
your limit.
Scheduling
Changes only mean something when the same input runs repeatedly:
- Create a saved task for this Actor with your store list (Actor page → Create task).
- In Apify Console, open Schedules → Create new, pick a cron such as
0 7 * * *(daily at 07:00 UTC) or0 * * * *(hourly), and add your task. - Each scheduled run compares with the previous one. Read the run's dataset, set
webhookUrl, or use Apify integrations (Slack, Zapier, Make, email) on the "run succeeded" event.
Keep the store list unchanged, or set a fixed snapshotKey, so runs keep comparing with the same
snapshot.
Limits
- Public catalog only. The Actor reads only
products.json, the catalog that Shopify storefronts publish to everyone. It never logs in, never touches cart or checkout, and collects no personal data. - Some stores hide
products.json, block it (HTTP 403), or are password protected. Those stores give astore_errorrow. There is no workaround in this Actor, by design. - Only products published to the Online Store appear in
products.json. Hidden or channel-only products are not visible. - Removal detection needs a complete fetch.
removed_variantis reported only when the whole catalog was read in this run. If a store stops atmaxProductsPerStore, at your cost limit, or on an error, removals are not reported for that store, and the summary says so. Changes among the variants that were read are still reported, and the snapshot keeps the unseen variants for the next complete run. - Stores larger than
maxProductsPerStoreare read in catalog order, and only the first N products are compared. If products move in or out of the first N between runs, they can appear asnew_variant. Raise the limit to cover the whole catalog if that matters to you. - It is polite, which makes it slower on big catalogs. Each store is read one request at a time, at
least 1 second apart, and HTTP 429
Retry-Afteris honored. That is about 250 products per second per store at best. Up to three stores are read in parallel. - Currency is whatever the store's default is.
products.jsondoes not include it. - Inventory counts are not in the public catalog. Only in stock or out of stock is available.
FAQ
Is this allowed? The Actor reads the same public JSON catalog that Shopify serves to every visitor, at about one request per second, and identifies itself in its User-Agent. You are responsible for your own use of the data and for respecting each store's terms.
Why did my first run return every variant? That is the baseline. Changes start from the second run of the same input.
I edited my store list and everything is baseline again. The default snapshot key comes from the store
list. Set snapshotKey to a fixed name to keep history when you edit the list.