Shopify Store Monitor: Price Drops, Restocks & New Products avatar

Shopify Store Monitor: Price Drops, Restocks & New Products

Pricing

from $0.60 / 1,000 change reporteds

Go to Apify Store
Shopify Store Monitor: Price Drops, Restocks & New Products

Shopify Store Monitor: Price Drops, Restocks & New Products

Watch any Shopify store on a schedule and get back only what changed: price drops, sales started and ended, restocks, sell-outs, new and removed products, variants added and removed, with real per-variant stock counts. Give it a domain or a collection URL. No API key, no app install, no password.

Pricing

from $0.60 / 1,000 change reporteds

Rating

0.0

(0)

Developer

Insight Solutions

Insight Solutions

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

Watch a Shopify store on a schedule and get back one row per change — not the catalogue again. Twelve named change types, per-variant stock counts where the store publishes them, $1.00 per 1,000 changes, and no per-product charge at all: a baseline run of a 5,000-product catalogue bills $0.003, and a quiet day bills $0.002 per store. Failures are free, typed rows, never invoices.

Give it a domain. No API key, no app install, no store password, no browser.

Input — a minimal runnable input; the Store form prefills the same stores entry with maxProductsPerStore: 25 and firstRunBehavior: "emit-all" so the first run shows you change rows straight away:

{
"stores": ["https://www.allbirds.com"],
"watch": ["price-drop", "restock", "new-product"],
"minPriceChangePct": 5
}

One change, one row:

{
"ok": true,
"rowType": "change",
"changeType": "price-drop",
"storeDomain": "www.allbirds.com",
"productId": 7218356060240,
"handle": "mens-strider-explore",
"title": "Men's Strider Explore",
"vendor": "Allbirds",
"productType": "Shoes",
"variantId": 42436493115472,
"variantTitle": "US 10 / Natural Black",
"sku": "A11582U030",
"oldPrice": 130,
"newPrice": 104,
"priceChangePct": -20,
"oldCompareAtPrice": null,
"newCompareAtPrice": 130,
"oldAvailable": true,
"newAvailable": true,
"currency": "USD",
"previousSeenAt": "2026-09-14T06:00:00.000Z",
"daysSinceLastChange": 41,
"productUrl": "https://www.allbirds.com/products/mens-strider-explore",
"runComparedAt": "2026-09-15T06:00:00.000Z"
}

Every row carries the same 68 columns, so the CSV export has the same header whatever mix a run produced.

Output — one row per change; the fields you will use most are changeType, handle, variantTitle, oldPrice, newPrice and runComparedAt (full list under Output reference), plus one store-summary row per watched store per run. Anything that could not be read comes back as a free diagnostic row (ok: false, errorType, error) instead of a charge.

Price — $1.00 per 1,000 changes on the FREE tier (+$0.002 per store compared, +$0.001 per run); there is no per-product charge at all, and a domain that is not Shopify, a store with its feed switched off, a store held back by the collapse guard, a store the run never reached, and a first (baseline) run's products are all free. Pay-per-event, no API key, no browser, limited permissions — works over the Apify MCP server (mcp.apify.com) and with agentic (x402) payments.

From codeclient.actor("insight.solutions/shopify-store-monitor").call(run_input={"stores": ["https://www.allbirds.com"], "watch": ["price-drop", "restock", "new-product"]}) with apify-client, or POST https://api.apify.com/v2/acts/insight.solutions~shopify-store-monitor/run-sync-get-dataset-items.


Try it in 30 seconds

Hit Start with the input already filled in. It watches one store, caps the catalogue at 25 products, and asks for the whole thing up front so you can see what a change row looks like without waiting a day.

  • Run 1 returns 25 new-product rows and 1 store-summary row. That is the baseline: the Actor now knows what the store looked like.
  • Run 2, tomorrow or in five minutes, returns 1 store-summary row and nothing else if the store did not move — and one row per change if it did. That is the product.

Then change firstRunBehavior to baseline-only, raise maxProductsPerStore, add the stores you care about, and put it on a daily schedule.


How the change detection works

Each run reads the store's own public /products.json feed, compares it against the snapshot the previous run left behind, and writes a row for each difference you asked for.

