# Shopify Store Monitor: Price Drops, Restocks & New Products (`insight.solutions/shopify-store-monitor`) Actor

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.

- **URL**: https://apify.com/insight.solutions/shopify-store-monitor.md
- **Developed by:** [Insight Solutions](https://apify.com/insight.solutions) (community)
- **Categories:** E-commerce, Business, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.60 / 1,000 change reporteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

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

**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:

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

One change, one row:

```json
{
  "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-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.

| `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 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 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.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.

```bash
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}'
```

```python
## 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.

***

### Legal and data-protection notes

- **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**

- [YouTube Transcript API](https://apify.com/insight.solutions/youtube-transcript-api) — captions as timed segments, text, SRT or VTT, with language fallback and translation.
- [YouTube Comments API](https://apify.com/insight.solutions/youtube-comments-api) — comments and replies with likes, pinned and hearted flags, newest or top sort.
- [YouTube Channel API](https://apify.com/insight.solutions/youtube-channel-api) — a channel's videos, Shorts and live streams, plus YouTube search.
- [Podcast Search, Episodes & Charts API](https://apify.com/insight.solutions/podcast-api) — Apple Podcasts search, charts and full episode feeds.
- [Bluesky Scraper](https://apify.com/insight.solutions/bluesky-scraper) — profiles, posts, followers and follows from the public AT Protocol API.
- [Telegram Channel Scraper](https://apify.com/insight.solutions/telegram-channel-scraper) — posts, views and channel stats from public Telegram channels.
- [Substack Scraper](https://apify.com/insight.solutions/substack-scraper) — posts with full free text, comments and publication profiles.
- [Hacker News API](https://apify.com/insight.solutions/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](https://apify.com/insight.solutions/google-news-api) — news search and topic feeds with the publisher's real URL decoded.
- [Website to Markdown — Content Extractor for LLMs & RAG](https://apify.com/insight.solutions/website-content-extractor) — any site as clean Markdown, text and heading-aware chunks.
- [Internet Archive API](https://apify.com/insight.solutions/internet-archive-api) — archive.org search, item metadata, files and reviews.
- [Wayback Machine Toolkit](https://apify.com/insight.solutions/wayback-toolkit) — archived URL inventories, snapshots and text diffs between dates.
- [Website Technology Detector](https://apify.com/insight.solutions/website-tech-detector) — the tech stack behind any site, with the evidence for each detection.
- [Domain Intelligence API](https://apify.com/insight.solutions/domain-intelligence-api) — DNS, RDAP registration, TLS certificate and HTTP facts in one row per domain.
- [SEO Page Audit](https://apify.com/insight.solutions/seo-page-audit) — sitemap crawl with on-page checks, structured data and broken-link reports.
- [Keyword Suggestions API](https://apify.com/insight.solutions/keyword-suggestions-api) — Google, YouTube, Bing, Amazon and eBay autocomplete with alphabet and question expansions.
- [Website Contact Extractor](https://apify.com/insight.solutions/website-contact-extractor) — emails, phone numbers and social profiles from any list of websites.
- [Web Search Results API](https://apify.com/insight.solutions/web-search-api) — Bing and DuckDuckGo organic results with snippets, no key, no browser.
- [Company Enrichment API](https://apify.com/insight.solutions/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](https://apify.com/insight.solutions/congress-insider-trades-api) — STOCK Act periodic transaction reports and SEC Form 4 insider trades in one schema.
- [Federal Contracts, Grants & Lobbying API](https://apify.com/insight.solutions/federal-contracts-grants-api) — SAM.gov opportunities, USAspending awards, Grants.gov notices and Senate lobbying filings in one schema.
- [SEC EDGAR API](https://apify.com/insight.solutions/sec-edgar-api) — filings, XBRL financials and full-text search by ticker or CIK.
- [Clinical Trials & FDA API](https://apify.com/insight.solutions/clinical-trials-fda-api) — ClinicalTrials.gov studies plus openFDA recalls, labels, approvals, 510(k)s and adverse-event reports.
- [Product & Vehicle Recalls API](https://apify.com/insight.solutions/product-recalls-api) — CPSC, NHTSA, FDA and USDA recalls, vehicle complaints and ratings, plus a VIN decoder.
- [Y Combinator Companies, Batches & Founders](https://apify.com/insight.solutions/yc-companies-directory) — the YC directory with founders and social links, filterable by batch, industry and hiring status.
- [Career Site Jobs API](https://apify.com/insight.solutions/ats-jobs-api) — jobs straight from Greenhouse, Lever, Ashby, Workable and 10+ other ATS career sites.
- [New Job Postings Monitor](https://apify.com/insight.solutions/job-postings-monitor) — new, closed and changed postings on the career sites you watch.
- [Remote Jobs API](https://apify.com/insight.solutions/remote-jobs-api) — RemoteOK, Remotive, We Work Remotely, Himalayas, Jobicy and more in one schema, deduplicated.
- [Shopify Products API](https://apify.com/insight.solutions/shopify-products-api) — any Shopify store's catalogue, variants, prices and stock signals.

**Apps & games**

- [App Store & Google Play Reviews API](https://apify.com/insight.solutions/app-reviews-api) — reviews from both stores with ratings, versions and developer replies.
- [App Store Top Charts & App Search API](https://apify.com/insight.solutions/app-charts-api) — Apple top charts by country and genre, plus app search and details.
- [App Store Keyword Rank Tracker](https://apify.com/insight.solutions/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](https://apify.com/insight.solutions/steam-reviews-api) — Steam reviews with playtime, helpfulness and game details.
- [Steam Game Data API](https://apify.com/insight.solutions/steam-store-stats-api) — prices, tags, review scores, live player counts and top charts.

# Actor input Schema

## `stores` (type: `array`):

One entry per store. A bare domain (`allbirds.com`), a full URL (`https://www.allbirds.com`) and a `myshopify.com` domain all work, and so does a link to any page of the shop — a product page, a link with tracking parameters glued on. A collection URL such as `https://www.allbirds.com/collections/mens` narrows **that entry** to that collection. Redirects are followed, and `example.com` and `www.example.com` are the same shop: they share one watch history and are billed once.

## `watch` (type: `array`):

Which changes are worth a row. This is applied **before** anything is billed, so a type you did not tick is never charged for. `inventory-change` needs "Track per-variant stock" on. `content-change` is off by default because themed stores rewrite their own descriptions and image sets constantly.

## `minPriceChangePct` (type: `integer`):

Report a price drop or increase only when the move is at least this big, in either direction. `5` is a good setting for a daily schedule: it filters out the cent-level rounding that currency conversion and tax settings produce on some stores. It does not affect sale-started and sale-ended, which are about the compare-at price rather than the price.

## `firstRunBehavior` (type: `string`):

What to do the first time a store is seen, when there is nothing to compare against. `baseline-only` records the catalogue and writes just the per-store summary, so you are not billed for a backfill you did not ask for. `emit-all` reports every product in the catalogue as new — which is billed per product, so pair it with a small "Products per store" the first time.

## `trackInventory` (type: `boolean`):

Also read each watched product's own `/products/<handle>.js` endpoint, which is the only public one that publishes a stock count. This is one extra request per product, so it is bounded by "Stock lookups per run" and spent on new products, products that moved, and products we already hold a count for. **Not every store publishes counts**: a store that does not track inventory, or whose theme withholds the field, returns `inventory: null` with `inventoryTracked` saying which of the two it was.

## `collections` (type: `array`):

Collection handles applied to **every** store in the list, so one store with three handles is three watched targets, each with its own history and its own summary row. A handle (`mens`), a path (`/collections/mens`) or a whole collection URL all work. A store you named with a collection URL keeps its own handle. Leave empty to watch each store's whole catalogue.

## `productHandles` (type: `array`):

An allowlist. When it is not empty, only these handles are watched: everything else in the catalogue is ignored, never compared and never billed. The handle is the last part of a product URL — `wool-runner-mizzle` in `/products/wool-runner-mizzle`. Changing this list between runs looks like products appearing and disappearing, because it changes what is being watched.

## `maxInventoryLookups` (type: `integer`):

Hard ceiling on how many products per run get the extra stock request, when "Track per-variant stock counts" is on. Ignored otherwise. This is the only part of a run whose cost scales with anything other than the number of stores, which is why it is capped rather than unbounded.

## `maxProductsPerStore` (type: `integer`):

Ceiling on how many products are watched per store. Keep it comfortably above the size of the catalogues you watch: if a store fills the ceiling, the products beyond it were not read, and unread is not removed — so removal detection is suspended for that store and its summary row is flagged `truncated`. It is also the page size, so asking for 25 downloads about 160 KB instead of 1.6 MB. See *Limits* in the README for snapshot sizing.

## `maxChangesPerStore` (type: `integer`):

The guard against a store-wide sale. Twenty per cent off a 5,000-product catalogue at ten variants each is 50,000 billable rows, and nobody means to buy that. Changes past this ceiling are not recorded in the snapshot either, so the next run reports them — each change is delivered, and billed, exactly once.

## `outageGuardThreshold` (type: `integer`):

If a store suddenly returns almost nothing and its previous snapshot held more than this many products, the run treats it as a storefront fault rather than a mass unpublish: no change events, the old snapshot is kept, nothing is charged, and a diagnostic row explains what happened. If the next run sees the same thing, the changes are reported then. Set to 0 to disable the guard — not recommended.

## `stateStoreName` (type: `string`):

Name of the key-value store that holds the previous run's catalogue snapshot. The Actor creates it on its first run and reads it back on every run after that — that memory is what makes this a monitor rather than a scraper. Keep one store per watchlist: two schedules sharing a store would compare against the same snapshot, so the second stops with a clear message instead of double-reporting. Do not point this at a store you made by hand; the Actor cannot read those.

## `requestTimeoutSecs` (type: `integer`):

Per-HTTP-request timeout. Raise it only if you are watching very large catalogues through a slow proxy.

## `maxConcurrency` (type: `integer`):

How many stores are read at once. Each parallel worker keeps its own proxy session, so a store that gets blocked burns only its own exit IP. Pages *within* one store are sequential and spaced 250–600 ms apart, so this is the only lever on speed for a long watchlist.

## `maxRunSecs` (type: `integer`):

Wall-clock budget for the whole run, so one unresponsive storefront cannot hold a scheduled run open until the platform timeout. When it is reached the Actor stops fetching, keeps and bills for everything already delivered, and writes a free diagnostic row for each store it never reached — those stores keep their snapshots and are not charged for.

## `proxyConfiguration` (type: `object`):

Shopify serves `/products.json` to anyone, so this Actor defaults to Apify's datacenter proxy — the cheap one — and its cost is already inside the event prices. Some large brands sit behind a bot filter that refuses datacenter address ranges; those come back as free `blocked` rows. Switching to the RESIDENTIAL group helps with some of them and not with others, and it is much slower on a large catalogue.

## Actor input object example

```json
{
  "stores": [
    "allbirds.com",
    "https://www.allbirds.com/collections/mens"
  ],
  "watch": [
    "new-product",
    "removed-product",
    "price-drop",
    "price-increase",
    "sale-started",
    "sale-ended",
    "restock",
    "out-of-stock",
    "variant-added",
    "variant-removed"
  ],
  "minPriceChangePct": 0,
  "firstRunBehavior": "emit-all",
  "trackInventory": false,
  "collections": [
    "mens",
    "sale"
  ],
  "productHandles": [
    "mens-wool-runner-mizzle"
  ],
  "maxInventoryLookups": 100,
  "maxProductsPerStore": 25,
  "maxChangesPerStore": 2000,
  "outageGuardThreshold": 10,
  "stateStoreName": "shopify-store-monitor-state",
  "requestTimeoutSecs": 20,
  "maxConcurrency": 3,
  "maxRunSecs": 240,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `changes` (type: `string`):

One row per change since the previous run, plus one summary row per watched store. Free diagnostic rows explain anything that could not be read. Delivered as JSON items in the default dataset.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "stores": [
        "https://www.allbirds.com"
    ],
    "watch": [
        "new-product",
        "removed-product",
        "price-drop",
        "price-increase",
        "sale-started",
        "sale-ended",
        "restock",
        "out-of-stock",
        "variant-added",
        "variant-removed"
    ],
    "minPriceChangePct": 0,
    "firstRunBehavior": "emit-all",
    "trackInventory": false,
    "collections": [],
    "productHandles": [],
    "maxInventoryLookups": 100,
    "maxProductsPerStore": 25,
    "maxChangesPerStore": 2000,
    "outageGuardThreshold": 10,
    "stateStoreName": "shopify-store-monitor-state",
    "requestTimeoutSecs": 20,
    "maxConcurrency": 3,
    "maxRunSecs": 240,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("insight.solutions/shopify-store-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "stores": ["https://www.allbirds.com"],
    "watch": [
        "new-product",
        "removed-product",
        "price-drop",
        "price-increase",
        "sale-started",
        "sale-ended",
        "restock",
        "out-of-stock",
        "variant-added",
        "variant-removed",
    ],
    "minPriceChangePct": 0,
    "firstRunBehavior": "emit-all",
    "trackInventory": False,
    "collections": [],
    "productHandles": [],
    "maxInventoryLookups": 100,
    "maxProductsPerStore": 25,
    "maxChangesPerStore": 2000,
    "outageGuardThreshold": 10,
    "stateStoreName": "shopify-store-monitor-state",
    "requestTimeoutSecs": 20,
    "maxConcurrency": 3,
    "maxRunSecs": 240,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("insight.solutions/shopify-store-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "stores": [
    "https://www.allbirds.com"
  ],
  "watch": [
    "new-product",
    "removed-product",
    "price-drop",
    "price-increase",
    "sale-started",
    "sale-ended",
    "restock",
    "out-of-stock",
    "variant-added",
    "variant-removed"
  ],
  "minPriceChangePct": 0,
  "firstRunBehavior": "emit-all",
  "trackInventory": false,
  "collections": [],
  "productHandles": [],
  "maxInventoryLookups": 100,
  "maxProductsPerStore": 25,
  "maxChangesPerStore": 2000,
  "outageGuardThreshold": 10,
  "stateStoreName": "shopify-store-monitor-state",
  "requestTimeoutSecs": 20,
  "maxConcurrency": 3,
  "maxRunSecs": 240,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call insight.solutions/shopify-store-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,insight.solutions/shopify-store-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Zv20hnwJPGhLqPbnl/builds/irXcOe1gszdhq1g4d/openapi.json
