# Shopify Variant Stock & Price Change Feed (`practical_ai_lab_jp/pal-shopify-variant-stock-feed`) Actor

Recheck public variant price and availability changes; do not infer private stock quantities. For Shopify category and inventory monitoring teams. Check the live Pricing tab and input limits.

- **URL**: https://apify.com/practical_ai_lab_jp/pal-shopify-variant-stock-feed.md
- **Developed by:** [Practical AI Lab](https://apify.com/practical_ai_lab_jp) (community)
- **Categories:** E-commerce, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 variant records

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 Stock & Price Change Feed

Reads the **public** `products.json` feed of Shopify stores and returns one row per product variant: published price, compare-at price, a derived discount, and the store's own published availability flag. On repeat runs it also tells you what changed since the last run: new variants, price changes and availability changes.

### What makes it different from a general product scraper

- **Variant-level rows** with SKU, variant ID and a direct variant link, ready for spreadsheets and monitoring.
- **Change detection built in.** A small snapshot per store and scope is kept in a named key-value store (`pal-variant-stock-snapshots`). Each row carries `changeType`, `previousPrice`, `previousAvailability` and `priceDelta`.
- **"Only changed" mode** outputs just the rows that moved, which suits scheduled monitoring.
- **Meaningful price drops and restocks:** `alertSignal` flags a price drop over your chosen threshold, a change from published `sold_out` to `available`, or both. `priceDropPercent` explains how much the listed price decreased. These are **dataset signals**, not emailed or pushed alerts.
- **Actionable-only output:** `onlyActionableChanges` suppresses rows that do not meet the threshold or show a restock. A baseline run has no signal, so keep it disabled initially; scheduled repeat runs can enable it.
- **Honest data.** Availability is the boolean the store publishes (`available`, `sold_out`, or `unknown` if absent). The Actor never outputs, estimates or guesses inventory counts and does not call any hidden or private stock API.

### Input

| Field | Meaning |
| --- | --- |
| `storeUrls` | 1–10 HTTPS store URLs. Only the domain is used. |
| `maxProductsPerStore` | 1–2000, default 250. |
| `collectionHandle` | Optional single collection, e.g. `new-arrivals`. |
| `availabilityFilter` | `all`, `available` or `sold_out` (output filter only). |
| `minPrice` / `maxPrice` | Optional price range in the store's currency (output filter only). |
| `trackChanges` | Default `true`. Compare with the previous run. |
| `onlyChangedRecords` | Default `false`. Output only new/changed variants. |
| `minPriceDropPercent` | Default 5. Price decrease threshold from 0% through 100%; not a currency conversion. |
| `onlyActionableChanges` | Default `false`. Output only threshold-crossing price drops or newly published availability. Requires `trackChanges=true`. |
| `requestDelayMs` | 500–10000, default 1000. |

#### Sample input

```json
{
    "storeUrls": ["https://www.allbirds.com"],
    "maxProductsPerStore": 100,
    "availabilityFilter": "all",
    "trackChanges": true,
    "onlyChangedRecords": false,
    "minPriceDropPercent": 5,
    "onlyActionableChanges": false,
    "requestDelayMs": 1000
}
```

Replace the sample store with stores you are permitted to monitor. The sample store is only an example of a public Shopify store; a store can change its feed or robots.txt at any time, and the Actor will then report the store as failed.

### Output

One dataset row per variant. Format example with placeholder values (not real data):

```json
{
    "store": "shop.example.com",
    "productId": 1,
    "productHandle": "blue-shirt",
    "productTitle": "Blue Shirt",
    "vendor": "Acme",
    "productType": "Shirts",
    "productUrl": "https://shop.example.com/products/blue-shirt?variant=11",
    "variantId": 11,
    "variantTitle": "S",
    "sku": "BS-S",
    "price": "20.00",
    "compareAtPrice": "25.00",
    "discountPercent": 20.0,
    "availability": "available",
    "variantUpdatedAt": "2026-01-01T00:00:00-00:00",
    "scrapedAt": "2026-01-02T08:00:00+00:00",
    "changeType": "price_changed",
    "previousPrice": "22.00",
    "previousAvailability": "available",
    "priceDelta": "-2.00"
}
```

`changeType` is one of `not_tracked`, `baseline`, `unchanged`, `new_variant`, `price_changed`, `availability_changed`, `price_and_availability_changed`. Additional output fields are `priceDropPercent` and `alertSignal` (`none`, `price_drop`, `restocked`, `price_drop_and_restock`). A change flag is **not** a notification and cannot verify checkout inventory.

A `SUMMARY` record in the default key-value store lists, per store, the status, products read, variants parsed or skipped, rows output and any error.

### Pricing event

The code emits one pay-per-event charge named `variant-record` per variant row pushed to the dataset. Rows are charged only after they were fetched and parsed successfully. Failed stores, blocked requests and invalid input produce no charged rows. The actual price per event is configured on the platform, not in this project. If your spending limit is reached, the run stops and keeps the previous snapshot untouched.

### Safety and compliance behavior

- HTTPS only; no credentials, ports, IP addresses or internal host names. Hosts resolving to non-public addresses are refused.
- `robots.txt` is fetched for every store and honored (wildcards, `$`, longest match, `Crawl-delay` up to 10 s). If robots.txt cannot be read safely (HTTP 401/403/429/5xx or a network error), the store is skipped.
- Redirects are followed only within the same host (max 3).
- Requests are sequential, paced, size-capped (12 MB per page), retried at most twice on HTTP 429/503.
- No proxies, no login, no browser, no personal data. The feed contains product data only.

### Limitations and caveats

- **Availability is not stock.** A store may publish `available: true` for products it oversells or backorders, and the flag can lag the real shop state. No quantities are provided.
- **No currency field.** `products.json` does not state the currency; prices are in whatever the store's feed uses for your request. Multi-market price variants are not covered.
- **Not every store works.** Stores may disable or restrict `products.json`, password-protect the shop, or disallow it in robots.txt. Those stores are reported as failed and not charged.
- **Published data only.** Hidden, unpublished or customer-segment-specific products and prices are not visible.
- **Change detection needs history.** The first run for a store and scope is a baseline. If a run was capped by `maxProductsPerStore`, variants beyond the cap are not compared, and a shift in the store's product ordering can make existing variants look `new_variant`. Removed variants are not reported.
- Snapshots are keyed by store and collection; changing filters on price or availability does not affect them.
- DNS is checked before each request, but the HTTP client resolves the host again; a hostile DNS rebinding setup is reduced, not fully excluded.
- Sample-size caveat: there are no benchmark or accuracy figures for this Actor yet. Test it on a few stores of your own before relying on it for decisions, and check a handful of rows against the live product pages.

### Local tests

Tests are offline and use only the standard library plus the project code:

```
python -m unittest discover -s tests -t .
```

# Actor input Schema

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

HTTPS addresses of Shopify stores (up to 10). Only the domain is used; any path is ignored. Replace the sample with stores you are allowed to monitor.

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

Upper bound of products read per store (each product can have several variants). Keeps runs small and cheap.

## `collectionHandle` (type: `string`):

Read only one collection, for example 'new-arrivals'. Applies to every store in the list. Leave empty for the whole catalog.

## `availabilityFilter` (type: `string`):

Which variants appear in the output. Change detection still sees all variants.

## `minPrice` (type: `number`):

Skip variants priced below this value (in the store's own currency, which the feed does not state).

## `maxPrice` (type: `number`):

Skip variants priced above this value (in the store's own currency).

## `trackChanges` (type: `boolean`):

Compare with the previous run of the same store and scope, using a small snapshot saved in a named key-value store. The first run is a baseline.

## `onlyChangedRecords` (type: `boolean`):

Output only variants with a new price, new availability, or that are new. Requires change tracking. The baseline run outputs nothing in this mode.

## `minPriceDropPercent` (type: `number`):

Flag a price-drop signal only when the published price decreased at least this percent since the last stored snapshot. Set 0 for any decrease. Baseline has no alert signal.

## `onlyActionableChanges` (type: `boolean`):

If enabled, output rows only when priceDropPercent meets the threshold or a published sold-out variant becomes available. Baseline outputs no rows; no emails are sent.

## `requestDelayMs` (type: `integer`):

Minimum pause between requests to a store. A longer Crawl-delay in robots.txt takes precedence (up to 10 seconds).

## Actor input object example

```json
{
  "storeUrls": [
    "https://www.allbirds.com"
  ],
  "maxProductsPerStore": 250,
  "availabilityFilter": "all",
  "trackChanges": true,
  "onlyChangedRecords": false,
  "minPriceDropPercent": 5,
  "onlyActionableChanges": false,
  "requestDelayMs": 1000
}
```

# Actor output Schema

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

Download rows from the latest run in JSON/CSV.

# 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"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("practical_ai_lab_jp/pal-shopify-variant-stock-feed").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"] }

# Run the Actor and wait for it to finish
run = client.actor("practical_ai_lab_jp/pal-shopify-variant-stock-feed").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"
  ]
}' |
apify call practical_ai_lab_jp/pal-shopify-variant-stock-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,practical_ai_lab_jp/pal-shopify-variant-stock-feed"
        }
    }
}
```

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/R1sQgeNylWpWCRbFU/builds/H3pMeSGxbgMESVE7Q/openapi.json
