Shopify Store Monitor: Price Drops, Restocks & New Products
Pricing
from $0.60 / 1,000 change reporteds
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
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 code — client.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-productrows and 1store-summaryrow. That is the baseline: the Actor now knows what the store looked like. - Run 2, tomorrow or in five minutes, returns 1
store-summaryrow 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.
changeType | Level | Fires when |
|---|---|---|
new-product | product | A product ID appears that the previous snapshot did not hold |
removed-product | product | A product ID in the snapshot is gone, and the catalogue was read in full |
price-drop | variant | A variant's price fell, by at least minPriceChangePct |
price-increase | variant | A variant's price rose, by at least minPriceChangePct |
sale-started | variant | The compare-at price went from "not above the price" to strictly above it |
sale-ended | variant | The compare-at price stopped being above the price |
restock | variant | available went from false to true |
out-of-stock | variant | available went from true to false |
variant-added | variant | A variant ID appeared on a product that already existed |
variant-removed | variant | A variant ID left a product that is still there |
inventory-change | variant | A published stock count moved (needs trackInventory) |
content-change | product | The 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 onestore-summaryrow withfirstRun: 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-allreports every product in the catalogue asnew-product. Those are billed per row, so pair it with a smallmaxProductsPerStorethe 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
stateStoreNameat 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/mensandshop.com/collections/saleare 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 freesnapshot-too-largerow and tells you to lowermaxProductsPerStoreor 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: 5filters 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,newPriceand 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.jsonis sorted newest-published first, so a smallmaxProductsPerStoreis 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 diff | This Actor | |
|---|---|---|
| What you pay for | Every product, every day | Only the changes |
| 10 stores × 5,000 products, daily | 1,500,000 rows a month | ≈ 4,500 rows a month |
| Where the diff lives | Your database, your code | In the Actor, with the ordering guarantees below |
| A half-broken storefront | A wave of false "removed" rows | Held back by the collapse guard, reported if it repeats |
| A store you could not read | Usually silence, or a failed run | A 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
| Key | Type | Default | What it does |
|---|---|---|---|
stores | string[] | — | 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. |
collections | string[] | [] | Handles applied to every store. One store × 3 handles = 3 watched targets. A store named with a collection URL keeps its own. |
productHandles | string[] | [] | Allowlist. Only these handles are watched, compared or billed. |
watch | string[] | the ten types above | Applied before anything is billed. inventory-change and content-change are off by default. |
minPriceChangePct | integer | 0 | Minimum size of a price move, in either direction. 5 is the recommendation for a daily schedule. Does not affect sale rows. |
trackInventory | boolean | false | Also read /products/<handle>.js for stock counts. One extra request per product. |
maxInventoryLookups | integer | 100 | Ceiling on those extra requests per run. |
firstRunBehavior | enum | baseline-only | baseline-only or emit-all. See above. |
stateStoreName | string | shopify-store-monitor-state | The named key-value store holding the snapshots. One per watchlist. |
maxProductsPerStore | integer | 2000 | Also the page size. Filling it suspends removal detection and flags the summary truncated. Max 10,000. |
maxChangesPerStore | integer | 2000 | The store-wide-sale guard. Changes past it are held for the next run, not lost. |
outageGuardThreshold | integer | 10 | Below this many products in the previous snapshot, the collapse guard does not arm. 0 disables it. |
requestTimeoutSecs | integer | 20 | Per-request timeout. |
maxConcurrency | integer | 3 | Stores in flight. Pages within one store are always sequential. |
maxRunSecs | integer | 240 | Wall-clock budget for the whole run. |
proxyConfiguration | object | { "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 scope — ok, rowType, changeType, input, storeDomain, storeUrl, collectionHandle, source, sourceUrl.
The product — productId, 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 variant — variantId, 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.
Prices — oldPrice, newPrice, priceChangePct (signed, two decimals), oldCompareAtPrice, newCompareAtPrice, discountPct (on sale-started only), currency.
Stock — oldAvailable, 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.
Provenance — changedFields (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 rows — productCount, 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 rows — ok: 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:
| Event | FREE | BRONZE | SILVER | GOLD |
|---|---|---|---|---|
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.
| Run | Bills |
|---|---|
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
| Situation | Billed? |
|---|---|
A store that 404s, 403s, is not Shopify, or has products.json switched off | No |
| A store held back by the catalogue-collapse guard | No |
A store the run never reached — maxRunSecs or your cost limit | No |
| A store whose snapshot could not be saved | The comparison is billed; the diagnostic row is not |
| Products and variants that did not change | No |
| The products we looked at — there is no per-product charge | No |
Per-store store-summary rows | Covered by store-monitored, not billed again |
| A first (baseline) run | actor-start + store-monitored only |
| A run that compared no store at all | Nothing, and the run is reported FAILED |
| A change already delivered in an earlier run | No — the snapshot advances past exactly what was delivered |
Limits, and the ones that might bite
- Some stores switch
/products.jsonoff. 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 freeproducts-disabledrow 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
proxyConfigurationto the residential groups is not a reliable cure for that class of store. Those are freeblockedrows. 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.
inventoryTrackedtells you which case you are in. Never infer stock fromavailablealone: a store with "continue selling when out of stock" reportsavailable: trueat zero. - Currency follows the exit IP. With Shopify Markets a storefront prices in the currency of the market it thinks you are in, so
currencyand every price column followproxyConfiguration. PinapifyProxyCountryif 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.
maxProductsPerStorebounds both the download and the snapshot. If a store is larger than your ceiling, removal detection is suspended for it and the summary row saystruncated: 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,collectionsormaxProductsPerStorechanges what is being watched, so it can look like products appearing and disappearing. Change them deliberately. content-changestarts 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-clientfrom apify_client import ApifyClientclient = 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.
Legal and data-protection notes
- Public storefront endpoints only.
/products.json,/collections.json,/products/<handle>.jsand/cart.jsare 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
- YouTube Transcript API — captions as timed segments, text, SRT or VTT, with language fallback and translation.
- YouTube Comments API — comments and replies with likes, pinned and hearted flags, newest or top sort.
- YouTube Channel API — a channel's videos, Shorts and live streams, plus YouTube search.
- Podcast Search, Episodes & Charts API — Apple Podcasts search, charts and full episode feeds.
- Bluesky Scraper — profiles, posts, followers and follows from the public AT Protocol API.
- Telegram Channel Scraper — posts, views and channel stats from public Telegram channels.
- Substack Scraper — posts with full free text, comments and publication profiles.
- Hacker News API — stories, comments, users, front page and a structured "Who is hiring?" parser from the official HN APIs.
News, documents & the web
- Google News Search, Topics & Real Article URLs — news search and topic feeds with the publisher's real URL decoded.
- Website to Markdown — Content Extractor for LLMs & RAG — any site as clean Markdown, text and heading-aware chunks.
- Internet Archive API — archive.org search, item metadata, files and reviews.
- Wayback Machine Toolkit — archived URL inventories, snapshots and text diffs between dates.
- Website Technology Detector — the tech stack behind any site, with the evidence for each detection.
- Domain Intelligence API — DNS, RDAP registration, TLS certificate and HTTP facts in one row per domain.
- SEO Page Audit — sitemap crawl with on-page checks, structured data and broken-link reports.
- Keyword Suggestions API — Google, YouTube, Bing, Amazon and eBay autocomplete with alphabet and question expansions.
- Website Contact Extractor — emails, phone numbers and social profiles from any list of websites.
- Web Search Results API — Bing and DuckDuckGo organic results with snippets, no key, no browser.
- Company Enrichment API — a domain in, a company profile out: firmographics, contacts, tech stack, DNS and hiring signal.
Business, finance & jobs
- Congress & Insider Trades API — STOCK Act periodic transaction reports and SEC Form 4 insider trades in one schema.
- Federal Contracts, Grants & Lobbying API — SAM.gov opportunities, USAspending awards, Grants.gov notices and Senate lobbying filings in one schema.
- SEC EDGAR API — filings, XBRL financials and full-text search by ticker or CIK.
- Clinical Trials & FDA API — ClinicalTrials.gov studies plus openFDA recalls, labels, approvals, 510(k)s and adverse-event reports.
- Product & Vehicle Recalls API — CPSC, NHTSA, FDA and USDA recalls, vehicle complaints and ratings, plus a VIN decoder.
- Y Combinator Companies, Batches & Founders — the YC directory with founders and social links, filterable by batch, industry and hiring status.
- Career Site Jobs API — jobs straight from Greenhouse, Lever, Ashby, Workable and 10+ other ATS career sites.
- New Job Postings Monitor — new, closed and changed postings on the career sites you watch.
- Remote Jobs API — RemoteOK, Remotive, We Work Remotely, Himalayas, Jobicy and more in one schema, deduplicated.
- Shopify Products API — any Shopify store's catalogue, variants, prices and stock signals.
Apps & games
- App Store & Google Play Reviews API — reviews from both stores with ratings, versions and developer replies.
- App Store Top Charts & App Search API — Apple top charts by country and genre, plus app search and details.
- App Store Keyword Rank Tracker — where any app ranks for any keyword on the App Store and Google Play, with rank changes and ASO suggestions.
- Steam Reviews API — Steam reviews with playtime, helpfulness and game details.
- Steam Game Data API — prices, tags, review scores, live player counts and top charts.