# Shopify Brand & Assortment Shift Tracker (`practical_ai_lab_jp/pal-shopify-brand-assortment-shifts`) Actor

Compare dated Shopify catalog snapshots to see brand, category and product assortment changes, with partial-feed caveats.

- **URL**: https://apify.com/practical_ai_lab_jp/pal-shopify-brand-assortment-shifts.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 $20.00 / 1,000 assortment comparisons

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 Brand & Assortment Shift Tracker

Compare two or more dated snapshots of a Shopify product catalog and see how the assortment changed: which brands (vendors) and categories (product types) gained or lost share, which are new or gone, and which products were added, removed, repriced or changed availability. Every result carries explicit limitations so small or partial feeds are not over-interpreted.

### What you get

For each consecutive pair of snapshots of a store, the dataset contains:

- **summary** – product counts, added/removed products, price and availability changes, new/exited brands and categories, top gaining/losing brands, key overlap ratio, partial-feed flag and a list of plain-language limitations.
- **brand** and **category** rows – counts and share of catalog before/after, share change in percentage points, median lowest-variant price before/after, status (`new`, `exited`, `growing`, `shrinking`, `stable`) and a `notable` flag.
- **product_change** rows – `added`, `removed`, `price_changed`, `availability_changed`, `brand_changed`, `category_changed`.

### Input

Copy the prefilled example, or supply your own:

- `snapshots` – each item has `store`, `capturedAt` (YYYY-MM-DD), optional `label`, optional `partialFeed`, and `products` in Shopify `products.json` style (`id`, `handle`, `title`, `vendor`, `product_type`, `variants[].price`, `variants[].available`). At least two dated snapshots per store are required; the maximum is 20 snapshots and 20,000 products each.
- `liveStoreUrls` (optional, max 5) – public HTTPS Shopify store URLs. The Actor reads only the public `/products.json` feed, only if `robots.txt` allows it, with a page cap and a delay between requests. The snapshot is saved in a named key-value store (`pal-brand-assortment-snapshots`) and compared with the one from your previous run. **The first run only saves a baseline.**
- `maxPagesPerStore`, `minShareShiftPoints`, `maxProductChanges`, `requestDelaySeconds` – bounds and thresholds.

### Safety

Live fetching uses HTTPS only, rejects credentials, IP literals, non-standard ports and local-style host names, checks that the host resolves only to public addresses, and does not follow redirects. Responses are size-limited. No proxies, logins or paid services are used. Input snapshots are processed as provided and nothing else is contacted.

### Pricing

The code defines one pay-per-event event name, `assortment-comparison`, charged once per completed snapshot-pair comparison after its results are written. Invalid input, fetch errors, robots refusals and baseline-only runs are not charged. No payable plan is configured in this repository.

### Limitations (please read)

- Catalog snapshots are listings, not sales, demand or stock quantities. A product appearing or vanishing may be a listing or feed change, not a commercial event.
- Brand is the `vendor` field, which can be a store name or reseller; blank vendors are grouped as `(unbranded)`.
- Price is the lowest variant price in the store's feed currency; no currency conversion is done. Availability is true if any variant is available.
- Shares are based on product counts, not revenue. With fewer than 30 products, a single product can move a share by several points; results are labeled as small-sample.
- Public `products.json` feeds may be disabled, capped or incomplete. When the page cap is reached the result is flagged partial and missing products must not be read as removals.
- Products are matched by `id` (or `handle` when no id exists). Re-created products with new ids appear as one removal plus one addition.
- Live store hostnames are checked before the request, but a DNS change between the check and the connection is not fully excluded.
- Dates in live mode are the UTC date of the run.

### Local tests

Tests are offline: `python -m unittest discover -s tests` from the project folder.

# Actor input Schema

## `snapshots` (type: `array`):

Two or more snapshots per store. Each item: store, capturedAt (YYYY-MM-DD), optional label, optional partialFeed (true if the export is incomplete), and products (Shopify products.json style objects with id, handle, title, vendor, product_type, variants\[].price/available). Snapshots are compared in date order.

## `liveStoreUrls` (type: `array`):

Public HTTPS Shopify store URLs (max 5). The Actor reads only the public /products.json feed if robots.txt allows it, saves a snapshot in a named key-value store, and compares it with the snapshot saved by your previous run. The first run only saves a baseline and is not charged.

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