changeTypeLevelFires when
new-productproductA product ID appears that the previous snapshot did not hold
removed-productproductA product ID in the snapshot is gone, and the catalogue was read in full
price-dropvariantA variant's price fell, by at least minPriceChangePct
price-increasevariantA variant's price rose, by at least minPriceChangePct
sale-startedvariantThe compare-at price went from "not above the price" to strictly above it
sale-endedvariantThe compare-at price stopped being above the price
restockvariantavailable went from false to true
out-of-stockvariantavailable went from true to false
variant-addedvariantA variant ID appeared on a product that already existed
variant-removedvariantA variant ID left a product that is still there
inventory-changevariantA published stock count moved (needs trackInventory)
content-changeproductThe title, tags, description or image set moved

Three rules are worth knowing about before you schedule this.

A sale is compare_at_price > price, strictly. In a 2,525-variant capture of one real store, 84 variants carried a non-null compare-at price and only 4 carried one above the price — the rest were set equal to the price, which is not a discount. Treating "has a compare-at" as "on sale" would have reported 84 sales that were not happening, on day one, on one store.

updated_at is ignored, because it is not a modification time. In four captures of four different stores, every product and every variant in the response shared one updated_at, equal to the second the response was generated — 250 products and 2,525 variants, one timestamp. Fingerprinting it would report the whole catalogue as changed on every run and bill you for it. This Actor compares the values themselves: price in integer cents, compare-at in cents, the availability flag, and the stock count where there is one.

A price move and a sale move are two separate events. A variant dropping from $130 to $104 and gaining a $130 compare-at emits both a price-drop and a sale-started — two rows, two charges. They are different facts, and people filter on them differently.


First run, and setting up a schedule

The first time a store is seen there is nothing to compare against, so firstRunBehavior decides what happens:

  • baseline-only (the default) records the catalogue and writes one store-summary row with firstRun: true. It bills $0.001 + $0.002 = $0.003, whether the store has 12 products or 5,000. There is no per-product charge, so a backfill you did not ask for cannot cost you anything.
  • emit-all reports every product in the catalogue as new-product. Those are billed per row, so pair it with a small maxProductsPerStore the first time — which is exactly what the prefill does.

From then on, schedule it. Daily is the setting most people want; the Actor is built so that a quiet day costs $0.001 + $0.002 per store and writes one row per store, which keeps your dataset readable and your dashboards non-empty.

Two schedules must not share a stateStoreName. They would each compare against the same snapshot and each report the same changes, so the second run refuses the lock, writes one free state-locked row and stops. Give concurrent watchlists different store names.

Where the state lives

The memory is a named key-value store (stateStoreName, default shopify-store-monitor-state), which is what makes this a monitor rather than a scraper: an Actor's default store is recreated for every run, including every run of a schedule.

  • Limited permissions. The Actor creates the store on its first run and re-opens its own store afterwards, which is what a limited-permissions Actor is expressly allowed to do. It never touches storage you created. Do not point stateStoreName at a store you made by hand — the Actor cannot read it, and the run would quietly restart from a baseline.
  • One record per watched target, where a target is one store crossed with one collection scope. shop.com, shop.com/collections/mens and shop.com/collections/sale are three targets with three histories.
  • A run lock, with an expiry derived from your own maxRunSecs, so a long run is never evicted mid-walk and two runs never double-report.
  • Size. A snapshot costs about 183 bytes per product plus 32 bytes per variant (measured, not estimated: 250 products and 2,525 variants serialise to 126 KB), plus 45 bytes per product if you watch content-change. One key-value record holds 9 MB and this Actor refuses to write past 8 MB, so the real ceiling is around 16,600 products at 10 variants each — not the 20,000 a per-product estimate suggests. Over that, the Actor keeps the previous snapshot, writes a free snapshot-too-large row and tells you to lower maxProductsPerStore or watch one collection at a time.
  • 90 days' retention. A store you stop watching falls out of the store by itself.
  • Nothing is stored that we do not compare. No descriptions, no SKUs, no image URLs, no updated_at.

