# Shopify Variant Price & Stock Changes (`natstandridge/shopify-variant-change-monitor`) Actor

Monitor known public Shopify products at variant level. Compare prices, compare-at prices and purchasability with a prior snapshot, with currency checks and honest coverage reports.

- **URL**: https://apify.com/natstandridge/shopify-variant-change-monitor.md
- **Developed by:** [Nathaniel Standridge](https://apify.com/natstandridge) (community)
- **Categories:** E-commerce
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 shopify variant observations

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 Variant Price & Stock Changes

Track price changes and public availability for known Shopify products. Each delivered variant includes its current price, compare-at price, currency, availability, and an explicit comparison with your previous snapshot. Use it for recurring competitor checks and catalog monitoring.

### Start with a watchlist

Supply 1–30 public HTTPS product URLs in `productUrls`, for example a URL ending in `/products/your-product-handle`. Locale paths such as `/en-ca/products/your-product-handle` are preserved. Collection URLs, headless storefronts without the Shopify Ajax API, password-protected shops, authenticated prices, and currency or country query overrides are unsupported. Use the final storefront hostname; redirects are reported instead of followed.

On your first run, leave `previousSnapshot` empty. Export the dataset as JSON after the run. On the next run, paste those rows into `previousSnapshot` to compare matching variant IDs within the same storefront and locale context. The Actor does not automatically fetch previous runs or schedule itself.

### Output and comparison

One dataset row represents one current, validated variant. `priceRaw` and `compareAtPriceRaw` preserve the exact Shopify Ajax integer amounts; `price` and `compareAtPrice` divide those values by 100 as decimal strings. No currency conversion is performed. Currency comes from the same locale's read-only `cart.js` response; its temporary anonymous session cookies are reused only for that product request and never saved.

- `changed`: at least one of price, compare-at price or availability differs; `changes` gives field names and before/after values.
- `unchanged`: those three fields match a valid older observation in the same context and currency.
- `first_seen`: no matching prior row was supplied. This does not mean a newly launched product or variant.
- `unknown`: currency is unavailable, the baseline is malformed, duplicated, not older, or uses another context/currency. No change is invented.

`available` is Shopify's public purchasable flag. It is not an inventory quantity, reservation or guarantee of stock. Backorders, selling plans, discounts, shipping, taxes and checkout rules can affect what a buyer can purchase. Subscription prices are not compared. Rows disclose `requiresSellingPlan` when the source provides it.

Missing variants are never labeled removed. Failed sources produce no variant rows and no per-row charges. The separate `OUTPUT` report lists every input, failure code, coverage warning and delivery count. A valid empty product produces a complete empty result. Invalid variants are omitted and make coverage partial. Shopify limits this API to 250 variants; a response at that ceiling is explicitly marked potentially truncated.

### Limits and billing

The default is 500 variants and 120 seconds of work. Maximums are 2,000 variants and 180 seconds, with a hard 20-second budget for each product's currency/product fetch. Keep the platform timeout at least 120 seconds above the work budget. Public Ajax GETs make at most three attempts for temporary network failures or HTTP 429, 500, 502, 503 and 504. Retries wait at least 1 then 2 seconds, respect a longer `Retry-After`, and stop when the remaining budget cannot accommodate that wait. A currency lookup with an outstanding server-requested pause stops that product rather than immediately requesting another endpoint on the same shop. Currency retries and socket timeouts use at most 8 seconds or half the remaining budget, whichever is smaller; a slow DNS lookup or streamed body can consume more, within the shared hard limit. Missing products, denied access, recognized access challenges, redirects and invalid responses are reported without retries or bypasses. Only documented public Ajax GET endpoints are requested; no cart updates, login or checkout operations occur.

Price: **$1 per 1,000 delivered variant rows** ($0.001 per row), including unchanged and currency-unknown observations. No startup fee or charge for source failures. Set Apify's maximum charge to control spend. Each row uses only Apify's native dataset-item billing event.

The result plan is frozen before delivery. Dataset writes disable automatic retries, retain a pending checkpoint and reconcile already delivered IDs on restart. An ambiguous unconfirmed write stops rather than risking duplicate rows or charges. Resuming the same run continues the original observation; start a new run for fresh data.

### Data scope and reliability

The observation is the anonymous public storefront view from the Actor's network location, not a chosen buyer country or authenticated market. The locale URL and currency are part of the comparison checks; even matching currency does not establish identical tax or discount treatment over time. Reported changes are differences between observations, not proof of a global seller price edit. Regional market rules can change prices even when the URL and currency match. Product and currency requests are close together, not an atomic store snapshot. Protect the previous snapshot as your own business data.

Official references: [Ajax Product API](https://shopify.dev/docs/api/ajax/reference/product), [Ajax Cart API](https://shopify.dev/docs/api/ajax/reference/cart), [Ajax requirements and limits](https://shopify.dev/docs/api/ajax), [Shopify monetary units](https://shopify.dev/docs/api/liquid/objects/product#product-price).

### Development

Run `uv run python -B -m unittest discover -s tests -v` inside this Actor directory. Dependencies and the Docker base are pinned. Tests cover compatible/incompatible comparisons, malformed sources, bounds, SSRF protection, deadlines, result caps, lost write responses, restart reconciliation and the actual pinned cloud client's retry behavior.

# Actor input Schema

## `productUrls` (type: `array`):

1–30 public HTTPS product URLs. Optional locale prefixes are retained. Do not supply collection URLs, credentials or currency query overrides. Duplicate product aliases are read once.

## `previousSnapshot` (type: `array`):

Optional prior dataset JSON array, up to 2,000 rows and 5 MB. Export the previous dataset and paste it here. Missing baseline rows mean first_seen, never newly launched. Failed or partial runs never imply deletion.

## `maxVariants` (type: `integer`):

Total row limit across products; capped products are reported incomplete.

## `maxRunSeconds` (type: `integer`):

Stops collection/new writes within this work budget. Set the platform timeout at least 120 seconds higher for storage and shutdown.

## Actor input object example

```json
{
  "productUrls": [
    "https://theme-dawn-demo.myshopify.com/products/small-convertible-flex-bag-cappuccino"
  ],
  "previousSnapshot": [],
  "maxVariants": 500,
  "maxRunSeconds": 120
}
```

# Actor output Schema

## `variants` (type: `string`):

No description

## `summary` (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 = {
    "productUrls": [
        "https://theme-dawn-demo.myshopify.com/products/small-convertible-flex-bag-cappuccino"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("natstandridge/shopify-variant-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 = { "productUrls": ["https://theme-dawn-demo.myshopify.com/products/small-convertible-flex-bag-cappuccino"] }

# Run the Actor and wait for it to finish
run = client.actor("natstandridge/shopify-variant-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 '{
  "productUrls": [
    "https://theme-dawn-demo.myshopify.com/products/small-convertible-flex-bag-cappuccino"
  ]
}' |
apify call natstandridge/shopify-variant-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,natstandridge/shopify-variant-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/QnHIajIb394oyJfOy/builds/mOdXJKk5GQ2x6Egsa/openapi.json