Each page holds up to 250 products. If the cap is reached the result is labeled partial.

## `minShareShiftPoints` (type: `number`):

Brand or category rows with at least this absolute share change are flagged notable. New and exited groups are always notable.

## `maxProductChanges` (type: `integer`):

Caps product-level rows; the summary reports when output was truncated.

## `requestDelaySeconds` (type: `number`):

Pause between page requests to a live store.

## Actor input object example

```json
{
  "snapshots": [
    {
      "store": "demo-store",
      "label": "October",
      "capturedAt": "2026-10-01",
      "products": [
        {
          "id": 1,
          "handle": "trail-shoe",
          "title": "Trail Shoe",
          "vendor": "Northpeak",
          "product_type": "Footwear",
          "variants": [
            {
              "price": "89.00",
              "available": true
            }
          ]
        },
        {
          "id": 2,
          "handle": "rain-jacket",
          "title": "Rain Jacket",
          "vendor": "Northpeak",
          "product_type": "Outerwear",
          "variants": [
            {
              "price": "120.00",
              "available": true
            }
          ]
        },
        {
          "id": 3,
          "handle": "wool-sock",
          "title": "Wool Sock",
          "vendor": "Fieldwool",
          "product_type": "Accessories",
          "variants": [
            {
              "price": "14.00",
              "available": true
            }
          ]
        }
      ]
    },
    {
      "store": "demo-store",
      "label": "November",
      "capturedAt": "2026-11-01",
      "products": [
        {
          "id": 1,
          "handle": "trail-shoe",
          "title": "Trail Shoe",
          "vendor": "Northpeak",
          "product_type": "Footwear",
          "variants": [
            {
              "price": "79.00",
              "available": true
            }
          ]
        },
        {
          "id": 2,
          "handle": "rain-jacket",
          "title": "Rain Jacket",
          "vendor": "Northpeak",
          "product_type": "Outerwear",
          "variants": [
            {
              "price": "120.00",
              "available": false
            }
          ]
        },
        {
          "id": 4,
          "handle": "city-bag",
          "title": "City Bag",
          "vendor": "Urbanloop",
          "product_type": "Bags",
          "variants": [
            {
              "price": "65.00",
              "available": true
            }
          ]
        }
      ]
    }
  ],
  "maxPagesPerStore": 4,
  "minShareShiftPoints": 2,
  "maxProductChanges": 500,
  "requestDelaySeconds": 1
}
```

