# Shopify Store Products, Prices & Stock Tracker (`fairfetch/shopify-price-stock-tracker`) Actor

Export products from Shopify stores, each with its variants' prices, compare-at prices, SKUs and stock, via the public products.json feed. Track price drops, restocks, sell-outs, new and removed products between runs. $1 per 1,000 products.

- **URL**: https://apify.com/fairfetch/shopify-price-stock-tracker.md
- **Developed by:** [Naveen Raja](https://apify.com/fairfetch) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 products

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 Store Products, Prices & Stock Tracker

Export **the products of Shopify stores, each with every variant's price, compare-at price, SKU and stock status**, in one clean JSON format. The Actor reads the public `products.json` feed that Shopify online stores publish, so it is fast and complete, with no browser and no login. Apify's residential proxy is on by default (included in the price) because Shopify throttles cloud servers.

Turn on **change tracking** and every later run tells you what changed since the previous run: **price drops, price increases, back in stock, sold out, new products, removed products, and variants added or removed**. Schedule it daily (or hourly) and you have a competitor price monitor or a restock alert.

**Price: $1 per 1,000 products** ($0.001 per product row, all variants included). With **Output only changed rows**, each product checked costs $0.0001 and each changed product returned $0.001. Stores that can't be read cost nothing.

### What you get

- **One row per product**, with its **variants nested** in `variants` (each size / colour with its own price, compare-at price, SKU, options, stock status, link and image).
- Product summary: lowest and highest price, whether any variant is on sale (and the largest discount), whether any variant is in stock, and how many variants are in stock.
- Product data: title, handle, vendor (brand), product type, tags, image, product URL, created and published dates.
- With tracking on: `changeType` (`new`, `removed`, `price_down`, `price_up`, `back_in_stock`, `out_of_stock`, `variant_added`, `variant_removed`, `unchanged`), a `changes` list naming every variant that changed with its old and new price and stock, `previousPrice`, `priceChangePct`, `previousAvailable` and `firstSeenAt`.
- Whole store or selected **collections** (e.g. only `sale` or `new-arrivals`).
- Optional clean-text **product description** (HTML converted to plain text).
- Built for automation: the same fields on every row, pay-per-event pricing with no usage fees, limited permissions, a machine-readable `SUMMARY` record, free error rows that say why a store failed, and a clean stop when your spending limit is reached.

### Use cases

- **Competitor price monitoring**: track a rival's whole catalog and get only the products whose price changed each day, with the old price and the change in percent.
- **Restock and sell-out alerts**: watch a store or a collection and get a row whenever a size or colour comes back in stock or sells out; `changes` says which one.
- **Catalog export**: download a complete product feed (titles, SKUs, prices, images, links) as JSON, CSV or Excel for analysis, comparison sites or a migration.
- **Sale and discount tracking**: filter `onSale` rows and sort by `discountPct` to see what a store is discounting right now.
- **New product launches**: with tracking on, new products show up as `new`, so you can follow a brand's drops.

### How to use

1. Add stores in **Shopify stores** (one per line): a domain (`allbirds.com`), a store URL, a collection URL (`https://www.deathwishcoffee.com/collections/coffee`) or a `name.myshopify.com` address. Enter the domain, not the store's name.
2. Optional: add **Collection handles** to read only those collections of every store.
3. Optional: set **Max products per store** (newest products first).
4. Optional: turn on **Track price and stock changes**, and **Output only changed rows** if you only want the changes. Use the same **Tracking ID** for every run of the same monitor.
5. Run the Actor (or schedule it) and download the results as JSON, CSV or Excel, or read them via API. A per-store summary is saved as the `SUMMARY` record in the run's key-value store.

#### Input example

```json
{
  "stores": [
    "deathwishcoffee.com",
    "https://www.deathwishcoffee.com/collections/coffee"
  ],
  "trackChanges": true,
  "onlyChanges": true,
  "trackingId": "my-competitor-monitor"
}
```

Other options: `"collections": ["sale"]`, `"maxProductsPerStore": 100`, `"includeDescription": true`.

#### Output example

A real product row from a test run on 20 Oct 2026 (one variant shown, tags shortened):

```json
{
  "store": "deathwishcoffee.com",
  "storeName": "Death Wish Coffee",
  "productId": "6608733995063",
  "handle": "gingerdead-coffee-cups",
  "title": "Gingerdead Single-Serve Pods",
  "vendor": "Death Wish Coffee Company",
  "productType": "Coffee",
  "tags": ["Gingerdead", "Single Serve"],
  "price": 12.99,
  "priceMax": 38.97,
  "compareAtPrice": null,
  "onSale": false,
  "discountPct": null,
  "currency": "USD",
  "available": true,
  "variantCount": 3,
  "availableVariants": 3,
  "url": "https://deathwishcoffee.com/products/gingerdead-coffee-cups",
  "imageUrl": "https://cdn.shopify.com/s/files/1/0271/7209/files/Gingerdead_10ct_Angle_Vertical_Pods_DTC.jpg?v=1791810372",
  "createdAt": "2021-11-02T18:17:23Z",
  "publishedAt": "2026-10-08T13:15:33Z",
  "variants": [
    {
      "variantId": "39534412496951",
      "variantTitle": "10-count",
      "sku": "LCGNGD10",
      "options": { "Size": "10-count" },
      "variantPrice": 12.99,
      "variantCompareAtPrice": null,
      "variantOnSale": false,
      "variantDiscountPct": null,
      "variantAvailable": true,
      "variantUrl": "https://deathwishcoffee.com/products/gingerdead-coffee-cups?variant=39534412496951",
      "variantImageUrl": "https://cdn.shopify.com/s/files/1/0271/7209/files/Gingerdead_10ct_Angle_Vertical_Pods_DTC.jpg?v=1791810372"
    }
  ],
  "changeType": null,
  "changes": null,
  "previousPrice": null,
  "priceChangePct": null,
  "previousAvailable": null,
  "firstSeenAt": null,
  "scrapedAt": "2026-10-20T00:21:01Z"
}
```

With tracking on, a changed product carries the change (a real row from a monitor run on 19 Oct 2026 UTC, shown with the field names of the current version; other fields as above). With tracking on, each variant also has `variantChangeType`, `variantPreviousPrice`, `variantPriceChangePct`, `variantPreviousAvailable` and `variantFirstSeenAt`:

```json
{
  "title": "Men's Tree Runner NZ - Medium Grey (Blizzard Sole)",
  "price": 100,
  "available": true,
  "variantCount": 14,
  "availableVariants": 2,
  "changeType": "out_of_stock",
  "changes": [
    { "variantId": "41206419226704", "variantTitle": "10", "sku": "A11925M100", "change": "out_of_stock",
      "variantPrice": 100, "variantPreviousPrice": 100, "variantAvailable": false, "variantPreviousAvailable": true },
    { "variantId": "41206419292240", "variantTitle": "11", "sku": "A11925M110", "change": "out_of_stock",
      "variantPrice": 100, "variantPreviousPrice": 100, "variantAvailable": false, "variantPreviousAvailable": true }
  ],
  "previousPrice": 100,
  "priceChangePct": 0,
  "previousAvailable": true,
  "firstSeenAt": "2026-10-19T17:12:50Z"
}
```

A **removed** product keeps the identifying fields saved when it was last seen (title, handle, URL, vendor, and in `variants` each variant's SKU, `variantPreviousPrice` and `variantPreviousAvailable`); `price` and `available` are `null` because the product is no longer listed.

A store that can't be read gives one **free error row** with `input`, `error` and `errorKind` (all product fields `null`); see the **Errors** view.

#### Output fields

| Field | Description |
|---|---|
| `store`, `storeName` | Store domain (without `www.`) and the store name published by Shopify |
| `productId`, `handle`, `title` | Shopify product ID, URL handle and title |
| `vendor`, `productType`, `tags` | Brand / vendor, product type and tags as set by the store |
| `price`, `priceMax` | Lowest and highest variant price |
| `onSale`, `discountPct`, `compareAtPrice` | Whether any variant is on sale; the largest discount of any variant in percent, and that variant's "was" price |
| `currency` | The store's main currency (ISO code) |
| `available`, `variantCount`, `availableVariants` | In stock if any variant is; number of variants and of variants in stock. The public feed has no stock quantities |
| `url`, `imageUrl` | Product page and image |
| `createdAt`, `publishedAt` | ISO 8601 timestamps in UTC |
| `variants` | Every variant: `variantId`, `variantTitle` (e.g. `Black / M`), `sku`, `options`, `variantPrice`, `variantCompareAtPrice`, `variantOnSale`, `variantDiscountPct`, `variantAvailable`, `variantUrl` (with `?variant=`), `variantImageUrl` |
| `description` | Plain-text description (only with **Include product description**) |
| `changeType`, `changes` | Change since the previous run (tracking on; `null` when off), and the variants that changed |
| `previousPrice`, `priceChangePct`, `previousAvailable` | Lowest price and stock in the previous run, and the change of the lowest price in percent |
| `firstSeenAt`, `scrapedAt` | When this tracker first saw the product, and when this run read it |
| `input`, `error`, `errorKind` | Only on free error rows |

There is no "updated at" field: Shopify's public feed now returns the time of the request as every product's `updated_at`, so it says nothing about the product.

The dataset has ready-made views: **Products**, **Changes**, **Errors** and **All fields**.

### How change tracking works

With **Track price and stock changes** on, the Actor saves a compact, **encrypted** snapshot of each store in a named key-value store in your Apify account (`shopify-price-stock-tracking`, one record per store, collection scope and Tracking ID). On the next run every variant is compared with it, and each product row reports what happened to its variants:

| `changeType` | Meaning |
|---|---|
| `new` | Product first seen by this monitor (in a monitor that has only ever read part of the catalog: published or re-published since the previous run, see below) |
| `back_in_stock` / `out_of_stock` | A variant became available / unavailable |
| `price_down` / `price_up` | A variant is cheaper / dearer than in the previous run |
| `variant_added` / `variant_removed` | The product got a new variant / lost one |
| `removed` | Product was listed before and is no longer listed. Output once, then forgotten |
| `unchanged` | None of the above |

If several apply, `changeType` is the first in this table and `changes` lists every changed variant (`change`: `back_in_stock`, `out_of_stock`, `price_down`, `price_up`, `new` or `removed`).

Good to know:

- **Output only changed rows** skips unchanged products. Each product compared costs $0.0001 (`product-checked`, once per product and run), and each changed product returned $0.001. The first run of a store saves a baseline and returns nothing for it (its products are checked and cost $0.0001 each); changes appear from the second run.
- If a run stops before a store is fully read (for example at your maximum charge), the next run completes the baseline: products not in it yet are added without being reported (or charged) as `new`. Products your maximum charge can't pay for are not compared or remembered in that run.
- Without **Output only changed rows**, the first run returns every product as `new`.
- Products that vanish from a store's listing are re-checked one by one on their own page before they are reported as `removed` (the feed can shift while a big store is being read). If there are more than a run can check in about two minutes, or more than your maximum charge could pay for, the rest are checked in the next run. In collection mode, a product that leaves the collection is reported as `removed` from that collection; when the collection spans several pages, it is read a second time first, and only products missing twice are reported.
- Removed products are only reported when the whole store (or collection) was read in that run: without **Max products per store**, or with a limit above the catalog size. If a run stops early (network trouble or your spending limit), unreported changes are kept and reported next time.
- **Max products per store** limits the products read (newest first), so with **Output only changed rows** it is the number of products checked per run.
- **Monitors that have only ever read part of the catalog** (every run used **Max products per store**) don't know the older products outside their window. When such a monitor meets a product it doesn't know, it reports `new` only if the product was published after the previous run (or, without a publish date, created after it; with neither date it is not `new`). Otherwise the product is added as `unchanged`: without output and for the price of the check with **Output only changed rows**. So raising or removing the limit doesn't report the whole catalog as `new`, and older products that move into the first N of a manually sorted collection aren't either. The trade-off: such a monitor also doesn't report an **older** product that is newly **added to a collection** (for example put on sale). To monitor additions to a collection, run the monitor once without a limit (or keep it at 0): from then on it knows every product, and every product it doesn't know is reported as `new`, also in runs with a limit.
- A store that **re-publishes** an old product gives it a new publish date, so in a monitor of the previous point it is reported as `new`; its `createdAt` shows that it is an old product.
- Use different **Tracking IDs** for independent monitors. Changing the collections starts a new snapshot.
- Monitors started with version 1.0 continue with the same **Tracking ID**. Monitors that used one row per variant (the default) keep their snapshot. Monitors that used `outputLevel: "product"` are compared once by lowest price and stock, and per variant from the next run; their old plain-text record is replaced by an encrypted one.

### Pricing

Pay per event, no subscription and no extra usage fees:

| Event | Price |
|---|---|
| `product`: one product row in the dataset, with all its variants (including new, changed and removed products) | **$0.001** ($1 per 1,000 products) |
| `product-checked`: one product read and compared with the previous run, **only with Output only changed rows** (once per product and run, also on the first run that saves the baseline) | **$0.0001** ($0.10 per 1,000) |
| Actor start (standard Apify event, per run) | $0.00005 |

Examples:

- Full export of a 692-product store with 7,454 variants (allbirds.com): about **$0.69**.
- A 148-product store: about $0.15.
- A daily "only changed rows" monitor of a 692-product store that finds 10 changed products: 692 x $0.0001 + 10 x $0.001 = about **$0.08 per run**.
- A 10,000-product store with **Output only changed rows**: about $1.00 per run plus $0.001 per changed product. Limit the products checked with **Max products per store** or watch a collection to pay less.

Apify platform costs (compute, storage, proxy, data transfer) are included in the price. Stores that fail (not Shopify, unknown domain, feed disabled, blocked) and their error rows are free, and so is the `SUMMARY` record. Set **Maximum cost per run** in the run options (or `maxTotalChargeUsd` via API) to cap spending; the Actor stops cleanly when the cap is reached, and everything returned up to that point is in the dataset.

### Use it from the API or an app

Run synchronously and get the rows in one call (keep your own API token in the `APIFY_TOKEN` environment variable):

```bash
curl -X POST "https://api.apify.com/v2/acts/fairfetch~shopify-price-stock-tracker/run-sync-get-dataset-items?maxTotalChargeUsd=0.50" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"stores": ["allbirds.com"], "collections": ["sale"]}'
```

The Actor runs with limited permissions and pay-per-event pricing without usage fees. Each run also writes a machine-readable `SUMMARY` record with per-store counts, errors and notes.

### Performance and limits

- One request per 250 products. In our cloud tests (Apify, residential proxy) a 692-product store was read in about 5 seconds and the 148-product store of the example in about 3 seconds; larger stores take longer, since each page of 250 products is one request and a store gets at most two requests per second.
- Requests are polite: one request at a time per store and at most 2 requests per second per store, automatic retries with exponential backoff for network and server errors, and timeouts. Several stores are read in parallel, each with its own proxy session.
- Shopify's rate limit (HTTP 429) slows the store down and is retried from a new proxy session. Shopify's bot protection (HTTP 430) is retried **at most once**, from another proxy IP address; if it comes again, the store is reported as `blocked` and nothing is charged for it.
- Shopify's public feed lists at most **25,000 products** per store or per collection. For bigger stores, add collection handles; the run summary tells you when a store hit the limit (removed-product detection is skipped for that store).
- The feed lists products published to the online store. Prices are the store's main-currency prices; stores selling in several countries may show converted prices on their website.
- Memory: the default 512 MB handles stores with tens of thousands of variants. For tracking stores with more than about 60,000 variants, run with 1 GB.
- If the platform moves a run to another server, the run continues where it was: rows already delivered are not repeated or charged again.

### Which stores work

Shopify stores that use Shopify's standard online store. The Actor checks each store and reports problems as a free error row, in the run summary and in the status message, then continues with the other stores. A run where every store fails because of the input still finishes successfully, with the error rows:

- **Invalid entry** (`invalid_input`): a store name with spaces ("Death Wish Coffee") or a bare word ("allbirds"). Enter the domain or the full `name.myshopify.com` address.
- **Not found** (`not_found`): the domain does not exist, or Shopify says "Store unavailable".
- **Not a Shopify store** (`not_shopify`): the site does not run on Shopify.
- **Headless Shopify store** (`headless`): the brand uses Shopify behind a custom or Hydrogen storefront that does not serve the public feed. If you know the store's `name.myshopify.com` address, try that.
- **Feed disabled** (`disabled`) or **password-protected** (`password`): the store has switched off its public product feed, or is not open to the public yet.
- **Blocked** (`blocked`): the store refused access (HTTP 403, or Shopify's bot protection twice). Try again later.

### FAQ

**Do I need a Shopify account, app or API key?**
No. The Actor reads the same public product feed that the store's own website exposes to every visitor.

**How do I know if a store runs on Shopify?**
Just add it. If it isn't on Shopify (or doesn't serve the feed) the error row and the run summary say so, and you are not charged for it.

**Why one row per product, not per variant?**
A product row carries all its variants in `variants`, so you pay once per product however many sizes and colours it has. To get one row per variant, read the dataset through Apify's API with `unwind=variants` (for example `https://api.apify.com/v2/datasets/<datasetId>/items?unwind=variants&format=csv`): each row then has the product's fields and one variant's `variant…` fields side by side.

**What does `product-checked` cost me, and when?**
Only with **Output only changed rows**: $0.0001 for each product the run reads and compares (also on the first run, which saves the baseline). A full export, or tracking without that option, never charges it.

**Can I monitor only a sale or new-arrivals collection?**
Yes. Paste the collection URL (`https://store.com/collections/sale`) or add the handle (`sale`) under **Collection handles**.

**How often should I run it?**
Daily is typical for price monitoring; hourly works for restock alerts. Use **Schedules** in Apify Console with **Track price and stock changes** and **Output only changed rows** on.

**Can I get stock quantities?**
No. Shopify's public feed only says whether each variant is available. The Actor reports `variantAvailable` true / false per variant (and `available` for the product) and tracks when it changes.

**Why are some prices `0` or some products odd?**
The Actor returns what the store publishes. Some stores list gift cards, samples or placeholder products; filter them by `productType`, `tags` or `price`.

**What does "newest first" mean for Max products per store?**
The store feed lists the most recently published products first, so a limit of 100 returns the 100 newest products. Collections keep the order set by the store.

**Do I need a proxy?**
It's on by default and included in the price: Apify residential proxy, because Shopify throttles requests from cloud and datacenter servers (HTTP 429 / 430). In tests from Apify's cloud, 5 of 5 stores worked with it, against 1 of 5 with datacenter IPs.

**What happens if one store fails?**
The other stores still run. The failure and its reason are in a free error row, in the `SUMMARY` record (`perStore[].error`, `errorKind`) and in the run status message.

**Is the saved snapshot readable by me?**
The tracking record in your account holds only the store, scope, Tracking ID, a row count and the save time in readable form; the snapshot itself is encrypted and only used by the Actor.

### Changes in version 1.1 (for users of 1.0)

- **One row per product** instead of one row per variant: the variants are in `variants`, and the price is $1 per 1,000 products instead of per 1,000 variants, so a full export costs less. Fields that were per variant (`variantId`, `variantTitle`, `sku`, `options`, the variant's price and stock) are now inside `variants`; `updatedAt` was removed. Use `unwind=variants` (see the FAQ) if your integration needs one row per variant.
- **"Output only changed rows" monitors** now cost $0.0001 per product checked plus $0.001 per changed product returned, and report changes per product with the changed variants listed in `changes`.
- `outputLevel` is no longer used (old inputs, tasks and schedules still run). Existing monitors continue with the same Tracking ID.
- Store names and bare words ("allbirds") are no longer guessed as `allbirds.myshopify.com`; enter the domain.
- Stores that can't be read now appear as free error rows, and a run whose stores all fail because of the input finishes successfully.

### Legal

This Actor only reads product information that stores publish openly to every visitor, through the public product feed of their own website. It does not log in, does not solve CAPTCHAs and collects no personal data. When Shopify's bot protection answers a request with HTTP 430, the request is retried at most once from another proxy IP address; if it comes again, the store is reported as blocked and nothing is charged for it. You are responsible for how you use the data and for complying with the terms of the websites you read and the laws that apply to you. This Actor is not affiliated with, endorsed or sponsored by Shopify Inc. or the stores it reads; "Shopify" is used only to describe where the data comes from.

# Actor input Schema

## `stores` (type: `array`):

One store per line (or separated by commas). Accepted forms:

- Domain: allbirds.com or www.allbirds.com
- Store URL: https://www.allbirds.com/
- Collection URL (reads only that collection): https://www.allbirds.com/collections/sale
- Shopify address: allbirds.myshopify.com
  Enter the domain, not the store's name: an entry with spaces ("Death Wish Coffee") or a bare word ("allbirds") is reported as invalid. Entries that are not Shopify stores, unknown domains and stores with a disabled feed are listed as free error rows; the other stores still run.

## `collections` (type: `array`):

Read only these collections of every store, e.g. "sale" or "new-arrivals" (the part after /collections/ in a collection URL). Leave empty to read the whole store. A collection given in a store URL takes precedence for that store.

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

Maximum number of products read per store, newest first (collections keep their own order). With "Output only changed rows" this is the number of products checked per run. Removed products are only detected when the whole store is read. If a monitor has only ever run with a limit, a product it doesn't know is reported as new only if it was published after the previous run; so raising or removing the limit later doesn't report the whole catalog as new, but older products newly added to a collection aren't reported either. To monitor additions to a collection, run the monitor once without a limit. 0 = no limit.

## `outputLevel` (type: `string`):

Kept so inputs, tasks and schedules from builds 1.0.x still run. Every row is now one product with its variants nested in "variants"; this field is ignored.

## `includeDescription` (type: `boolean`):

Add the product description converted from HTML to plain text. Makes rows larger; no extra requests.

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

Remember every store's products in a named key-value store in your account (stored encrypted), so each later run marks products as new, price_down, price_up, back_in_stock, out_of_stock, variant_added, variant_removed or unchanged (`changes` names the variants that changed), and also returns products removed since the previous run (once). Schedule the Actor (e.g. daily) to monitor stores.

## `onlyChanges` (type: `boolean`):

Return only products that changed since the previous run (new, removed, price or stock changes of any variant). Switches on change tracking. Each product checked costs $0.0001 (product-checked, once per product and run), and each changed product returned costs $0.001. The first run of a store saves a baseline and returns nothing; changes appear from the second run. If a run stops before the whole store is read (for example at your maximum charge), the next run completes the baseline without reporting those products as new.

## `trackingId` (type: `string`):

Name of the tracking state. Runs with the same ID compare against each other; use different IDs for independent monitors. Letters, numbers, dashes and underscores.

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

How many stores are read at the same time. Each store gets at most one request at a time and at most 2 requests per second.

## `requestTimeoutSecs` (type: `integer`):

Timeout for a single HTTP request.

## `maxRetries` (type: `integer`):

Retries for network errors, HTTP 5xx and Shopify's rate limit (HTTP 429), with exponential backoff that honours Retry-After. Shopify's bot protection (HTTP 430) is retried at most once, from another proxy IP address; if it comes again, the store is reported as blocked and not charged.

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

On by default (Apify residential proxy, included in the price): Shopify throttles requests from cloud and datacenter IPs heavily (HTTP 429/430). Each store gets its own proxy session. Turning it off is only advised for local runs.

## Actor input object example

```json
{
  "stores": [
    "allbirds.com",
    "https://www.deathwishcoffee.com/collections/coffee",
    "colourpop.com"
  ],
  "collections": [
    "sale",
    "new-arrivals"
  ],
  "maxProductsPerStore": 10,
  "includeDescription": false,
  "trackChanges": false,
  "onlyChanges": false,
  "trackingId": "default",
  "maxConcurrency": 5,
  "requestTimeoutSecs": 30,
  "maxRetries": 4,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `products` (type: `string`):

All rows from this run as JSON: price range, compare-at price, stock, variants, links, and changeType when tracking is on.

## `changes` (type: `string`):

The same rows in the "Changes" view: change type, previous price, price change %, stock, changed variants.

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

Per-store counts (products read and checked, rows pushed, new, removed, price and stock changes) and any errors.

# 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 = {
    "stores": [
        "colourpop.com",
        "deathwishcoffee.com"
    ],
    "maxProductsPerStore": 10,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("fairfetch/shopify-price-stock-tracker").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 = {
    "stores": [
        "colourpop.com",
        "deathwishcoffee.com",
    ],
    "maxProductsPerStore": 10,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("fairfetch/shopify-price-stock-tracker").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 '{
  "stores": [
    "colourpop.com",
    "deathwishcoffee.com"
  ],
  "maxProductsPerStore": 10,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call fairfetch/shopify-price-stock-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fairfetch/shopify-price-stock-tracker"
        }
    }
}
```

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/3iIbxq0lTyg3hiSVu/builds/kDf450hYFrhAqgCck/openapi.json
