# Shopify Variant Availability Trends (`practical_ai_lab_jp/pal-shopify-variant-availability-trends`) Actor

Compare two public Shopify variant snapshots and flag sold-out and back-in-stock transitions without guessing inventory quantities.

- **URL**: https://apify.com/practical_ai_lab_jp/pal-shopify-variant-availability-trends.md
- **Developed by:** [Practical AI Lab](https://apify.com/practical_ai_lab_jp) (community)
- **Categories:** E-commerce, Developer tools
- **Security:** Limited permissions
- **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 transitions

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 Trends

Compare two public Shopify variant snapshots and list which variants **sold out** or came **back in stock** between them. The Actor works only from Shopify's public true/false `available` flag. It does not estimate inventory quantities, sales volume or demand.

### What it does

- Matches variants across an earlier ("before") and later ("after") snapshot by variant id.
- Labels each variant: `sold_out`, `back_in_stock`, `new_sold_out`, `new_available`, `removed`, `availability_unknown`, `still_sold_out`, `still_available`.
- Flags whether a whole product has no available variant (`productAllSoldOutBefore` / `productAllSoldOutAfter`), plus price changes seen on the same variant.
- Saves a compact `SNAPSHOT` in the key-value store. Paste it as `beforeSnapshot` in your next run to build a recurring comparison without storing raw feeds yourself.
- Saves a `SUMMARY` with counts, snapshot interval and warnings.

### Input

| Field | Meaning |
| --- | --- |
| `beforeSnapshot` (required) | Shopify `products.json` object, or a previous run's `SNAPSHOT`. |
| `afterSnapshot` | Later snapshot in the same format. |
| `afterStoreUrl` | Alternative: a public https store domain. The Actor reads `/products.json` after checking `robots.txt`. Provide exactly one of `afterSnapshot` or `afterStoreUrl`; clear the pre-filled `afterSnapshot` when using a URL. |
| `maxPages` | Live feed pages to read (250 products each, 1-20, default 5). |
| `beforeCapturedAt`, `afterCapturedAt` | Optional ISO 8601 times used to report the interval. |
| `includeUnchanged` | Also output variants whose availability did not change (default false). |
| `maxRecords` | Upper bound on dataset rows (default 1000). |

The default input is a small copyable example and runs without any network access.

#### Example input

```json
{
  "beforeSnapshot": {"products": [{"id": 1002, "handle": "ceramic-mug", "title": "Ceramic Mug", "variants": [
    {"id": 2003, "title": "White", "sku": "MUG-WHT", "available": false, "price": "14.00"}]}]},
  "afterSnapshot": {"products": [{"id": 1002, "handle": "ceramic-mug", "title": "Ceramic Mug", "variants": [
    {"id": 2003, "title": "White", "sku": "MUG-WHT", "available": true, "price": "15.00"}]}]},
  "beforeCapturedAt": "2026-10-01T09:00:00Z",
  "afterCapturedAt": "2026-10-08T09:00:00Z"
}
```

#### Example output row (illustrative, derived by hand from the input above)

```json
{
  "changeType": "back_in_stock",
  "variantId": "2003",
  "productHandle": "ceramic-mug",
  "productPath": "/products/ceramic-mug",
  "variantTitle": "White",
  "availabilityBefore": false,
  "availabilityAfter": true,
  "priceBefore": "14",
  "priceAfter": "15",
  "priceChanged": true,
  "productAllSoldOutBefore": true,
  "productAllSoldOutAfter": false
}
```

Prices are normalized decimal strings (`"14.00"` becomes `"14"`).

### Safety and live-feed rules

- Live reads use https only, a public domain name (no IP addresses, credentials, custom ports or local/internal suffixes), and refuse hosts that resolve to non-public addresses.
- `robots.txt` is checked first; a disallow, or a robots server error, stops the run.
- Redirects are not followed, pages are capped at 20, responses are size-limited, and requests are spaced one second apart.
- Only the feed fields needed for the comparison are kept in memory.
- Nothing is read from Google Maps, Amazon or any non-Shopify site.

### Pricing event

The code reports one pay-per-event event, `variant-transition`, for each delivered `sold_out` or `back_in_stock` row. Context rows (new, removed, unchanged, unknown) are never charged, and runs that fail validation or fetching push nothing and charge nothing. No payable plan is configured in this repository; pricing must be set separately by the publisher.

### Limitations and sample-size caveats

- **Availability only.** `available` is a boolean. Quantities, sales and restock volume are not exposed and are not guessed.
- **Two points in time.** A variant that sold out and returned between snapshots looks unchanged. Longer intervals hide more.
- **Small samples mislead.** A handful of matched variants says little about a store; the summary warns below 30 matched variants. Counts are observations, not forecasts.
- **Feed quirks.** Some stores hide, customize or rate-limit `/products.json`; draft or unpublished products never appear. Products disappearing from a feed are reported as `removed`, which may mean unpublished rather than discontinued.
- **Incomplete live reads.** If `maxPages` is reached the snapshot can be partial and may produce false `removed` rows; a warning is added.
- **Snapshot consistency.** Both snapshots should come from the same store and market. Variant ids must be unique and are the only matching key.
- **DNS timing.** Public-address checks happen before connecting, but a hostile DNS server could still change answers between check and connection.
- **Demand not proven.** Similar Shopify scrapers have free-tier users, but paid demand for this specific comparison has not been validated.

### Local development

Tests are offline: `python -m unittest discover -s tests` from the project root. Running the Actor requires the Apify SDK 3.x from `requirements.txt`.

# Actor input Schema

## `beforeSnapshot` (type: `object`):

Earlier public snapshot: a Shopify products.json object ("products" with nested "variants"), or the SNAPSHOT saved by a previous run of this Actor.

## `afterSnapshot` (type: `object`):

Later snapshot in the same format. Leave empty if you set 'Live store URL' instead. Provide exactly one of the two.

## `afterStoreUrl` (type: `string`):

Optional public https store domain, for example https://shop.example.com. The Actor reads only its public /products.json feed after checking robots.txt. Clear the 'Later snapshot' field when using this.

## `maxPages` (type: `integer`):

Upper bound on live feed pages read when a store URL is used.

## `beforeCapturedAt` (type: `string`):

Optional, for example 2026-10-01T09:00:00Z. Used to report the interval between snapshots.

## `afterCapturedAt` (type: `string`):

Optional. Defaults to the run time when a live store URL is used.

## `includeUnchanged` (type: `boolean`):

Also output variants that stayed available or stayed sold out. These rows are context only.

## `maxRecords` (type: `integer`):

Upper bound on dataset rows. Sold-out and back-in-stock rows are listed first.

## Actor input object example

```json
{
  "beforeSnapshot": {
    "products": [
      {
        "id": 1001,
        "handle": "canvas-tote",
        "title": "Canvas Tote",
        "variants": [
          {
            "id": 2001,
            "title": "Natural",
            "sku": "TOTE-NAT",
            "available": true,
            "price": "24.00"
          },
          {
            "id": 2002,
            "title": "Black",
            "sku": "TOTE-BLK",
            "available": true,
            "price": "24.00"
          }
        ]
      },
      {
        "id": 1002,
        "handle": "ceramic-mug",
        "title": "Ceramic Mug",
        "variants": [
          {
            "id": 2003,
            "title": "White",
            "sku": "MUG-WHT",
            "available": false,
            "price": "14.00"
          },
          {
            "id": 2004,
            "title": "Blue",
            "sku": "MUG-BLU",
            "available": true,
            "price": "14.00"
          }
        ]
      },
      {
        "id": 1003,
        "handle": "notebook",
        "title": "Notebook",
        "variants": [
          {
            "id": 2005,
            "title": "A5",
            "sku": "NB-A5",
            "available": true,
            "price": "9.00"
          }
        ]
      }
    ]
  },
  "afterSnapshot": {
    "products": [
      {
        "id": 1001,
        "handle": "canvas-tote",
        "title": "Canvas Tote",
        "variants": [
          {
            "id": 2001,
            "title": "Natural",
            "sku": "TOTE-NAT",
            "available": false,
            "price": "24.00"
          },
          {
            "id": 2002,
            "title": "Black",
            "sku": "TOTE-BLK",
            "available": true,
            "price": "24.00"
          }
        ]
      },
      {
        "id": 1002,
        "handle": "ceramic-mug",
        "title": "Ceramic Mug",
        "variants": [
          {
            "id": 2003,
            "title": "White",
            "sku": "MUG-WHT",
            "available": true,
            "price": "15.00"
          },
          {
            "id": 2004,
            "title": "Blue",
            "sku": "MUG-BLU",
            "available": false,
            "price": "14.00"
          }
        ]
      },
      {
        "id": 1003,
        "handle": "notebook",
        "title": "Notebook",
        "variants": [
          {
            "id": 2005,
            "title": "A5",
            "sku": "NB-A5",
            "available": false,
            "price": "9.00"
          },
          {
            "id": 2006,
            "title": "A6",
            "sku": "NB-A6",
            "available": false,
            "price": "7.00"
          }
        ]
      }
    ]
  },
  "maxPages": 5,
  "includeUnchanged": false,
  "maxRecords": 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 = {
    "beforeSnapshot": {
        "products": [
            {
                "id": 1001,
                "handle": "canvas-tote",
                "title": "Canvas Tote",
                "variants": [
                    {
                        "id": 2001,
                        "title": "Natural",
                        "sku": "TOTE-NAT",
                        "available": true,
                        "price": "24.00"
                    },
                    {
                        "id": 2002,
                        "title": "Black",
                        "sku": "TOTE-BLK",
                        "available": true,
                        "price": "24.00"
                    }
                ]
            },
            {
                "id": 1002,
                "handle": "ceramic-mug",
                "title": "Ceramic Mug",
                "variants": [
                    {
                        "id": 2003,
                        "title": "White",
                        "sku": "MUG-WHT",
                        "available": false,
                        "price": "14.00"
                    },
                    {
                        "id": 2004,
                        "title": "Blue",
                        "sku": "MUG-BLU",
                        "available": true,
                        "price": "14.00"
                    }
                ]
            },
            {
                "id": 1003,
                "handle": "notebook",
                "title": "Notebook",
                "variants": [
                    {
                        "id": 2005,
                        "title": "A5",
                        "sku": "NB-A5",
                        "available": true,
                        "price": "9.00"
                    }
                ]
            }
        ]
    },
    "afterSnapshot": {
        "products": [
            {
                "id": 1001,
                "handle": "canvas-tote",
                "title": "Canvas Tote",
                "variants": [
                    {
                        "id": 2001,
                        "title": "Natural",
                        "sku": "TOTE-NAT",
                        "available": false,
                        "price": "24.00"
                    },
                    {
                        "id": 2002,
                        "title": "Black",
                        "sku": "TOTE-BLK",
                        "available": true,
                        "price": "24.00"
                    }
                ]
            },
            {
                "id": 1002,
                "handle": "ceramic-mug",
                "title": "Ceramic Mug",
                "variants": [
                    {
                        "id": 2003,
                        "title": "White",
                        "sku": "MUG-WHT",
                        "available": true,
                        "price": "15.00"
                    },
                    {
                        "id": 2004,
                        "title": "Blue",
                        "sku": "MUG-BLU",
                        "available": false,
                        "price": "14.00"
                    }
                ]
            },
            {
                "id": 1003,
                "handle": "notebook",
                "title": "Notebook",
                "variants": [
                    {
                        "id": 2005,
                        "title": "A5",
                        "sku": "NB-A5",
                        "available": false,
                        "price": "9.00"
                    },
                    {
                        "id": 2006,
                        "title": "A6",
                        "sku": "NB-A6",
                        "available": false,
                        "price": "7.00"
                    }
                ]
            }
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("practical_ai_lab_jp/pal-shopify-variant-availability-trends").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 = {
    "beforeSnapshot": { "products": [
            {
                "id": 1001,
                "handle": "canvas-tote",
                "title": "Canvas Tote",
                "variants": [
                    {
                        "id": 2001,
                        "title": "Natural",
                        "sku": "TOTE-NAT",
                        "available": True,
                        "price": "24.00",
                    },
                    {
                        "id": 2002,
                        "title": "Black",
                        "sku": "TOTE-BLK",
                        "available": True,
                        "price": "24.00",
                    },
                ],
            },
            {
                "id": 1002,
                "handle": "ceramic-mug",
                "title": "Ceramic Mug",
                "variants": [
                    {
                        "id": 2003,
                        "title": "White",
                        "sku": "MUG-WHT",
                        "available": False,
                        "price": "14.00",
                    },
                    {
                        "id": 2004,
                        "title": "Blue",
                        "sku": "MUG-BLU",
                        "available": True,
                        "price": "14.00",
                    },
                ],
            },
            {
                "id": 1003,
                "handle": "notebook",
                "title": "Notebook",
                "variants": [{
                        "id": 2005,
                        "title": "A5",
                        "sku": "NB-A5",
                        "available": True,
                        "price": "9.00",
                    }],
            },
        ] },
    "afterSnapshot": { "products": [
            {
                "id": 1001,
                "handle": "canvas-tote",
                "title": "Canvas Tote",
                "variants": [
                    {
                        "id": 2001,
                        "title": "Natural",
                        "sku": "TOTE-NAT",
                        "available": False,
                        "price": "24.00",
                    },
                    {
                        "id": 2002,
                        "title": "Black",
                        "sku": "TOTE-BLK",
                        "available": True,
                        "price": "24.00",
                    },
                ],
            },
            {
                "id": 1002,
                "handle": "ceramic-mug",
                "title": "Ceramic Mug",
                "variants": [
                    {
                        "id": 2003,
                        "title": "White",
                        "sku": "MUG-WHT",
                        "available": True,
                        "price": "15.00",
                    },
                    {
                        "id": 2004,
                        "title": "Blue",
                        "sku": "MUG-BLU",
                        "available": False,
                        "price": "14.00",
                    },
                ],
            },
            {
                "id": 1003,
                "handle": "notebook",
                "title": "Notebook",
                "variants": [
                    {
                        "id": 2005,
                        "title": "A5",
                        "sku": "NB-A5",
                        "available": False,
                        "price": "9.00",
                    },
                    {
                        "id": 2006,
                        "title": "A6",
                        "sku": "NB-A6",
                        "available": False,
                        "price": "7.00",
                    },
                ],
            },
        ] },
}

# Run the Actor and wait for it to finish
run = client.actor("practical_ai_lab_jp/pal-shopify-variant-availability-trends").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 '{
  "beforeSnapshot": {
    "products": [
      {
        "id": 1001,
        "handle": "canvas-tote",
        "title": "Canvas Tote",
        "variants": [
          {
            "id": 2001,
            "title": "Natural",
            "sku": "TOTE-NAT",
            "available": true,
            "price": "24.00"
          },
          {
            "id": 2002,
            "title": "Black",
            "sku": "TOTE-BLK",
            "available": true,
            "price": "24.00"
          }
        ]
      },
      {
        "id": 1002,
        "handle": "ceramic-mug",
        "title": "Ceramic Mug",
        "variants": [
          {
            "id": 2003,
            "title": "White",
            "sku": "MUG-WHT",
            "available": false,
            "price": "14.00"
          },
          {
            "id": 2004,
            "title": "Blue",
            "sku": "MUG-BLU",
            "available": true,
            "price": "14.00"
          }
        ]
      },
      {
        "id": 1003,
        "handle": "notebook",
        "title": "Notebook",
        "variants": [
          {
            "id": 2005,
            "title": "A5",
            "sku": "NB-A5",
            "available": true,
            "price": "9.00"
          }
        ]
      }
    ]
  },
  "afterSnapshot": {
    "products": [
      {
        "id": 1001,
        "handle": "canvas-tote",
        "title": "Canvas Tote",
        "variants": [
          {
            "id": 2001,
            "title": "Natural",
            "sku": "TOTE-NAT",
            "available": false,
            "price": "24.00"
          },
          {
            "id": 2002,
            "title": "Black",
            "sku": "TOTE-BLK",
            "available": true,
            "price": "24.00"
          }
        ]
      },
      {
        "id": 1002,
        "handle": "ceramic-mug",
        "title": "Ceramic Mug",
        "variants": [
          {
            "id": 2003,
            "title": "White",
            "sku": "MUG-WHT",
            "available": true,
            "price": "15.00"
          },
          {
            "id": 2004,
            "title": "Blue",
            "sku": "MUG-BLU",
            "available": false,
            "price": "14.00"
          }
        ]
      },
      {
        "id": 1003,
        "handle": "notebook",
        "title": "Notebook",
        "variants": [
          {
            "id": 2005,
            "title": "A5",
            "sku": "NB-A5",
            "available": false,
            "price": "9.00"
          },
          {
            "id": 2006,
            "title": "A6",
            "sku": "NB-A6",
            "available": false,
            "price": "7.00"
          }
        ]
      }
    ]
  }
}' |
apify call practical_ai_lab_jp/pal-shopify-variant-availability-trends --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-availability-trends"
        }
    }
}
```

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/6pSiiLsVTBRlpadHt/builds/lZXMC6sf3MtpbepOY/openapi.json