Use cases

  • Competitor price tracking. Watch a rival's catalogue and get the price moves, not the catalogue. minPriceChangePct: 5 filters out currency-rounding noise.
  • MAP monitoring. Watch the retailers who carry your products and see the day one of them goes below your minimum advertised price — with oldPrice, newPrice and the percentage on the row.
  • Restock alerts. watch: ["restock"] on a store you resell from, on a 15-minute schedule, is a few dollars a month.
  • New-drop feeds. watch: ["new-product"], then push the rows straight into Slack or a webhook from the Integrations tab. Shop-level /products.json is sorted newest-published first, so a small maxProductsPerStore is a cheap rolling window on exactly the new arrivals.
  • Dropship repricing. Watch your suppliers and re-price when their prices or stock move, rather than re-scraping 5,000 products a day to find the 12 that changed.

How it compares

Against re-scraping a catalogue every day and diffing it yourself:

Re-scrape and diffThis Actor
What you pay forEvery product, every dayOnly the changes
10 stores × 5,000 products, daily1,500,000 rows a month≈ 4,500 rows a month
Where the diff livesYour database, your codeIn the Actor, with the ordering guarantees below
A half-broken storefrontA wave of false "removed" rowsHeld back by the collapse guard, reported if it repeats
A store you could not readUsually silence, or a failed runA free typed row saying which of five things went wrong

And the guarantees that are hard to get right yourself, which the test suite pins down: a change is delivered exactly once — the snapshot only advances past what actually reached your dataset, so a run that stops at your cost limit leaves the rest for tomorrow instead of losing them or billing them twice; rows are written before they are charged for; and an unread product is never a removed one.


Input reference

KeyTypeDefaultWhat it does
storesstring[]Domains, URLs, or a /collections/<handle> URL that scopes that entry to that collection. example.com and www.example.com are the same shop and share one history.
collectionsstring[][]Handles applied to every store. One store × 3 handles = 3 watched targets. A store named with a collection URL keeps its own.
productHandlesstring[][]Allowlist. Only these handles are watched, compared or billed.
watchstring[]the ten types aboveApplied before anything is billed. inventory-change and content-change are off by default.
minPriceChangePctinteger0Minimum size of a price move, in either direction. 5 is the recommendation for a daily schedule. Does not affect sale rows.
trackInventorybooleanfalseAlso read /products/<handle>.js for stock counts. One extra request per product.
maxInventoryLookupsinteger100Ceiling on those extra requests per run.
firstRunBehaviorenumbaseline-onlybaseline-only or emit-all. See above.
stateStoreNamestringshopify-store-monitor-stateThe named key-value store holding the snapshots. One per watchlist.
maxProductsPerStoreinteger2000Also the page size. Filling it suspends removal detection and flags the summary truncated. Max 10,000.
maxChangesPerStoreinteger2000The store-wide-sale guard. Changes past it are held for the next run, not lost.
outageGuardThresholdinteger10Below this many products in the previous snapshot, the collapse guard does not arm. 0 disables it.
requestTimeoutSecsinteger20Per-request timeout.
maxConcurrencyinteger3Stores in flight. Pages within one store are always sequential.
maxRunSecsinteger240Wall-clock budget for the whole run.
proxyConfigurationobject{ "useApifyProxy": true }Apify datacenter proxy. See Limits.

Output reference

rowType is change, store-summary or diagnostic. Every row carries every column; a column that does not apply is null.

Identity and scopeok, rowType, changeType, input, storeDomain, storeUrl, collectionHandle, source, sourceUrl.

The productproductId, handle, title, vendor, productType, tags, productUrl, imageUrl. On a removed-product row these are read back out of the snapshot, so tags and imageUrl are null: they are not stored between runs.

The variantvariantId, variantTitle, sku. sku is always null on a variant-removed row. SKUs are not stored between runs; doing so would add megabytes to a large catalogue's snapshot to fill one column on a rare row.

PricesoldPrice, newPrice, priceChangePct (signed, two decimals), oldCompareAtPrice, newCompareAtPrice, discountPct (on sale-started only), currency.

StockoldAvailable, newAvailable, oldInventory, newInventory, inventoryDelta, inventoryTracked. available is Shopify's own flag, and a store that lets customers buy past zero reports its variants as available. inventoryTracked is what makes a null count honest: true means the store counts this variant, false means it does not, null means it did not say.

ProvenancechangedFields (on content-change: which of title, tags, description, images), reposted, previousProductId, firstSeenAt, previousSeenAt (the age of the comparison), lastSeenAt, removedAt, daysSinceLastChange. Dates the Actor stores itself are kept to the day and returned as midnight UTC, because three timestamps per product at second precision is most of a megabyte on a large catalogue. daysSinceLastChange is product-scoped, not variant-scoped, for the same reason.

