# Universal Shopify Product Price Inventory Monitor (`arezdev/universal-shopify-product-price-inventory-monitor`) Actor

Extract public Shopify products, variants, SKUs, prices and availability. Compare against a previous dataset to detect price changes, restocks and sold-out variants. Export catalog snapshots or changed rows for competitor monitoring and automation.

- **URL**: https://apify.com/arezdev/universal-shopify-product-price-inventory-monitor.md
- **Developed by:** [Arezdev](https://apify.com/arezdev) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.60 / 1,000 shopify variant extracteds

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

**Turn public Shopify catalogs into ready-to-use product, price, and inventory alerts, with one clean row per variant and automatic change detection.**

Monitor competitor pricing, find restocks, synchronize SKUs, and track discounts without browsers. The Actor reads public bulk JSON endpoints, follows product sitemaps when necessary, and compares each variant against a previous dataset.

Best for public catalog monitoring and variant-level price alerts. Run once to create a baseline, then pass its dataset ID in previousDatasetId. Scheduled runs do not select a baseline automatically; stock availability does not guarantee an exact inventory quantity.

### How to use

1. Enter Shopify store domains, such as `allbirds.com`.
2. Run the prefilled sample to save up to three variant rows. Remove `maxItems` and increase `maxProductsPerStore` for a larger catalog.
3. Open the dataset to review prices and availability, then export JSON, CSV, or Excel.
4. Select a completed baseline dataset in `previousDatasetId` to detect changes. Enable `onlyChangedItems` if you only want changes.

Minimal input:

```json
{
  "storeDomains": ["allbirds.com"],
  "maxProductsPerStore": 3,
  "maxItems": 3
}
```

For scheduled monitoring, create an Apify Schedule with the desired input. Keep a complete baseline by leaving `onlyChangedItems` and `onlyInStock` disabled for runs that will become future baselines. Your n8n, Make, or API workflow can pass the latest successful full run's dataset ID into the next run's `previousDatasetId`. Reusing a change-only dataset as the next baseline can classify unchanged variants as new. There is no implicit cross-run state or automatic dataset selection.

### What data can you extract?

| Data | Output fields |
| --- | --- |
| Store and product | `storeDomain`, `productId`, `productTitle`, `handle`, `vendor`, `productType`, `tags`, `productUrl` |
| Variant identity | `variantId`, `variantTitle`, `sku`, `grams` |
| Pricing | `price`, `compareAtPrice`, `discountPercent` |
| Availability | `availableForSale` |
| Changes | `deltaStatus`, `previousPrice`, `priceDifference` |
| Images and timestamps | `featuredImageUrl`, `updatedAt`, `scrapedAt` |
| Diagnostics | `status`, `errorMessage` |

IDs are strings to preserve precision. `tags` is an array of strings; all other fields are scalar values or `null`. A product with 12 variants produces up to 12 rows. Prices are decimal amounts in the storefront's returned currency context. Bulk decimal prices and Ajax integer subunit prices are normalized to the same representation. The output does not include a currency field, so compare runs using consistent storefront and proxy geography. Shopify documents these responses in its [Product API reference](https://shopify.dev/docs/api/ajax/reference/product).

Availability means the storefront considers a variant purchasable. It does not expose exact warehouse quantities; backorders may be purchasable. Missing availability is `null`.

Delta statuses use `(storeDomain, variantId)`, normalizing a leading `www.` for comparison:

| Status | Meaning |
| --- | --- |
| `NEW_VARIANT` | No successful match in the baseline; also used on the initial run. |
| `BACK_IN_STOCK` | Availability changed from false to true. |
| `SOLD_OUT` | Availability changed from true to false. |
| `PRICE_DROPPED` | Current price is lower than the baseline price. |
| `PRICE_INCREASED` | Current price is higher than the baseline price. |
| `UNCHANGED` | No price or known availability change. |

Availability changes take precedence when both price and stock change. `priceDifference` still records current minus previous price, rounded to cents. Missing products are not assumed sold out. Unknown availability does not create a stock event. Changes to titles, tags, discounts, or SKU alone do not change `deltaStatus`.

### Input settings

| Setting | Default | Description |
| --- | --- | --- |
| `storeDomains` | Required; sample `allbirds.com` | Public domains or home URLs, at most 1,000. |
| `collectionHandles` | All products | Handles applied to every store; overlapping products are deduplicated. |
| `onlyInStock` | `false` | Save known purchasable variants. Disable to retain `SOLD_OUT` changes. |
| `onlyChangedItems` | `false` | Exclude `UNCHANGED` rows. Requires `previousDatasetId`. |
| `previousDatasetId` | None | Baseline dataset from this Actor; picker requests `READ` permission. |
| `maxProductsPerStore` | `250`; sample `3` | Unique products visited before filtering, from 1 to 50,000 per store. |
| `timeoutSecs` | `15` | Per-URL deadline including retries and redirects, from 3 to 60 seconds. |
| `proxyConfiguration` | Direct, then datacenter if blocked | Custom URLs force proxy use. `useApifyProxy: false` forces direct access. Configured or residential fallback requires explicit `useApifyProxy: true`. |
| `maxConcurrency` | `25` | Requested upper bound from 1 to 100; automatically reduced for available memory. |
| `maxItems` | No additional cap; sample `3` | Optional saved row limit across all stores, including errors, from 1 to 10,000,000. |

The product cap applies to the union of collections per store. Filters can reduce saved rows; the Actor does not exceed the product cap to compensate. Error rows remain visible with any filter.

### Output example

```json
{
  "storeDomain": "example.com",
  "status": "SUCCESS",
  "productId": "100000001",
  "productTitle": "Everyday Runner",
  "handle": "everyday-runner",
  "vendor": "Example Brand",
  "productType": "Shoes",
  "variantId": "200000001",
  "variantTitle": "Blue / M",
  "sku": "RUN-BLUE-M",
  "price": 90,
  "compareAtPrice": 120,
  "discountPercent": 25,
  "availableForSale": true,
  "deltaStatus": "PRICE_DROPPED",
  "previousPrice": 100,
  "priceDifference": -10,
  "grams": 250,
  "tags": ["running", "summer"],
  "featuredImageUrl": "https://cdn.shopify.com/example.jpg",
  "productUrl": "https://example.com/products/everyday-runner",
  "updatedAt": "2026-10-19T12:00:00.000Z",
  "scrapedAt": "2026-10-20T12:00:00.000Z",
  "errorMessage": null
}
```

Unreachable stores and invalid responses produce `ERROR` rows with a safe domain and error code. Missing product details are `null`. No stack traces or proxy authentication details are included. Strings that could execute spreadsheet formulas receive a leading apostrophe; numeric prices and differences remain numeric.

### Use the results in Clay, n8n, Make, or your API

- **Clay:** Import JSON or CSV and match records using `storeDomain` plus `variantId`. Use `deltaStatus` and `priceDifference` to prioritize opportunities.
- **n8n:** Start a run with the Apify node, retrieve dataset items, and route `PRICE_DROPPED` or `BACK_IN_STOCK` rows to your workflow.
- **Make:** Start the Actor, iterate dataset rows, and filter on `deltaStatus` before updating a sheet, database, or alert.
- **API:** Start runs through the [Actor API](https://docs.apify.com/api/v2/act-runs-post), then read `defaultDatasetId` through the [Dataset items API](https://docs.apify.com/api/v2/dataset-items-get). Authenticate with an authorization header rather than a query string.

Pass `previousDatasetId` from your workflow to compare against the correct full snapshot. No storefront login or Shopify Admin API access is required.

### Pricing and limits

The custom event is **`product-scraped`**. Each saved `SUCCESS` variant row counts as one event. Filtered variants do not incur this event. **`ERROR` rows cost $0 in event fees.** Compute, storage, and proxy usage may still have platform costs.

For example, at **$0.001 per successful variant**, **1,000 successful rows cost $1.00** in event fees and 1,000 error rows cost **$0.00**. This is an illustrative launch price, not a live Store quote; the actual event price is configured in Apify Console and shown on the Pricing tab. A run's maximum charge stops extraction cleanly before the custom event budget is exceeded.

Free Apify accounts are limited to **25 saved rows per run**, including successes and errors. Paid accounts and local runs have no free-plan cap; product, optional row, run time, and budget limits still apply. The default run uses 256 MB RAM with a 3,600-second timeout. The sample saves at most three rows; network conditions determine its duration.

### FAQ and troubleshooting

**Will this extract every product from any Shopify store?**

It paginates public products in batches of up to 250 and follows exposed sitemaps. Password protection, private B2B catalogs, region restrictions, and blocked endpoints can limit coverage. Raise `maxProductsPerStore` and remove the sample `maxItems` cap for a full public catalog, up to 50,000 products per store. Hidden inventory cannot be guaranteed.

**What happens when `/products.json` is blocked?**

HTTP 401, 403, or 404 triggers sitemaps plus `/products/{handle}.js`. Oversized JSON also uses this fallback. The Actor first requests `/sitemap_products_1.xml`, then discovers query-based or split product sitemaps through `/sitemap.xml`. Collection fallback requires public `/collections/{handle}/products/{product}` links in collection pages. If scope cannot be verified, it reports an error rather than returning unrelated products.

**What happens with HTTP 403, 429, or 503?**

Direct access is followed by the SHADER datacenter group and the default account datacenter proxy. Unavailable SHADER access moves to the next tier. Configured Apify groups, including residential, require explicit opt-in. Custom proxy URLs bypass this cascade. The Actor uses run-provided proxy access and does not fetch user profile information. The timeout covers the whole cascade; increasing `timeoutSecs` can help slow sources.

**Why do I see no changes, fewer rows, or new variants on every run?**

Check filters, caps, and the baseline. Filtered, partial, failed, or different-store baselines can produce incomplete comparisons. If the baseline cannot be read, the Actor saves error rows and stops comparison instead of labeling all variants new. Initial runs and empty baselines mark observed variants `NEW_VARIANT`.

**Does a successful run guarantee every store succeeded?**

Check `status`, `errorMessage`, row counts, and the final status message. Per-store HTTP, DNS, TLS, and parser failures do not crash the Actor. Invalid input exits with code 1. Caps and event budgets stop cleanly with code 0. Uncertain storage writes are not automatically repeated because a retry could duplicate rows or charges.

**Can this run with limited permissions?**

Yes. The dataset picker declares `resourceType: "dataset"` and `resourcePermissions: ["READ"]`. Select an accessible baseline. The Actor does not request the user profile endpoint. Its temporary comparison database is deleted at shutdown.

### Development and deployment

Use Node.js 22.13 or later. The Node 22 container uses built-in SQLite to avoid an in-memory copy of the baseline.

```sh
npm ci
npm run build
npm test
npm run smoke
```

Tests use `node:test` for security, streaming, parsing, deltas, billing, history, and pagination. The live smoke requests Allbirds directly and reports up to three rows, duration, and process RSS without Apify event charges. Test and start runners set `UV_THREADPOOL_SIZE=64`, `NODE_ENV=production`, and TLS validation.

For local execution, save the minimal input to `storage/key_value_stores/default/INPUT.json`, then run `npm start`. The Actor does not load `.env` files. Alternatively, build and run the non-root multi-stage image:

```sh
docker build -t shopify-monitor .
docker run --rm -v "${PWD}/storage:/app/storage" shopify-monitor
```

The client validates every address and redirect, pins DNS without changing HTTP authority, verifies TLS, limits redirects to six, and caps decompressed responses at 2 MB JSON, 15 MB XML, and 4 MB HTML. XML DTD and external entities are rejected. Nesting is capped at 128, and partial HTML recovery requires a successful response with a valid retained prefix.

Before publishing, configure `product-scraped` and its actual price in Apify Console. Keep automatic dataset-item event pricing disabled so errors remain free of event fees; the Actor rejects conflicting pricing. Store monetization settings are managed by Apify Console outside this repository.

# Actor input Schema

## `storeDomains` (type: `array`):

Public Shopify domains or store home URLs. One store per line. Product paths are not accepted.

## `collectionHandles` (type: `array`):

Optional collection handles, such as mens-shoes. Applied to every store. Duplicate products across collections are saved once.

## `onlyInStock` (type: `boolean`):

Save only variants whose storefront availability is true. Disable this setting to capture SOLD_OUT events.

## `onlyChangedItems` (type: `boolean`):

Requires a previous dataset. Saves new variants and price or availability changes, excluding UNCHANGED rows.

## `previousDatasetId` (type: `string`):

An accessible, complete baseline dataset from this Actor. Under limited permissions, select a dataset you can grant READ access to.

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

Unique products to visit per store before filtering. Each product can produce multiple variant rows.

## `timeoutSecs` (type: `integer`):

Total timeout per URL including DNS, redirects, protocol fallbacks, and proxy retries.

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

Direct first, then datacenter on 403/429/503. Residential or configured Apify fallback requires explicit opt-in. Custom proxy URLs are used exclusively; disabling Apify Proxy forces direct requests.

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

Upper bound for concurrent stores. Automatically reduced to fit the run memory limit.

## `maxItems` (type: `integer`):

Optional run-wide output cap, including error rows. The prefilled sample saves 3 rows. Omit this field for no additional cap. Free Apify accounts are always limited to 25 saved rows.

## Actor input object example

```json
{
  "storeDomains": [
    "allbirds.com"
  ],
  "onlyInStock": false,
  "onlyChangedItems": false,
  "maxProductsPerStore": 3,
  "timeoutSecs": 15,
  "maxConcurrency": 25,
  "maxItems": 3
}
```

# Actor output Schema

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

Default dataset items. Review each record status before using extracted data downstream.

# 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 = {
    "storeDomains": [
        "allbirds.com"
    ],
    "maxProductsPerStore": 3,
    "maxItems": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("arezdev/universal-shopify-product-price-inventory-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 = {
    "storeDomains": ["allbirds.com"],
    "maxProductsPerStore": 3,
    "maxItems": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("arezdev/universal-shopify-product-price-inventory-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 '{
  "storeDomains": [
    "allbirds.com"
  ],
  "maxProductsPerStore": 3,
  "maxItems": 3
}' |
apify call arezdev/universal-shopify-product-price-inventory-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arezdev/universal-shopify-product-price-inventory-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/b2xtB516OfqBQw2OI/builds/tz8zQI8uSt8cDgMQI/openapi.json