# 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 = {
    "snapshots": [
        {
            "store": "demo-store",
            "label": "October",
            "capturedAt": "2026-10-01",
            "products": [
                {
                    "id": 1,
                    "handle": "trail-shoe",
                    "title": "Trail Shoe",
                    "vendor": "Northpeak",
                    "product_type": "Footwear",
                    "variants": [
                        {
                            "price": "89.00",
                            "available": true
                        }
                    ]
                },
                {
                    "id": 2,
                    "handle": "rain-jacket",
                    "title": "Rain Jacket",
                    "vendor": "Northpeak",
                    "product_type": "Outerwear",
                    "variants": [
                        {
                            "price": "120.00",
                            "available": true
                        }
                    ]
                },
                {
                    "id": 3,
                    "handle": "wool-sock",
                    "title": "Wool Sock",
                    "vendor": "Fieldwool",
                    "product_type": "Accessories",
                    "variants": [
                        {
                            "price": "14.00",
                            "available": true
                        }
                    ]
                }
            ]
        },
        {
            "store": "demo-store",
            "label": "November",
            "capturedAt": "2026-11-01",
            "products": [
                {
                    "id": 1,
                    "handle": "trail-shoe",
                    "title": "Trail Shoe",
                    "vendor": "Northpeak",
                    "product_type": "Footwear",
                    "variants": [
                        {
                            "price": "79.00",
                            "available": true
                        }
                    ]
                },
                {
                    "id": 2,
                    "handle": "rain-jacket",
                    "title": "Rain Jacket",
                    "vendor": "Northpeak",
                    "product_type": "Outerwear",
                    "variants": [
                        {
                            "price": "120.00",
                            "available": false
                        }
                    ]
                },
                {
                    "id": 4,
                    "handle": "city-bag",
                    "title": "City Bag",
                    "vendor": "Urbanloop",
                    "product_type": "Bags",
                    "variants": [
                        {
                            "price": "65.00",
                            "available": true
                        }
                    ]
                }
            ]
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("practical_ai_lab_jp/pal-shopify-brand-assortment-shifts").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 = { "snapshots": [
        {
            "store": "demo-store",
            "label": "October",
            "capturedAt": "2026-10-01",
            "products": [
                {
                    "id": 1,
                    "handle": "trail-shoe",
                    "title": "Trail Shoe",
                    "vendor": "Northpeak",
                    "product_type": "Footwear",
                    "variants": [{
                            "price": "89.00",
                            "available": True,
                        }],
                },
                {
                    "id": 2,
                    "handle": "rain-jacket",
                    "title": "Rain Jacket",
                    "vendor": "Northpeak",
                    "product_type": "Outerwear",
                    "variants": [{
                            "price": "120.00",
                            "available": True,
                        }],
                },
                {
                    "id": 3,
                    "handle": "wool-sock",
                    "title": "Wool Sock",
                    "vendor": "Fieldwool",
                    "product_type": "Accessories",
                    "variants": [{
                            "price": "14.00",
                            "available": True,
                        }],
                },
            ],
        },
        {
            "store": "demo-store",
            "label": "November",
            "capturedAt": "2026-11-01",
            "products": [
                {
                    "id": 1,
                    "handle": "trail-shoe",
                    "title": "Trail Shoe",
                    "vendor": "Northpeak",
                    "product_type": "Footwear",
                    "variants": [{
                            "price": "79.00",
                            "available": True,
                        }],
                },
                {
                    "id": 2,
                    "handle": "rain-jacket",
                    "title": "Rain Jacket",
                    "vendor": "Northpeak",
                    "product_type": "Outerwear",
                    "variants": [{
                            "price": "120.00",
                            "available": False,
                        }],
                },
                {
                    "id": 4,
                    "handle": "city-bag",
                    "title": "City Bag",
                    "vendor": "Urbanloop",
                    "product_type": "Bags",
                    "variants": [{
                            "price": "65.00",
                            "available": True,
                        }],
                },
            ],
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("practical_ai_lab_jp/pal-shopify-brand-assortment-shifts").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 '{
  "snapshots": [
    {
      "store": "demo-store",
      "label": "October",
      "capturedAt": "2026-10-01",
      "products": [
        {
          "id": 1,
          "handle": "trail-shoe",
          "title": "Trail Shoe",
          "vendor": "Northpeak",
          "product_type": "Footwear",
          "variants": [
            {
              "price": "89.00",
              "available": true
            }
          ]
        },
        {
          "id": 2,
          "handle": "rain-jacket",
          "title": "Rain Jacket",
          "vendor": "Northpeak",
          "product_type": "Outerwear",
          "variants": [
            {
              "price": "120.00",
              "available": true
            }
          ]
        },
        {
          "id": 3,
          "handle": "wool-sock",
          "title": "Wool Sock",
          "vendor": "Fieldwool",
          "product_type": "Accessories",
          "variants": [
            {
              "price": "14.00",
              "available": true
            }
          ]
        }
      ]
    },
    {
      "store": "demo-store",
      "label": "November",
      "capturedAt": "2026-11-01",
      "products": [
        {
          "id": 1,
          "handle": "trail-shoe",
          "title": "Trail Shoe",
          "vendor": "Northpeak",
          "product_type": "Footwear",
          "variants": [
            {
              "price": "79.00",
              "available": true
            }
          ]
        },
        {
          "id": 2,
          "handle": "rain-jacket",
          "title": "Rain Jacket",
          "vendor": "Northpeak",
          "product_type": "Outerwear",
          "variants": [
            {
              "price": "120.00",
              "available": false
            }
          ]
        },
        {
          "id": 4,
          "handle": "city-bag",
          "title": "City Bag",
          "vendor": "Urbanloop",
          "product_type": "Bags",
          "variants": [
            {
              "price": "65.00",
              "available": true
            }
          ]
        }
      ]
    }
  ]
}' |
apify call practical_ai_lab_jp/pal-shopify-brand-assortment-shifts --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-brand-assortment-shifts"
        }
    }
}
```

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/SgfmzzaWrDvciMnEd/builds/XtrhMQfgSn6Id1jZL/openapi.json
