# Shopify Price & Stock Change Monitor (`studio_ojeda/shopify-store-price-stock-change-monitor`) Actor

Reports only what changed in public Shopify catalogs since the last run: new and removed products, price changes and stock changes.

- **URL**: https://apify.com/studio_ojeda/shopify-store-price-stock-change-monitor.md
- **Developed by:** [Tony Ojeda](https://apify.com/studio_ojeda) (community)
- **Stats:** 2 total users, 1 monthly users, 77.8% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$5.00 / 1,000 change detecteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Shopify Price & Stock Change Monitor

Watch public Shopify stores and get **only what changed** since your last run: new products, removed products, price changes (with old price, new price and percent change) and stock changes (in stock to sold out, and back). No giant catalog dumps to diff yourself.

### Who it is for

- Ecommerce teams tracking competitor pricing and stock-outs
- Resellers and deal watchers who want an alert when a price drops or an item returns to stock
- Analysts building price histories without re-processing full catalogs every day

### How it works

1. You give it a list of public Shopify store URLs.
2. For each store it reads `robots.txt`, then the store's public `/products.json` catalog, politely (one request per second per store at most).
3. **Before your first run, pick a State storage in the input** (a key-value store in your own account; create one under Storage if you have none) and use the same one every time. Apify runs Actors from the Store with limited permissions, so this is how the Actor remembers each catalog between runs. Without it every run starts a new baseline and reports no changes.
4. The **first run for a store only saves a baseline** and outputs one `summary` item with status `baseline_saved`. No change items are produced.
5. Every later run compares the fresh catalog with the saved one and outputs one dataset item per change, plus one `summary` item per store.

Schedule it daily or hourly with Apify Schedules and connect the dataset to a webhook, Zapier, Make, Google Sheets or your own pipeline.

> The two stores prefilled in the input are only a **default test input**. They are well-known public Shopify stores that this Actor's author does not control and is not affiliated with. Replace them with the stores you want to monitor, and make sure you are allowed to.

### Input

```json
{
  "storeUrls": ["https://www.allbirds.com", "https://www.brooklinen.com"],
  "maxStores": 25,
  "maxProductsPerStore": 5000,
  "maxPagesPerStore": 20,
  "minDelaySeconds": 1,
  "maxChangesPerStore": 1000,
  "maxCatalogShrinkPercent": 50,
  "stateStoreName": "shopify-price-stock-monitor-state"
}
```

| Field | Default | Meaning |
|---|---|---|
| `storeUrls` | two example stores | Public Shopify stores, `https://` only, canonical address (redirects are not followed). |
| `maxStores` | 25 | Stores beyond this are skipped with a note. |
| `maxProductsPerStore` | 5000 | Larger catalogs are reported as too large, never partially monitored. |
| `maxPagesPerStore` | 20 | Pages of up to 250 products. |
| `minDelaySeconds` | 1 | Pause between requests to one store. Cannot be lower than 1. |
| `maxChangesPerStore` | 1000 | Safety cap on billable items per store per run (for example after a store-wide sale). Price and stock changes are emitted before new and removed products. Changes beyond the cap are **held back, not lost**: they are reported on the following runs. |
| `maxCatalogShrinkPercent` | 50 | Glitch guard. If a store's catalog is suddenly smaller than last time by more than this (for example an empty or maintenance response), nothing is reported and the old data is kept; it is accepted only if the next run sees about the same size (within 10%). 100 turns the guard off. |
| `stateStore` | none | **Pick a key-value store here** (create one under Storage if you have none) and keep it the same for every run and schedule. It remembers each store's last snapshot so later runs report only what changed. Apify runs Actors from other users with limited permissions, which cannot open storage in your account unless you pick it here. Without it the Actor still runs, but every run saves a fresh baseline and reports no changes (a `notice` item says so). |
| `stateStoreName` | `shopify-price-stock-monitor-state` | Used only when `stateStore` is empty and the run is allowed to open a named store (the Actor's owner, or a full-permission run). Lowercase letters, digits and hyphens. Change it to start over with fresh baselines. |

### Output

Every dataset item has a `type`. These four are **change items** (billable):

```json
{"type": "price_change", "store": "https://www.example-store.com", "observed_at": "2026-01-01T12:00:00+00:00",
 "product_id": 1001, "handle": "wool-runner", "title": "Wool Runner",
 "variant_id": 5001, "variant_title": "M8", "sku": "SKU-5001",
 "old_price": "98.00", "new_price": "88.20", "percent_change": -10.0, "compare_at_price": null}
```

```json
{"type": "availability_change", "store": "https://www.example-store.com", "observed_at": "2026-01-01T12:00:00+00:00",
 "product_id": 1001, "handle": "wool-runner", "title": "Wool Runner",
 "variant_id": 5002, "variant_title": "M9", "sku": "SKU-5002",
 "old_available": true, "new_available": false, "change": "sold_out"}
```

`new_product` and `removed_product` carry the product's `id`, `handle`, `title`, `vendor`, `product_type`, `updated_at` and its `variants` (`id`, `title`, `sku`, `price`, `compare_at_price`, `available`). Removed products show their last known data.

These are **not billable**:

```json
{"type": "summary", "store": "https://www.example-store.com", "observed_at": "2026-01-01T12:00:00+00:00",
 "status": "baseline_saved", "products_seen": 412, "pages_fetched": 2, "malformed_products_skipped": 0,
 "changes_found": 0, "changes_emitted": 0, "changes_suppressed": 0, "changes_undelivered": 0,
 "counts": {"new_product": 0, "removed_product": 0, "price_change": 0, "availability_change": 0}}
```

`status` is `baseline_saved` on a store's first run and `checked` afterwards. If the run has no storage that survives between runs (no State storage picked), `status` is `not_monitoring`, the first dataset item is a `notice` with code `state_not_persisted`, and the run's status message starts with `NOT MONITORING`: no changes can be detected until you pick a State storage. `changes_emitted` is exactly the number of change items delivered (and billed) for that store; `changes_suppressed` counts changes held back by `maxChangesPerStore`; `changes_undelivered` counts changes that could not be delivered (spending limit reached, billing could not be verified, or a dataset error). Held-back and undelivered changes are re-detected and reported on the next run, and the summary `note` says so. A `skipped` item (for example the store's robots.txt disallows the catalog) or an `error` item (invalid URL, not a Shopify store, store unreachable, ...) carries a `code` and a plain `message`. One failing store never fails the run.

Prices are the strings Shopify publishes, in the store's own currency (the catalog does not state the currency). Nothing else is collected: no images, no descriptions, no customer or order data.

### Pricing

Pay per event:

- **Change detected**: one billable event per change item (a new product, a removed product, a price change on one variant, or a stock change on one variant).
- **Summaries, skipped stores, errors and baselines**: not billable.

A run with no changes produces no per-change charges. Apify may still apply its own standard platform charges for a run (for example a start event on pay-per-event Actors); check the pricing shown on the Actor page. Use `maxChangesPerStore` and your Apify spending limit to cap a worst case.

Billing and delivery happen together: a change item is pushed to the dataset only when the platform confirms the charge. If your spending limit is reached mid-run, changes that could not be charged are held back and delivered on the next run after you raise the limit. If the platform returns no charge information (for example the Actor is run outside pay-per-event pricing), the Actor treats billing as unverified: it stops emitting change items for that run, says `billing_unverified` in the run status message, and keeps the changes for the next run (at most one item that revealed the problem may already be in the dataset and will be reported again). A product that is delisted and later relisted produces two billed items (removed, then new).

### Limitations

- Only public stores that expose `/products.json`. Password-protected stores, stores that disable the catalog, and non-Shopify sites are reported as errors.
- Catalogs over 5,000 products (configurable up to 20,000), over 250 variants per product or over 50,000 variants in total are not monitored (reported as too large); a partial catalog would create false "removed" items.
- A sudden large shrink (an empty or maintenance response) is treated as a glitch: reported as an error item, nothing is billed, and the old data is kept. The smaller catalog is accepted only if the next run sees about the same size (within 10%); a different size starts the check over. There is no matching guard for the very first run or for sudden growth: take your baseline on a healthy day. A first baseline that is empty, or has exactly a multiple of 250 products or the product cap, is flagged in the summary note because it may be incomplete.
- Each store has a time budget (5 minutes) and each run has one (50 minutes); stores that do not fit are reported as errors or skipped and can be run again.
- Shopify can show different prices by visitor country (Shopify Markets). The Actor runs from Apify data centers without a country setting, so if a store prices by region you may see price changes caused by where the run happened, not by the store. Check a few results before relying on bulk price alerts.
- Do not run two overlapping runs against the same state storage; both would report and bill the same changes.
- Interrupted runs: before delivering, the Actor saves the changes it is about to deliver as a pending list in the saved state, and the next run delivers pending changes first. This means a crash cannot lose changes, but items delivered shortly before the crash may be delivered (and billed) again. If the saved state cannot be updated after delivery, the store summary says so.
- **Spreadsheet formula injection:** product titles, SKUs and vendors are store-controlled text and are passed through unchanged (only control characters are removed and length is limited). This Actor does not neutralize values that start with `=`, `+`, `-` or `@`. If you export the dataset to CSV or Excel, neutralize or review such cells yourself before opening the file.
- Redirects are not followed. Enter the store's canonical address (for example the `www` form if the bare domain redirects).
- Only the four change types are reported. New or removed variants of an existing product are not.
- Changes are detected between runs, not in real time. If a price moves twice between runs you see the net change.
- Prices carry no currency code and can vary by market or logged-in customer; you see what the public catalog shows.

### Responsible use

- Only public catalog data is read: one `robots.txt` request and the public `/products.json` pages. No other URLs are requested.
- It respects `robots.txt` (RFC 9309): if a store disallows `/products.json` (or any catalog page) for crawlers it is skipped and the reason is shown; a robots.txt server error (5xx) is treated as "disallow all"; a missing robots.txt (4xx) as "allowed". The most specific user-agent group wins.
- It is rate limited (at most one request per second per store), backs off on errors and honors `Retry-After`. It identifies itself in its User-Agent. Every request has a hard total time limit and size limit, and connections go only to public internet addresses.
- Site owners can block it with a `User-agent: StudioOjedaShopifyMonitor` / `Disallow: /` rule in robots.txt; see https://studioojeda.com/bots.
- No personal data is collected.
- You are responsible for complying with each store's terms of service and with applicable law (including competition, database and data protection rules in your jurisdiction). Do not use this Actor against a store that has asked you not to.
- This Actor is not affiliated with, endorsed by or sponsored by Shopify Inc. "Shopify" is a trademark of its owner and is used only to describe what the Actor reads.

### Contact

Bot information for site owners (including how to block this crawler in robots.txt with the token `StudioOjedaShopifyMonitor`): https://studioojeda.com/bots. Contact: bots@studioojeda.com.

# Actor input Schema

## `storeUrls` (type: `array`):

Public Shopify store addresses, https only, for example https://www.example-store.com. Use the store's canonical address; redirects are not followed.

## `maxStores` (type: `integer`):

Stores beyond this number are skipped with a note.

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

Stores with a larger catalog are reported as too large (never partially monitored, which would cause false removals).

## `maxPagesPerStore` (type: `integer`):

Each page holds up to 250 products.

## `minDelaySeconds` (type: `number`):

Minimum pause between requests to one store. Cannot be lower than 1.

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

Safety cap on billable items per store per run, for example after a store-wide repricing. Price and stock changes are delivered before new and removed products. Changes beyond the cap are held back and reported on the following runs; none are lost.

## `maxCatalogShrinkPercent` (type: `integer`):

Glitch guard. If a store's catalog is suddenly smaller than last time by more than this percentage (for example an empty or maintenance response), nothing is reported and the old data is kept. The smaller catalog is accepted only if the next run sees about the same size (within 10%). 100 turns the guard off.

## `stateStore` (type: `string`):

The key-value store that remembers each store's last catalog so later runs can report only what changed. Pick one (create it under Storage if needed) and keep it the same for every run and schedule. Without it the Actor still runs, but every run starts a fresh baseline and reports no changes.

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

Named key-value store holding the last catalog snapshot of each store. Lowercase letters, digits and hyphens. Keep it the same between runs; change it to start over with fresh baselines.

## Actor input object example

```json
{
  "storeUrls": [
    "https://www.allbirds.com",
    "https://www.brooklinen.com"
  ],
  "maxStores": 25,
  "maxProductsPerStore": 5000,
  "maxPagesPerStore": 20,
  "minDelaySeconds": 1,
  "maxChangesPerStore": 1000,
  "maxCatalogShrinkPercent": 50,
  "stateStoreName": "shopify-price-stock-monitor-state"
}
```

# Actor output Schema

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

No description

## `allItems` (type: `string`):

No description

# 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 = {
    "storeUrls": [
        "https://www.allbirds.com",
        "https://www.brooklinen.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("studio_ojeda/shopify-store-price-stock-change-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 = { "storeUrls": [
        "https://www.allbirds.com",
        "https://www.brooklinen.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("studio_ojeda/shopify-store-price-stock-change-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 '{
  "storeUrls": [
    "https://www.allbirds.com",
    "https://www.brooklinen.com"
  ]
}' |
apify call studio_ojeda/shopify-store-price-stock-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,studio_ojeda/shopify-store-price-stock-change-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/IQ6fbg7kAIFe5KDHH/builds/BVQ4faLnweqxgBukZ/openapi.json
