# Shopify Variant Availability & Price Evidence Monitor (`latentlark/shopify-variant-availability-price-monitor`) Actor

Check if selected Shopify variants (size, color) are in stock or sold out, and save the raw price as evidence. Restock and competitor variant watch. $0.005 per product check; blocked, failed or unknown checks are free. No login, proxy or browser.

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

## Pricing

$5.00 / 1,000 successful product checks

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 Availability & Price Evidence Monitor

Check whether **specific Shopify variants** (a size, a color) are **in stock or sold out** on a public storefront, and save the price Shopify returned as timestamped evidence. Paste a product URL and a variant ID and click Start. You get one clear row per variant, and *sold out* is never confused with *blocked*, *failed* or *unknown*.

*No login, no cookies, no proxy, no browser. It reads only Shopify's documented public product endpoint.*

*Built and maintained by LatentLark.* Want the rows in a spreadsheet? Send every run to Google Sheets with [Google Sheets Export & Import (No OAuth)](https://apify.com/latentlark/sheets-export).

### Quick start (first run in under a minute)

1. Click **Try for free** (or **Start** in Console). The form is prefilled with a real product on Shopify's public **Dawn theme demo store**: the *Thelma sandal*, variant `39576998346841` (`Black / 35`).
2. Click **Start**. The run takes a few seconds and gives you **1 row** in the **Output** tab, for **$0.005**.
3. Replace the URL and variant ID with the products you want to watch, up to **5 products and 20 variants** per run.
4. To be told what *changed*, click **Save as task** and add a **Schedule** (for example, daily). Each run of a task compares against the earlier runs of that same task. A one-off run reports `initial_observation`.

**Finding a variant ID:** open the product page, pick the size or color, and copy the number after `?variant=` in the address bar. You can also open `https://STORE/products/HANDLE.js` and look at `variants[].id`.

### What you can do with it

- **Restock watch:** see when a sold-out size or color becomes buyable again (`availability_changed` from `out_of_stock` to `in_stock`).
- **Competitor variant availability:** track which sizes or colors a competitor has run out of, and when they come back.
- **Ad and campaign QA:** label each variant with the ad or campaign it belongs to (`label`), so you know when an advertised variant is no longer buyable.
- **Price evidence snapshots:** keep a dated record of the raw price integer Shopify returned for each variant, with the exact source URL and HTTP status.

### Example: the prefilled demo

Input (this is the form prefill):

```json
{
  "watchlist": [
    {
      "productUrl": "https://theme-dawn-demo.myshopify.com/products/thelma-sandal",
      "variantId": "39576998346841",
      "label": "dawn-demo-thelma-sandal-black-35"
    }
  ]
}
```

Real output row from a platform run on 9 November 2026 (some fields shortened):

```json
{
  "label": "dawn-demo-thelma-sandal-black-35",
  "productUrl": "https://theme-dawn-demo.myshopify.com/products/thelma-sandal",
  "sourceUrl": "https://theme-dawn-demo.myshopify.com/products/thelma-sandal.js",
  "productHandle": "thelma-sandal",
  "productId": "6920720777305",
  "variantId": "39576998346841",
  "variantTitle": "Black / 35",
  "availability": "out_of_stock",
  "previousAvailability": null,
  "event": "initial_observation",
  "priceMinor": 38500,
  "currency": null,
  "currencyStatus": "unknown",
  "quantity": null,
  "httpStatus": 200,
  "variantCoverage": "complete",
  "returnedVariantCount": 42,
  "baselineWrite": "appended",
  "charged": true,
  "chargeEvent": "successful-product-check",
  "chargeScope": "availability_only",
  "requestsUsed": 2,
  "requestLimit": 12,
  "limitations": ["currency_unknown", "currency_endpoint_disallowed_by_robots"],
  "checkedAt": "2026-11-09T18:16:38.035Z"
}
```

The *Black / 35* variant really is sold out on that demo store, while other variants of the same product are in stock. Results on a live demo can change over time.

### Pricing (pay per event)

| Event | Price |
|---|---|
| Successful product check | **$0.005** per product ($5 per 1,000) |
| Actor start | none |

- **One charge per product, not per variant.** It covers every variant you selected on that product (up to 20 in total per run).
- **Charged only when every selected variant on the product was classified:** `in_stock`, `out_of_stock`, `variant_not_found` or `removed`.
- **Free:** blocked, rate-limited, failed (404, 5xx), unsupported (password page), truncated, timed-out or unknown results. If one selected variant of a product is unknown, that whole product is free.
- **Availability check, not a price check.** Most stores don't expose currency to this read-only method (see Limitations). The price integer is saved as evidence, but you pay for the availability classification.
- **Cheap to run:** our 1-product test run used about **$0.0003** of Apify platform usage in roughly 4 seconds at 256 MB. Example: checking 5 products once a day for 30 days costs 150 × $0.005 = **$0.75** in events.

### Input

| Field | Required | What it does |
|---|---|---|
| `watchlist` | yes | List of `{ productUrl, variantId, label?, locale? }`. Duplicate product and variant pairs are merged. |
| `watchlist[].productUrl` | yes | HTTPS Shopify product URL, for example `https://store.com/products/handle`. Any `?variant=` in the URL is ignored. |
| `watchlist[].variantId` | yes | Numeric Shopify variant ID, as a string. |
| `watchlist[].label` | no | Your own name for the row (an ad, campaign or SKU). It is never sent to the store. |
| `watchlist[].locale` | no | URL locale prefix such as `en` or `en-ca`. It does **not** simulate a shopper's country. |
| `previousSnapshot` | no | Paste `OUTPUT.snapshot` from an earlier run to compare against it without using a task. |
| `stateScope` | no | Snapshot scope name for local (non-Apify) runs. On Apify, use a saved task or `previousSnapshot` for history. A task's own ID always takes priority. |
| `maxProducts` | no | Lower the product cap (1–5, default 5). |
| `maxVariants` | no | Lower the variant cap (1–20, default 20). |
| `maxRequests` | no | Lower the HTTP request cap (1–12, default 12). |
| `timeoutSeconds` | no | Lower the time budget (5–60 seconds, default 60). |

### Output

**Dataset:** one row per selected variant, including unchanged and failed checks. Main fields:

| Field | Meaning |
|---|---|
| `availability` | `in_stock`, `out_of_stock`, `variant_not_found`, `removed`, `truncated`, `blocked`, `rate_limited`, `unsupported`, `http_error` or `unknown` |
| `event` | `initial_observation`, `unchanged`, `availability_changed`, `price_changed`, `currency_changed`, `variant_not_found`, `removal_confirmed`, `truncated` or `unknown` |
| `previousAvailability`, `previousPriceMinor` | Values from the last good check of the same task or scope |
| `priceMinor`, `compareAtPriceMinor` | Price integers exactly as Shopify returned them, not divided or converted |
| `currency`, `currencyStatus`, `currencySource` | The currency code only when a documented field supplied it. Otherwise `null` / `unknown` |
| `quantity` | Always `null`. This Actor does not report unit counts. |
| `httpStatus`, `reason`, `sourceUrl` | Where the evidence came from, and why a check failed |
| `variantCoverage`, `returnedVariantCount` | Whether the product JSON listed all of the product's variants (Shopify lists at most 250) |
| `baselinePreserved`, `baselineWrite` | Whether an error left the earlier good result untouched, and whether this run's snapshot was saved |
| `charged`, `chargeReason`, `chargeScope` | Whether this product was billed, and why or why not |
| `label`, `labels`, `checkedAt`, `limitations` | Your label, the check time, and any caveats |

**Key-value store `OUTPUT`:** a run summary with counts per availability and event, requests used, the caps applied, the charges made, and `snapshot`, which you can pass as `previousSnapshot` next time.

### Limits

| Cap | Maximum |
|---|---|
| Unique products per run | 5 |
| Selected variants per run | 20 |
| HTTP requests per run (robots.txt, product, redirects, one retry) | 12 |
| Run time | 60 seconds |
| Memory | 256 MB |

If an input is over a cap, the run stops before sending any request and nothing is charged. Requests to each store are limited to 1 per second, with at most 1 retry. Proxies and notifications are not supported, and the run refuses them.

### How it works

- It reads only `GET /{locale}/products/{handle}.js`, [Shopify's documented Ajax product endpoint](https://shopify.dev/docs/api/ajax/reference/product), and respects each store's `robots.txt`.
- `out_of_stock` is reported only when Shopify returns `available: false` for that exact variant. A 403, a 404, a timeout or a password page is never recorded as sold out.
- A variant missing from a complete product document is `variant_not_found` the first time and `removed` only after two consecutive complete checks.
- Errors never overwrite the earlier good result. Each run appends its own snapshot and never rewrites an older one.
- There are no cart actions, no Storefront or Admin API tokens, and no browser.

### Limitations

- **Currency is often unknown.** The product JSON has no currency field. Shopify's documented place to read it (`/cart.js`) is disallowed by many stores' `robots.txt`, including Shopify's theme demos, and this Actor respects that. In those cases `currency` is `null` and `price_changed` is not emitted, because a price can't be compared safely without a currency.
- **At most 250 variants per product.** Shopify's product JSON stops at 250. A selected variant that isn't in a 250-variant response is reported as `truncated` (unknown), not sold out.
- **Overlapping runs may duplicate events.** If two runs of the same task overlap, both can report the same `availability_changed`. Schedule runs far enough apart that one finishes before the next starts (a run usually takes seconds).
- **History needs a task or `previousSnapshot`.** One-off runs only report `initial_observation`. After more than 50 stored snapshots in one task, that task's history is no longer trusted; pass `previousSnapshot` from a recent run instead.
- **No stock counts and no country simulation.** `quantity` is always `null`, and `locale` is only a URL prefix.
- Only public storefronts are supported. Password-protected stores and non-Shopify sites return `unsupported` or `blocked`, free of charge. Only monitor stores you are allowed to check.

### FAQ

**I got `blocked` or `http_error`. Was I charged?**
No. Only fully classified products are charged. Check that the URL is a public `/products/...` page and that the store's `robots.txt` allows `/products/`.

**Why is `currency` null?**
The store doesn't expose currency to this read-only method, or its `robots.txt` disallows `/cart.js`. The raw `priceMinor` is still saved.

**Why don't I see `availability_changed`?**
The first run of a task, or a one-off run, is always `initial_observation`. Save the input as a task and schedule it, or pass `previousSnapshot`.

**`variant_not_found` but the product exists?**
Check the variant ID (it's the number after `?variant=`). A variant is only marked `removed` after two consecutive complete checks without it.

**Can I watch 100 products?**
Not in one run. The limit is 5 products and 20 variants per run, which keeps every run fast and cheap. Use several tasks for more.

**Does it tell me how many units are left?**
No. `available: false` means the storefront isn't selling that variant right now. It isn't a unit count.

### More LatentLark Actors

- [Google Ads Transparency & LinkedIn Ad Library Scraper](https://apify.com/latentlark/competitor-ads-monitor): see a competitor's paid ads next to the variants they are selling.
- [Creator Commerce Competitor Finder — LTK & ShopMy](https://apify.com/latentlark/creator-commerce-competitor-finder): find which creators promote a brand's products.
- [Google Trends Compare: Unlimited Keywords, One Scale](https://apify.com/latentlark/google-trends-compare): check demand for a product category over time.
- [Google Sheets Export & Import (No OAuth)](https://apify.com/latentlark/sheets-export): send every run's rows to a Google Sheet.

# Actor input Schema

## `watchlist` (type: `array`):

Each item is one public Shopify product URL plus the variant ID to watch. Duplicate product and variant pairs are collapsed. The prefill is a real product on Shopify's public Dawn theme demo, checked with the documented Ajax product endpoint.

## `previousSnapshot` (type: `object`):

Optional immutable snapshot copied from an earlier run's OUTPUT.snapshot. Records from a different scope are ignored. This is an explicit baseline, not a shared lock.

## `stateScope` (type: `string`):

Optional persistence name for a local or non-task run. An Apify task ID always wins so two tasks cannot be pointed at each other.

## `maxProducts` (type: `integer`):

Optional cap of 5 or less. Larger values are rejected.

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

Optional cap of 20 or less. Larger values are rejected.

## `maxRequests` (type: `integer`):

Counts robots, product, currency, redirects, and the single retry. Cannot exceed 12.

## `timeoutSeconds` (type: `integer`):

Wall-clock budget for this run. Cannot exceed 60.

## Actor input object example

```json
{
  "watchlist": [
    {
      "productUrl": "https://theme-dawn-demo.myshopify.com/products/thelma-sandal",
      "variantId": "39576998346841",
      "label": "dawn-demo-thelma-sandal-black-35"
    }
  ],
  "maxProducts": 5,
  "maxVariants": 20,
  "maxRequests": 12,
  "timeoutSeconds": 60
}
```

# Actor output Schema

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

Counts, charge attempts, request usage, and limitations. This is not a bill and not a measured platform cost.

## `rows` (type: `string`):

One dataset row per selected variant, including unchanged and failed checks.

# 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 = {
    "watchlist": [
        {
            "productUrl": "https://theme-dawn-demo.myshopify.com/products/thelma-sandal",
            "variantId": "39576998346841",
            "label": "dawn-demo-thelma-sandal-black-35"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("latentlark/shopify-variant-availability-price-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 = { "watchlist": [{
            "productUrl": "https://theme-dawn-demo.myshopify.com/products/thelma-sandal",
            "variantId": "39576998346841",
            "label": "dawn-demo-thelma-sandal-black-35",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("latentlark/shopify-variant-availability-price-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 '{
  "watchlist": [
    {
      "productUrl": "https://theme-dawn-demo.myshopify.com/products/thelma-sandal",
      "variantId": "39576998346841",
      "label": "dawn-demo-thelma-sandal-black-35"
    }
  ]
}' |
apify call latentlark/shopify-variant-availability-price-monitor --silent --output-dataset

```

## MCP server setup

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