# Shopify Product Scraper & Competitor Price Monitor (`adastra-connect/store-product-feed-monitor`) Actor

Scrape any Shopify or WooCommerce store's products, variants, prices, discounts and stock. Monitor competitor price drops, restocks and new products on a schedule. No login or proxy.

- **URL**: https://apify.com/adastra-connect/store-product-feed-monitor.md
- **Developed by:** [Ad Astra Connect](https://apify.com/adastra-connect) (community)
- **Categories:** E-commerce, Marketing, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Product Scraper and Competitor Price Monitor (WooCommerce too)

**Track competitor prices, discounts and stock on any public Shopify or WooCommerce store, and export the full product catalog with every variant.** Enter store URLs, get one row per product (or per variant) with price, compare-at price, discount, availability, vendor, product type and images. Switch to Changes mode and schedule it: each run returns only what changed (price drops, price increases, back in stock, out of stock, new and removed products). It reads the stores' public product feeds (Shopify `/products.json`, WooCommerce Store API), so there is no login, no proxy and no browser.

Typical buyers: ecommerce brands watching competitors, pricing analysts, dropshippers and product researchers, and agencies running catalog audits.

### Sample output: Shopify product prices and discounts

Real rows from a run on October 21, 2026 (store `allbirds.com`), shortened:

| platform | title | vendor | price | compareAtPrice | discountPct | onSale | available | variantCount |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| shopify | Women's Allbirds Flip Flop - Dusty Pink | Allbirds | 25 | 50 | 50 | true | true | 7 |
| shopify | Men's Canvas Runner NZ - Deep Navy Stripes (Blizzard Sole) | Allbirds | 100 | | | false | true | 13 |
| shopify | Men's Cruiser Terry - Warm White (Warm White Sole) | Allbirds | 115 | | | false | true | 13 |

Each row also has `storeUrl`, `productId`, `handle`, `url`, `imageUrl`, `currency`, `priceMax`, `tags`, `variants`, `createdAt`, `updatedAt` and `publishedAt`.

### Why use this Shopify and WooCommerce scraper?

- **Two platforms, one schema.** Shopify and WooCommerce rows have the same fields, so you can compare stores side by side.
- **Auto-detects the platform.** Just paste the store URL.
- **Price and stock monitoring built in.** A compact baseline is kept in Apify storage; later runs return only what changed. The first baseline run is free.
- **Products or variants.** One row per product with a nested variants array, or one row per variant (size, colour, SKU).
- **Filters before billing.** Keyword, price range and availability filters run before output, so you pay only for rows you keep.
- **Fast and light.** Reads public JSON feeds directly. A typical store of a few hundred products finishes in seconds.

### Use cases

- **Competitor price monitoring** for ecommerce brands and retailers.
- **Back-in-stock and sale alerts** via Apify webhooks, Make, Zapier or n8n.
- **Product research and catalog analysis:** assortment size, price bands, discount depth, vendors.
- **Catalog exports** to CSV or Excel for merchandising, marketplaces or affiliate sites.
- **Market and trend tracking** across a list of DTC brands.

### What data can you get

| Fields | What they contain |
| --- | --- |
| `platform`, `storeUrl` | Detected platform and input store origin |
| `productId`, `handle`, `title` | Product identity and name |
| `vendor`, `productType`, `categories`, `tags` | Shopify vendor/type or WooCommerce brand/categories, and catalog tags |
| `url`, `imageUrl`, `imageCount` | Product page, first image, number of images |
| `currency`, `price`, `priceMax` | Currency code and lowest/highest variant prices |
| `compareAtPrice`, `onSale`, `discountPct` | Lowest sale reference price, any-variant sale flag, lowest-priced variant's percentage discount |
| `available`, `variantCount` | Any-variant availability and variant count |
| `variants` | Product-mode array of variant IDs, titles, SKUs, prices, reference prices, stock and up to three options |
| `variantId`, `variantTitle`, `sku` | Single variant identity in variants mode |
| `variantPrice`, `variantCompareAtPrice`, `variantAvailable` | Single variant price, reference price and availability |
| `createdAt`, `updatedAt`, `publishedAt`, `scrapedAt` | Feed timestamps when exposed, plus collection time |
| `descriptionText` | Optional plain text description, with contact details removed |
| `changeType`, `detectedAt` | Monitoring event and observation timestamp |
| `previousPrice`, `currentPrice`, `priceChangePct` | Variant prices before/after and signed percentage change |
| `previousAvailable`, `currentAvailable` | Variant stock status before/after |

The dataset has **Overview** and **Changes** table views. Export it as JSON, CSV, Excel, or another supported Apify format. IDs are strings. Missing values are `null`; descriptions are omitted unless requested. Feed fields such as customer details, reviews, ratings, and extension payloads are never included in the output.

### How to scrape Shopify and WooCommerce products

1. Open the Actor input and enter public store URLs. Bare domains work too. The prefilled examples are two stores whose public feeds were verified during development.
2. Choose **Products**, **Variants**, or **Changes**. Leave platform on **Auto detect** unless you know it.
3. Set product and output limits, plus any availability, price, or keyword filters.
4. Run the Actor. Open the dataset for results and the `RUN_SUMMARY` key-value record for store statuses, detected platforms, counts and errors.
5. For monitoring, schedule repeated Changes runs with the same `monitorKey`, collection, and product limit.

```json
{
  "startUrls": [
    "https://www.allbirds.com",
    "https://offermanwoodshop.com"
  ],
  "platform": "auto",
  "mode": "products",
  "maxProductsPerStore": 30,
  "maxResults": 60,
  "includeDescription": false,
  "onlyAvailable": false,
  "requestDelayMs": 1000
}
```

A Shopify collection URL such as `https://shop.example.com/collections/shoes` reads that collection's product feed. Other store paths select the origin's full product feed. WooCommerce installations in a WordPress subdirectory are currently unsupported.

#### Input reference

| Input | Default | Behavior |
| --- | --- | --- |
| `startUrls` | Allbirds and Offerman Woodshop | Required in the editor; empty raw input uses these defaults |
| `platform` | `auto` | `auto`, `shopify`, `woocommerce` |
| `mode` | `products` | `products`, `variants`, `changes` |
| `monitorKey` | `default` | Namespace for independent monitoring baselines |
| `emitBaselineOnFirstRun` | `false` | Emit `new_product` per matching variant on a first Changes run |
| `minPriceChangePct` | `0` | Absolute percentage threshold, inclusive; stock events unaffected |
| `maxProductsPerStore` | `500` | Parent products read before filtering; `0` means unlimited |
| `maxResults` | `2000` | Hard cap on dataset rows across all stores; minimum 1 |
| `includeDescription` | `false` | Add plain text product descriptions |
| `onlyAvailable` | `false` | Require any variant in stock at product level |
| `minPrice`, `maxPrice` | Unset | Inclusive bounds on the lowest variant price, in store currency |
| `keywords` | `[]` | Any keyword may match title, vendor, type or tags, ignoring case |
| `requestDelayMs` | `1000` | Minimum per-host interval; larger Crawl-delay wins |

#### Output example

Illustrative product row, with the variant array shortened:

```json
{
  "platform": "shopify",
  "storeUrl": "https://shop.example.com",
  "productId": "101",
  "handle": "wool-runner",
  "title": "Wool Runner",
  "vendor": "Sample Footwear",
  "productType": "Shoes",
  "categories": [],
  "tags": ["wool", "running"],
  "url": "https://shop.example.com/products/wool-runner",
  "imageUrl": "https://cdn.example.com/shoe.jpg",
  "imageCount": 1,
  "currency": "USD",
  "price": 80,
  "priceMax": 90,
  "compareAtPrice": 100,
  "onSale": true,
  "discountPct": 20,
  "available": true,
  "variantCount": 2,
  "variants": [
    {
      "variantId": "201",
      "title": "Blue / 8",
      "sku": "WR-B-8",
      "price": 80,
      "compareAtPrice": 100,
      "available": true,
      "option1": "Blue",
      "option2": "8",
      "option3": null
    }
  ],
  "createdAt": "2026-01-01T00:00:00Z",
  "updatedAt": "2026-02-01T00:00:00Z",
  "publishedAt": "2026-01-01T00:00:00Z",
  "scrapedAt": "2026-10-20T12:00:00Z"
}
```

### How much does it cost to scrape Shopify and WooCommerce products?

This Actor uses **pay per event** pricing: **$1.00 per 1,000 results** (Apify Store discount tiers pay less). The price on the Store page is authoritative. Platform compute is included, so you pay for the rows you receive plus Apify's small per-run start fee, nothing else.

| Example run | Rows returned | Approximate cost |
| --- | --- | --- |
| Products mode, 3 stores, 100 products each | 300 | about $0.30 |
| Variants mode, one store with 1,000 variants | 1,000 | about $1.00 |
| Daily Changes mode on 5 stores, about 20 changes per day | about 600 per month | about $0.60 per month |

What you are charged for: each dataset row is one `product` event (a product row, a variant row or a change record). A variant with both a price and a stock change produces two rows. The silent first baseline in Changes mode emits no rows and costs nothing beyond the start fee. Use `maxResults` to cap a run, and set a maximum cost per run in Apify; the Actor stops cleanly when that limit is reached.

### Incremental runs / monitoring

Set `mode` to `changes`. On the first successful run per store, the Actor saves a baseline and logs that it saved it. The default is zero output rows. Set `emitBaselineOnFirstRun` to `true` to emit `new_product` records for that baseline.

Snapshots live in the **named** key-value store `store-product-feed-monitor-state`. A hash of the input store host and `monitorKey` selects the record. Keep the same host spelling, including `www`, across runs. The store retains only compact per-variant identity, prices, reference price, stock, title, URL, currency and filter eligibility. Large snapshots are split into chunks below 8 MB, with the manifest written last. Descriptions and images are not saved in monitoring state.

Later runs can emit `new_product`, `removed_product`, `price_drop`, `price_increase`, `back_in_stock`, and `out_of_stock`. These names apply per variant, so a newly added size is a `new_product` event. `priceChangePct` is signed, rounded to two decimals, and null when there is no meaningful previous price or the previous price is zero. The minimum percentage threshold uses the unrounded absolute change. Compare-at price is retained in the baseline, but a compare-at-only edit has no dedicated event type.

A capped feed scan cannot prove that an unseen variant was removed. It preserves unseen baseline entries and suppresses removal events. To monitor removals across the entire store, use `maxProductsPerStore: 0`. Changing the collection, product limit, origin, or detected platform starts a fresh baseline. Use separate `monitorKey` runs for multiple collections on the same host. Filters apply to product-level matches; an event is included if the previous or current product matched, so an out-of-stock transition can still be reported with `onlyAvailable: true`.

Failed or malformed feed scans leave the previous snapshot intact and emit no monitoring events. If a row or spending limit interrupts change delivery, the snapshot is not advanced; already delivered events can repeat next run. Increase limits and deduplicate by variant, event and values if needed. Avoid overlapping schedules that write the same host/monitorKey pair; the KV store has no cross-run lock. Public feeds can change while paging, so no run represents a transactional catalog export.

### Scheduling and integrations

Use Apify schedules to check feeds hourly, daily, or at another suitable interval. Trigger webhooks after runs, download datasets through the Apify API, or connect results to Make, Zapier, and n8n. AI agents can start runs and consume datasets through Apify's MCP integration. The Actor itself sends no alerts or messages; configure your chosen integration to route change records.

### Is it legal to scrape Shopify and WooCommerce stores?

The Actor reads only the official public product endpoints exposed to visitors by Shopify and WooCommerce. It does not log in, send cookies, fetch customer data or reviews, or access cart, checkout, or account endpoints. It identifies its User-Agent, respects robots.txt, and honors Crawl-delay. Output selects catalog fields only and removes email addresses and recognizable phone numbers from text. A product catalog can contain names as part of product or brand names; the Actor does not retrieve personal profiles or identify individuals.

Users are responsible for checking applicable law, store terms, and their rights to use and redistribute catalog information, images, and descriptions. Public accessibility does not grant unrestricted reuse rights.

### Limitations

- Some stores disable feeds, block automated access, or require authentication. These are recorded as unsupported, blocked, or failed; there is no bypass.
- Shopify uses `/products.json?limit=250&page=N`, or the equivalent collection route, and optionally `/meta.json` for currency. Currency stays null if that metadata is unavailable or disallowed. No cart-based currency lookup is used.
- WooCommerce uses `/wp-json/wc/store/v1/products` and falls back to `/?rest_route=/wc/store/v1/products` only on HTTP 404. Real variant details are fetched in batches through the same public Store API. If advertised variants cannot be retrieved, that target fails rather than inventing variant values.
- Feed timestamps, Shopify categories, WooCommerce brands, and stock detail depend on what the platform exposes. Missing timestamps are null. Shopify availability is the feed's boolean; stock quantities are not collected.
- Prices use the store's currency. There is no conversion, shipping calculation, tax adjustment, customer-specific pricing, or regional session selection. WooCommerce minor units are converted using `currency_minor_unit`.
- `compareAtPrice` is the lowest reference price among sale variants. `discountPct` is calculated from the lowest-priced variant's own reference price and is null if that variant is not on sale. A different variant can still make `onSale` true.
- Products mode retains all variants of a matching product. Variants mode emits all those variants, including unavailable ones if another variant makes the product available.
- One request at a time keeps memory and load small. Retries cover 429, 5xx, and network failures with exponential backoff, jitter, Retry-After and a 20-second timeout per attempt, up to four retries. Robots HTTP 404 allows proceeding; inaccessible or invalid robots responses fail closed.
- A store status is `ok`, `not_found`, `blocked_by_robots`, `unsupported`, or `error`. Duplicate URLs are marked `duplicate`; targets after an output limit are `not_processed`. Only runs with no successful targets fail overall. Partial catalog output from a later failing page may remain in the dataset.

### FAQ

**Why did my first Changes run return no rows?** It saved a baseline. Run it again with the same host, monitorKey and scope to compare, or enable baseline output.

**Why are no removal alerts reported?** The product cap may have stopped the scan. Use unlimited product scans for reliable removal detection.

**Can I monitor a Shopify collection?** Yes. Supply its `/collections/{handle}` URL. Use an independent monitorKey for a different collection on the same host.

**Are WooCommerce REST API keys required?** No. This Actor uses the public Store API, not the authenticated administrative REST API.

**How often should I run Changes mode?** Daily suits most price monitoring. Hourly works for fast-moving sales; keep the same `monitorKey` and avoid overlapping runs.

**Can I get alerts by email or Slack?** The Actor writes change rows to the dataset. Use an Apify webhook or the Make, Zapier or n8n integration to route them to email, Slack or a spreadsheet.

**Does it work with every store?** No. Only reachable public feeds with allowed robots rules are supported. Custom headless stores, site protections, older Store APIs and subdirectory WordPress installations can prevent collection.

**Why can a row show no currency or timestamp?** The store's public endpoint did not supply it, or the optional metadata route was unavailable.

### Support

Use the **Issues** tab on the Actor's Apify Store page. Include the run link, input settings, affected store URL and expected behavior. Do not include credentials or customer information.

### Disclaimer

Shopify and WooCommerce are trademarks of their respective owners. This Actor is an independent tool and is not affiliated with, sponsored by or endorsed by Shopify Inc., Automattic or any store you monitor. Nothing in this README is legal advice.

# Actor input Schema

## `startUrls` (type: `array`):

Public Shopify or WooCommerce store URLs. Bare domains and Shopify collection URLs are accepted.

## `platform` (type: `string`):

Auto checks Shopify first, then the WooCommerce public Store API.

## `mode` (type: `string`):

Products: one row per product. Variants: one row per variant. Changes: compare against a persistent baseline.

## `monitorKey` (type: `string`):

Use the same value across scheduled monitoring runs. Use separate namespaces for different collections or purposes.

## `emitBaselineOnFirstRun` (type: `boolean`):

When enabled, emit a new_product record for every matching variant on the first changes run.

## `minPriceChangePct` (type: `number`):

Ignore price changes smaller than this absolute percentage. Stock changes are unaffected.

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

Maximum parent products to read before filters, 0 means unlimited. Limited scans suppress removal alerts.

## `maxResults` (type: `integer`):

Hard cap on billable product, variant, or change rows across all stores.

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

Include plain text descriptions with HTML and contact details removed.

## `onlyAvailable` (type: `boolean`):

Match products with at least one available variant. In changes mode either previous or current match can emit a stock change.

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

Inclusive minimum for the lowest variant price, in the store currency. No currency conversion.

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

Inclusive maximum for the lowest variant price, in the store currency.

## `keywords` (type: `array`):

Case-insensitive substring matching in title, vendor, product type and tags. Any keyword may match.

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

Minimum interval between requests to a host. A larger robots.txt Crawl-delay takes precedence.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.allbirds.com",
    "https://offermanwoodshop.com"
  ],
  "platform": "auto",
  "mode": "products",
  "monitorKey": "default",
  "emitBaselineOnFirstRun": false,
  "minPriceChangePct": 0,
  "maxProductsPerStore": 50,
  "maxResults": 2000,
  "includeDescription": false,
  "onlyAvailable": false,
  "keywords": [],
  "requestDelayMs": 1000
}
```

# Actor output Schema

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

All rows in the default dataset.

## `overview` (type: `string`):

Dataset table view "overview".

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

Dataset table view "changes".

## `runSummary` (type: `string`):

Per-target status, counts and errors (RUN_SUMMARY record).

# 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 = {
    "startUrls": [
        "https://www.allbirds.com",
        "https://offermanwoodshop.com"
    ],
    "maxProductsPerStore": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("adastra-connect/store-product-feed-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "startUrls": [
        "https://www.allbirds.com",
        "https://offermanwoodshop.com",
    ],
    "maxProductsPerStore": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("adastra-connect/store-product-feed-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    "https://www.allbirds.com",
    "https://offermanwoodshop.com"
  ],
  "maxProductsPerStore": 50
}' |
apify call adastra-connect/store-product-feed-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,adastra-connect/store-product-feed-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/8K9xWFeL1oQJCmbNT/builds/dbMJ9oUdAdrWtc7s0/openapi.json
