# Shopify Variant Monitor - Prices & Availability (`maydit/shopify-variant-monitor`) Actor

Monitor public Shopify variants for price, availability and option changes. Save stable IDs and previous observations. No login, API key or proxy required; bounded catalog and product URL inputs.

- **URL**: https://apify.com/maydit/shopify-variant-monitor.md
- **Developed by:** [Brandt May](https://apify.com/maydit) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.90 / 1,000 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.
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?

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

![Follow the variant, not just the page — illustrative workflow](https://api.apify.com/v2/key-value-stores/YGxD9JRpduom8Q5Zd/records/shopify-variant-monitor-d764e243ce5c.png?signature=8OR4mD6fO9kaQMny3vLg)

### Follow the variant, not just the page

For product and pricing research: collect public Shopify variant observations with stable product and variant IDs, listed prices, options and purchasing availability. Use a bounded catalog or selected product pages.

[Open this Actor in Console](https://console.apify.com/actors/XezQhbcdVf0c74hYh/input) · [First run](#try-this-actor) · [Results](#result-preview) · [Cost](#usage-and-pricing) · [Full guide](#full-guide)

### Try this Actor

- **First run:** Use **Death Wish Coffee - Variant Price Sample** below: up to five products and 25 saved rows. Inspect the variant IDs and currency evidence before enabling saved comparisons.
- **Read the results:** Open the default Dataset for variant rows and **SUMMARY** for store coverage. The saved price, change types and source product link provide the evidence for a comparison.
- **Keep in mind:** Availability means the variant is purchasable, not a stock quantity. Prices remain decimal strings; currency is populated only when verified from the source. Unsupported storefronts or catalog limits can leave gaps.

**Death Wish Coffee - Variant Price Sample** — copy this input into the JSON editor in Console.

```json
{
  "stores": [
    "https://www.deathwishcoffee.com"
  ],
  "maxProductsPerStore": 5,
  "maxResults": 25
}
```

### Result preview

The dataset view highlights these columns:

- **Product** · `title`
- **Variant** · `variantTitle`
- **Product URL** · `productUrl`
- **Product ID** · `productId`
- **Variant ID** · `variantId`
- **Listed price** · `price`

Use the original source links and the field definitions below when interpreting the results. CSV works well for flat columns; JSON preserves nested values.

### Usage and pricing

Base result rate on the FREE tier: **$1.5 per 1,000 variant observations**. The [Pricing tab](https://apify.com/maydit/shopify-variant-monitor/pricing) shows your plan's result rate and any Actor Start charge. Set a small result limit for the first run; the requested limit is not a guarantee of that many results.

***

### Customer workflow: A variant price and availability review board

Watch a small list of exact public product URLs, update each variant in place, and route verified price or availability changes for review without inventing inventory quantities or cross-currency discounts.

**Setup:** Connect your own Apify and Google Sheets latest-variant register and merchant review queue through [Make](https://docs.apify.com/integrations/make) or [n8n](https://docs.apify.com/integrations/n8n). Keep credentials in your connector's credential manager.

```json
{
  "stores": [],
  "productUrls": [
    "https://www.deathwishcoffee.com/products/klean-kanteen-camp-mug"
  ],
  "maxResults": 250,
  "comparePrevious": true,
  "onlyChanges": false,
  "snapshotName": "customer-product-watch-v1",
  "maxRunSeconds": 240
}
```

Start with a **300-second Run timeout** and **$0.50 maximum run charge**, alongside the input's 240-second budget. Check current pricing and the selected scope before increasing either limit.

1. Create a Variants sheet and replace the demo URL with your product watchlist. Run the bootstrap above before switching `onlyChanges` to true. Keep product URLs, locale, source mode and `snapshotName` consistent between runs.
2. Review Run report, `SUMMARY.failedSources`, `sourcesNotReached`, and `sourceCoverage`. Resolve source, time or spending failures first. Inspect `hasMore` and `publicVariantLimitReached` before calling the selected scope fully observed.
3. Use `JSON.stringify([storeUrl, variantId])` as your destination key. Upsert with Sheets "Append or Update Row" in n8n or a lookup/update branch in Make. Preserve locale roots, string IDs, exact decimal prices and nullable currency.
4. After baseline, send `new`/`changed` rows to a merchant review queue. Use `changeTypes` for price and availability routing. Send `price_changed_currency_unverified` to manual review rather than calculating a cross-currency increase or discount.

| Destination information | Actor fields |
| --- | --- |
| Store/locale and variant | `storeUrl`, `variantId` |
| Product | `title`, `variantTitle`, `productUrl` |
| Exact money | `price`, `compareAtPrice`, `currency` |
| Public purchasability | `available` |
| Revision evidence | `comparisonStatus`, `changeTypes`, `previous`, `observedAt` |

**Keep the meaning:** `available` is public storefront purchasability, not stock quantity or checkout success. The public endpoint has a 250-variant boundary. Keep decimal price strings as supplied; do not divide them by 100 again. Unknown currency stays null.

**Empty or interrupted runs:** A healthy change-only repeat may save zero rows. Keep existing variants; missing observations never mean deletion or stockout. History can advance for saved rows even when a later source fails, so replay the original dataset after delivery trouble.

**Reliable delivery:** Read the completed run's own SUMMARY and paginated dataset. Retain its run ID; if destination delivery fails, replay that dataset and deduplicate by run ID plus row key. Keep destination writes sequential, and write source text literally.

**Validation:** Inputs and mappings were checked locally against this Actor. The exact customer connection has not been run end to end. Choose recurrence after a manual bootstrap; this guide creates no schedule. See the [run API](https://docs.apify.com/api/v2/actors-runs-post), [pagination guide](https://docs.apify.com/api/v2/dataset-items-get), and the full Actor guide below for detailed setup and limits.

### Full guide

Build a repeatable feed of public Shopify variant prices and availability for merchandising research, competitor comparisons, and saved-observation alerts. No API key or merchant credentials are required. Empty input `{}` inspects five products from Death Wish Coffee; it is a bounded sample.

#### What each row contains

One variant observation: stable string `productId` and `variantId`, product and variant titles, public product URL, vendor/product type, source SKU/barcode, option names and values, `price`, `compareAtPrice`, `currency`, explicit boolean `available`, observation time and optional comparison details. A barcode is raw source text, not a validated GTIN.

Prices are exact decimal **strings** derived from Shopify Ajax integer hundredths, without floating-point arithmetic. Shopify uses this representation even for currencies such as JPY. Currency is emitted only when the same product's public HTML explicitly declares one unambiguous `Shopify.currency.active`; otherwise it is null. Price increases/decreases are classified only when both observations have the same verified currency; otherwise a price edit is `price_changed_currency_unverified`.

`available` means the storefront reports the variant as purchasable. **Stock quantity is not available and is not inferred.** A product may have up to 250 variants exposed by Shopify Ajax; `publicVariantLimitReached` flags this boundary. Headless/custom stores lacking `/products.json` or `/products/handle.js`, password pages and robots-disallowed paths are unsupported. Product URL mode needs only the product endpoint, not the catalog endpoint.

#### Inputs

- `stores`: up to 10 store homepage or locale-root URLs.
- `productUrls`: up to 100 `/products/handle` HTML URLs. Both arrays can be used; overlapping saved variants are deduplicated by store origin, locale prefix and variant ID. Different locale roots retain separate observations and history.
- `maxProductsPerStore`: 5 by default, up to 1,000. `maxPagesPerStore`: 10 by default, up to 40.
- `maxResults`: 100 saved variants by default, up to 10,000.
- `comparePrevious`, `onlyChanges`, `snapshotName`, `maxRunSeconds`: described below.

Currency HTML failure does not invent a currency: the variant remains usable with null currency, and `currencyUnavailableProducts` records the limitation. Catalog shape/identity failures are source failures.

#### Three example inputs

##### Death Wish Coffee - Variant Price Sample

Inspect five public catalog products with exact price strings and availability, capped at 25 variant rows.

Use the [copyable first-run input](#try-this-actor) above.

##### Klean Kanteen Mug - Public Variant Snapshot

Get variants for one exact public product URL, without scanning a store catalog.

```json
{
  "productUrls": [
    "https://www.deathwishcoffee.com/products/klean-kanteen-camp-mug"
  ],
  "maxResults": 20
}
```

##### Coffee Merchandise - Price & Availability History

Compare two products with their last saved observations. The first run establishes a baseline; later runs still emit unchanged variants.

```json
{
  "productUrls": [
    "https://www.deathwishcoffee.com/products/klean-kanteen-camp-mug",
    "https://www.deathwishcoffee.com/products/white-chocolate-pistachio-klean-kanteen-20oz"
  ],
  "comparePrevious": true,
  "snapshotName": "coffee-merchandise",
  "maxResults": 30
}
```

#### Saved observations and change detection

`comparePrevious: true` enables persistent, named comparison history. `onlyChanges: true` also enables comparison and suppresses unchanged rows. The first observation emits `comparisonStatus: "baseline"`; later rows are `new`, `changed`, or `unchanged`. Without comparison they are `not_requested`. `changedFields`, `changeTypes` and `previous` explain each comparison.

Only confirmed saved rows update history. Existing records outside the selected window are retained. A partial run never establishes removals, and neither Actor emits deletion events. Comparison means **last successfully saved observation**, not an uninterrupted history of every change between runs. The state limit is 50,000 identities per source and snapshot name.

Run a given snapshot name sequentially. A reread detects an already changed snapshot before writing and reports a conflict, but this is not a transactional lock. Dataset and history writes are separate: a storage failure can leave saved rows without a new baseline; retrying may repeat observations. Use distinct names for independent workflows.

#### Coverage, limits and failures

`maxResults` caps saved rows across sources. Source/product/page limits select a bounded sample and are reported through `SUMMARY.sourceCoverage`, including `hasMore`; reaching a selected cap is not itself an operational failure. Always inspect SUMMARY before interpreting an empty or partial dataset. The output tab links to both results and SUMMARY.

Requests honor robots.txt, reject nonpublic network destinations and unsafe redirects, and stay within `maxRunSeconds` (default 240, range 30–3600) and 25 seconds before the platform deadline. HTTP 429, server errors and transient connection failures receive at most two bounded retries, honoring Retry-After. No browser rendering, login, proxy or anti-bot bypass is used.

Failures appear in `failedSources`, never as data rows. If all sources fail before usable output, the run fails. Genuine empty results or no changes produce a warning and succeed. Spending limits stop output cleanly, preserve existing rows, and prevent unsaved observations from advancing history. A source outage, robots restriction or time limit can produce partial coverage.

#### Development

Node 20 Docker image; exact Apify SDK 3.7.2 and Cheerio 1.1.2 lockfile. `npm ci` installs dependencies and `npm test` runs offline regression tests. `npm start` reads the normal Apify INPUT record. Public storefronts may change independently of this Actor.

#### Pricing

Planned pay-per-result rate: **$1.50 per 1,000 saved variant observations**, plus the configured Actor-start event. The current Apify pricing tab is authoritative. An unchanged row is billable when saved; `onlyChanges` avoids saving unchanged rows. Errors are not dataset results.

#### Source documentation

[Shopify Ajax API](https://shopify.dev/docs/api/ajax) · [Product endpoint](https://shopify.dev/docs/api/ajax/reference/product) · [Shopify price representation](https://shopify.dev/docs/api/liquid/objects/variant)

### Help with your run

Open the **Issues** tab on this Actor with the run URL, the input field that caused trouble and what you expected to receive. Remove API keys and private information before posting.

[Explore more Mayd-It Actors](https://apify.com/maydit)

### Understand your run

Open **Run report** in the run’s Output tab for saved-record counts, reported source issues and limits, and suggested next steps. Use **CSV export** to take the saved results into a spreadsheet. The existing JSON results and detailed machine-readable summary remain available.

The report reflects this run’s recorded coverage. A successful run or a saved result does not establish that every possible source record was collected.

# Actor input Schema

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

Public Shopify store homepages or locale roots, up to 10. When both source arrays are empty, uses a small Death Wish Coffee catalog sample.

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

Up to 100 public /products/handle URLs. Each must expose Shopify product Ajax JSON. Headless stores without that endpoint are unsupported.

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

Maximum distinct catalog products to inspect per store. All available variants of selected products are considered until the saved-row cap.

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

Maximum public catalog pages requested per store. Reaching this selected cap does not imply a complete catalog.

## `maxResults` (type: `integer`):

Stop after this many saved rows across all sources. This is a selected sample limit, not proof of complete source coverage.

## `comparePrevious` (type: `boolean`):

Compare against this snapshot name. First observation emits baseline rows; only successfully saved rows advance history. Run the same snapshot name sequentially.

## `onlyChanges` (type: `boolean`):

Also enables comparison. First run emits baseline rows. Later unchanged observations produce no rows; this can legitimately return an empty dataset.

## `snapshotName` (type: `string`):

Named persistent history, isolated by source. Use 1–40 letters, digits, hyphens or underscores. Do not run the same name concurrently.

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

Own deadline, also shortened to 25 seconds before the platform timeout. Requests and retry delays stay within the remaining time.

## Actor input object example

```json
{
  "maxProductsPerStore": 5,
  "maxPagesPerStore": 10,
  "maxResults": 100,
  "comparePrevious": false,
  "onlyChanges": false,
  "snapshotName": "default",
  "maxRunSeconds": 240
}
```

# Actor output Schema

## `results` (type: `string`):

One confirmed public observation per dataset row. Failures are recorded in SUMMARY, never as paid error rows.

## `runReport` (type: `string`):

Readable summary of saved records, reported source coverage, limits and next steps. Available when this run saves its SUMMARY.

## `csvExport` (type: `string`):

Download saved dataset records as a CSV file for spreadsheets and other tools.

## `summary` (type: `string`):

Saved counts, selected scope, pagination coverage, failed sources, and time or spending limits.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("maydit/shopify-variant-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("maydit/shopify-variant-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 '{}' |
apify call maydit/shopify-variant-monitor --silent --output-dataset

```

## MCP server setup

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