On store-summary rowsproductCount, variantCount, previousProductCount, productCountDelta, then newCount, removedCount, priceChangeCount, saleChangeCount, stockChangeCount, variantChangeCount, contentChangeCount, unchangedCount, changeCount; firstRun, truncated, changesTruncated; and the velocity block newLast7Days, removedLast7Days, changesLast7Days, newLast30Days, removedLast30Days, changesLast30Days, historyDays. The counts describe what you received, so they always reconcile with the change rows next to them. historyDays is there so a genuine zero is distinguishable from a window we have not watched long enough to fill.

On diagnostic rowsok: false, error in plain language, and errorType, one of: not-shopify, products-disabled, blocked, not-found, collection-not-found, timeout, invalid-input, state-locked, snapshot-too-large, state-write-failed, outage-suspected, changes-truncated, charge-limit, deadline, no-stores, run-failed. None of them is ever charged for.

Six dataset views are set up for you: Changes, Price moves, Stock moves, Catalogue moves, Store summaries and Problems.


Pricing

Pay per event. Three events, no per-product charge:

EventFREEBRONZESILVERGOLD
Monitoring run started (actor-start)$0.001$0.001$0.001$0.001
Store monitored (store-monitored)$0.002$0.002$0.002$0.002
Change detected (change-event)$0.001$0.001$0.0008$0.0006

$1.00 per 1,000 changes. store-monitored is charged once per store that was actually compared, however many products it holds.

RunBills
1 store, first run, baseline-only$0.003
1 store, first run, emit-all, 25 products (the prefill)$0.028
10 stores, quiet day, no changes$0.021
10 stores, typical day, 140 changes$0.161
10 stores daily for a month at that rate≈ $4.83

Remember that a price move and a sale move on the same variant are two rows and two charges. If you only want one, watch one of them.

What you are never charged for

SituationBilled?
A store that 404s, 403s, is not Shopify, or has products.json switched offNo
A store held back by the catalogue-collapse guardNo
A store the run never reached — maxRunSecs or your cost limitNo
A store whose snapshot could not be savedThe comparison is billed; the diagnostic row is not
Products and variants that did not changeNo
The products we looked at — there is no per-product chargeNo
Per-store store-summary rowsCovered by store-monitored, not billed again
A first (baseline) runactor-start + store-monitored only
A run that compared no store at allNothing, and the run is reported FAILED
A change already delivered in an earlier runNo — the snapshot advances past exactly what was delivered

Limits, and the ones that might bite

  • Some stores switch /products.json off. Headless storefronts and password-protected stores look like this. There is no other public endpoint that lists their catalogue, so those come back as a free products-disabled row and stay that way.
  • Some stores are behind a bot filter that refuses us. One well-known brand in the test set answers HTTP 403 with a CloudFront block page — and it did so through a US residential exit, so switching proxyConfiguration to the residential groups is not a reliable cure for that class of store. Those are free blocked rows. We rotate the exit IP once and then stop rather than hammering the shop.
  • Stock counts are null on most stores. Of three stores captured for this build, one does not track inventory at all (so a count would be meaningless), one publishes real counts, and one has inventory tracked but its theme withholds the field entirely. inventoryTracked tells you which case you are in. Never infer stock from available alone: a store with "continue selling when out of stock" reports available: true at zero.
  • Currency follows the exit IP. With Shopify Markets a storefront prices in the currency of the market it thinks you are in, so currency and every price column follow proxyConfiguration. Pin apifyProxyCountry if you need one fixed market. The currency itself costs nothing: it comes out of a response header we have already paid for.
  • Big catalogues want a collection. maxProductsPerStore bounds both the download and the snapshot. If a store is larger than your ceiling, removal detection is suspended for it and the summary row says truncated: true — an unread product is never reported as removed. Watching two or three collections instead of the whole catalogue is usually both cheaper and more useful.
  • Changing productHandles, collections or maxProductsPerStore changes what is being watched, so it can look like products appearing and disappearing. Change them deliberately.
  • content-change starts quiet. The content hashes are only stored while that type is watched, so the run after you switch it on records a baseline and reports nothing. That is deliberate: the alternative is one false row for every product in the catalogue.
  • The upstream format may change. Everything here reads public storefront endpoints that Shopify does not version. The parsers are written against real captures and are tolerant of missing fields, but a format change upstream can still turn readings into typed diagnostic rows. Those are free, and they say what happened.

Use it from an AI agent, or from code

One JSON object in, one flat array out — the shape agent runtimes want. The Actor runs with limited permissions, uses pay-per-event pricing and never enters Standby, so it works over the Apify MCP server and with x402 agentic payments. The Integrations tab pushes results to Slack, a webhook, Zapier, Make, Google Sheets, Snowflake or BigQuery.

curl -X POST "https://api.apify.com/v2/acts/insight.solutions~shopify-store-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"stores":["allbirds.com"],"watch":["price-drop","restock"],"minPriceChangePct":5,"maxProductsPerStore":250}'
# pip install apify-client
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("insight.solutions/shopify-store-monitor").call(run_input={
"stores": ["allbirds.com", "colourpop.com"],
"watch": ["price-drop", "price-increase", "sale-started", "restock", "out-of-stock", "new-product"],
"minPriceChangePct": 5,
"maxProductsPerStore": 2000,
"stateStoreName": "my-competitor-watchlist",
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if row.get("ok") and row["rowType"] == "change":
print(row["storeDomain"], row["changeType"], row["title"], row["variantTitle"],
row["oldPrice"], "->", row["newPrice"], row["currency"], row["productUrl"], sep=" | ")
elif row["rowType"] == "store-summary":
print(f'{row["storeDomain"]}: {row["changeCount"]} change(s), {row["productCount"]} products watched')

Keep one stateStoreName per watchlist and put the task on a schedule. The first run is a baseline; every run after it returns only what moved.


FAQ

Do I need a Shopify API key, an app, or the store's password? No. Every endpoint this Actor reads is one the store publishes to anyone with a browser: /products.json, /collections.json, /products/<handle>.js and /cart.js. There is no login, no Admin API and no checkout.

How do I know it will not miss a change? The snapshot only advances past changes that actually reached your dataset. If a run stops at your cost limit or at maxChangesPerStore half-way through a store, the changes it did not deliver are left unrecorded, so the next run finds them and reports them — once. The same is true if a push fails.

Why did my first run return only one row? Because firstRunBehavior is baseline-only: there was nothing to compare against yet. That run cost $0.003 and the next one will report changes. Set emit-all if you want the catalogue up front.

Why is inventory null? Either the store does not track inventory for that variant (inventoryTracked: false) or its theme does not publish the number (inventoryTracked: null). Both are common. available is still there and still honest.

Why did a product show as new-product with reposted: true? Its handle came back under a different product ID — a delete-and-recreate, usually by an app. It is reported once, as one new product, with the original first-seen date inherited, rather than as a removal plus an arrival.

A store came back nearly empty. Why were there no removed-product rows? The catalogue-collapse guard. A storefront that answers with a handful of a catalogue it held yesterday is far more likely to be half-broken than to have been emptied. The reading is held for one run and nothing is charged; if the next run sees the same thing, the removals are reported then.

Can I watch more than one collection of the same store? Yes. Put the handles in collections and each one becomes its own watched target with its own history and its own summary row — and its own store-monitored charge.

Does it work on myshopify.com domains? Yes, and on custom domains, and on a link to any page of the shop.


  • Public storefront endpoints only. /products.json, /collections.json, /products/<handle>.js and /cart.js are published by the store to any visitor. No login, no session, no account.
  • No cookies that identify anyone. The Actor sends no cookies at all. It reads one response header — the store's own cart_currency — and discards the rest.
  • No Admin API, no checkout, no cart mutation. Nothing is added to a cart, nothing is ordered, nothing is written to any store.
  • Merchant-published data only. Products, prices, variants and stock flags are what the merchant chose to publish. No personal data is collected, because none is exposed by these endpoints.
  • Rate-limited by design. Pages of one store are sequential and spaced 250–600 ms apart, and a store that refuses an exit IP is retried once and then left alone.
  • You are responsible for how you use the data, including any contractual or competition-law limits that apply to you.

Our other Actors

Every Insight Solutions Actor is pay-per-result with no browser, no login and no API key, and every one of them returns free diagnostic rows instead of billing for failures. Prices are per 1,000 results.

Video, audio & social

News, documents & the web

Business, finance & jobs

Apps & games