# WooCommerce Products Scraper & Price Tracker (`jtpalms/woocommerce-store-products`) Actor

Scrape every product from any WooCommerce store through its public Store API: prices, sale prices, stock, SKUs, categories, tags, attributes, ratings and images. Schedule it to track price drops, price increases, restocks, sell-outs, new and removed products. Pay only per product.

- **URL**: https://apify.com/jtpalms/woocommerce-store-products.md
- **Developed by:** [JT Palms](https://apify.com/jtpalms) (community)
- **Categories:** E-commerce, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 product scrapeds

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

## WooCommerce Products Scraper & Price Tracker: scrape any WooCommerce store, then track its price changes

Give it a list of WooCommerce stores and get every product back: title, type, SKU, price, regular and sale price, discount, price range, stock status, low-stock count, categories, tags, brands, attributes, variation IDs, rating, review count, images and description. Turn on **change tracking**, schedule it daily, and each run tells you which products dropped in price, went up, came back in stock, sold out, are new, or were removed.

**USD 1 per 1,000 products.** Stores that are not WooCommerce, unreachable or blocked are reported free. Removed-product alerts are free.

### What people use it for

- **Competitor price monitoring.** Schedule a daily run over your competitors' WooCommerce stores with **Only output changes** on. You get a short list of price drops, price increases, restocks, sell-outs and new launches, and pay only for those rows.
- **Catalog research.** See a store's full range, price points, discount depth (regular vs sale price), categories, best-rated products and stock gaps before you pick a niche, a supplier or a wholesale partner.
- **Product feeds.** Pull a clean product feed (SKU, price, stock, categories, images, permalink) from your own store or a partner's for Google Sheets, a comparison site, a marketplace listing or an affiliate catalog.
- **Sale and stock watching.** Use the **On sale only** or **Stock status** filters to track just a store's discounted or out-of-stock products, and pay only for those.

### Change tracking (the main reason to schedule it)

With **Track changes between runs** on, every row gets a `change` field that compares it with the previous run:

| `change` | Meaning |
|---|---|
| `new` | Not seen in the previous run (a new launch, or the first run for this store) |
| `price_drop` | Lowest price went down (or, if equal, the highest price went down) |
| `price_increase` | Lowest price went up (or, if equal, the highest price went up) |
| `back_in_stock` | Was out of stock, now in stock |
| `sold_out` | Was in stock, now out of stock |
| `unchanged` | Same price range and same stock status |
| `removed` | Was in the previous run, no longer listed (deleted, unpublished, hidden, or no longer matching your filters). Free. |

`previousPriceMin` and `previousPriceMax` hold the earlier prices, and `changes` lists every label that applies when more than one does (for example `["price_drop", "back_in_stock"]`). A product that got cheaper looks like this (other fields left out):

```json
{ "title": "Insulated Flask 16oz", "priceMin": 34.95, "previousPriceMin": 44.95, "currency": "AUD", "change": "price_drop", "changes": ["price_drop"] }
```

**Only output changes** skips the `unchanged` rows, so you pay only for products that actually changed.

**Schedule tip.** Save your store list as a task with **Track changes** and **Only output changes** on, then add a schedule (for example daily at 06:00). The first run sets the baseline and marks every product `new`; from the second run on you get only real changes. Add an integration (email, Slack, Google Sheets, Make, Zapier or a webhook) on "run succeeded" to receive each day's changes automatically.

How it works: after each run the actor keeps a small snapshot per store (product ID, title, URL, price range, currency, in stock) in a key-value store named `woocommerce-price-tracker` in your account. Each store gets one record, and each store plus filter combination (category, tag, search, on sale, stock status) gets its own. Named stores do not expire, so the history survives between runs. Good to know:

- Snapshots are shared by every run in your account that tracks the same store and filters. An extra manual run with tracking on moves the baseline forward.
- `removed` is only reported when the whole catalog (or the whole filtered list) was read. Runs limited by **Max products per store**, stopped by your cost limit, or cut short by an error do not report removals, and they keep the older snapshot entries for products they did not reach.
- Some stores convert prices to the visitor's currency based on location. If a product comes back in a different currency than last run, its price is not compared (so you never get a fake price drop), and the run log explains why. Pick a proxy country under **Advanced** to keep one currency.
- To start over for a store, delete its record from the `woocommerce-price-tracker` store under **Storage**.

### Sample output

One row per product (from a real run on nalgene.com with tracking on; arrays and text shortened):

```json
{
  "store": "nalgene.com",
  "productId": 959,
  "title": "24oz Neoprene Sleeve",
  "slug": "24oz-sleeve-gray",
  "type": "variable",
  "sku": "Z0119",
  "url": "https://nalgene.com/product/24oz-sleeve-gray/",
  "price": 5,
  "regularPrice": 13.99,
  "salePrice": 5,
  "priceMin": 5,
  "priceMax": 5,
  "currency": "USD",
  "onSale": true,
  "discountPercent": 64,
  "inStock": true,
  "purchasable": true,
  "onBackorder": false,
  "lowStockRemaining": null,
  "stockText": "In stock",
  "categories": ["Accessories", "Sleeves", "Today's Sales"],
  "categorySlugs": ["accessories", "sleeves", "sales"],
  "tags": ["Sale"],
  "brands": [],
  "attributes": [
    { "name": "Bottle Color", "options": ["Cerulean", "Electric Magenta", "Gray"], "usedForVariations": true },
    { "name": "Weight", "options": ["4.8 oz (136g)"], "usedForVariations": false }
  ],
  "variationCount": 2,
  "variations": [
    { "id": 653154, "attributes": { "Bottle Color": "Cerulean" } },
    { "id": 653147, "attributes": { "Bottle Color": "Electric Magenta" } }
  ],
  "averageRating": 4,
  "reviewCount": 1,
  "images": ["https://nalgene.com/wp-content/uploads/2022/10/24oz-neoprene-RED_Front.jpg"],
  "shortDescription": "Keep your 24oz OTF bottle dry with a neoprene sleeve.",
  "description": "... Fits snugly around the 24oz OTF bottle , keeps your refreshment at a refreshing temperature longer. This product is imported.",
  "scrapedAt": "2026-09-28T18:42:52.933Z",
  "change": "new",
  "changes": ["new"],
  "previousPriceMin": null,
  "previousPriceMax": null,
  "previousCurrency": null
}
```

Field notes:

- Prices are normal numbers in the store's currency (the Store API sends them in cents or the currency's smallest unit; the actor converts them, including zero-decimal and three-decimal currencies).
- `price` is the current price. For a product with several prices (a variable product), `priceMin` and `priceMax` are the range the storefront shows and `price` is its lowest ("from") price. `regularPrice` is the price before any sale, `salePrice` is set only while the product is on sale, and `discountPercent` compares the two.
- `lowStockRemaining` is filled only when the store shows a low-stock count such as "Only 3 left in stock".
- `variations` lists each variation's ID and options. The Store API product list does not include per-variation prices or stock, so the price range covers them.

A store that could not be read comes back as one free row with an `error`, for example `"Not a WooCommerce store: no Store API found (HTTP 404, not JSON)."`, `"Not a WooCommerce store, or its Store API is turned off: the site runs WordPress but has no /wc/store route (rest_no_route). ..."`, `"Store blocked the request with a bot check (HTTP 403). Try again with a proxy under Advanced."` or `"Domain not found (DNS lookup failed). Check the spelling."`

Export as JSON, CSV or Excel, or read the results through the Apify API. The dataset has three table views: Products, Changes, and Stock and sales.

### How to use it

1. Put your stores in **WooCommerce stores**, one per line. Bare domains (`lunchconcept.com`) and full URLs both work. A category page URL such as `https://store.com/product-category/sale/` reads only that category, and a tag page URL such as `https://store.com/product-tag/summer/` only that tag.
2. Optional: set **Max products per store** for a quick sample, or use the filters (**Category**, **Tag**, **Search text**, **On sale only**, **Stock status**). Filters run on the store's side, so you pay only for matching products. Category and tag take the slug from the store's URL or the numeric ID; the `categorySlugs` field in the results shows the slugs.
3. Optional: turn on **Track changes between runs** and **Only output changes**, then schedule the actor (see the tip above).
4. Click **Start**, then download the results or connect them to your tools.

### Pricing

| What | Price |
|---|---|
| Product row | USD 0.001 (USD 1 per 1,000) |
| Unchanged product skipped by **Only output changes** | Free |
| `removed` row | Free |
| Store that is not WooCommerce, unreachable, blocked or has the Store API turned off | Free |

Examples: a full read of a 2,000-product store costs USD 2. Tracking 5 stores of 2,000 products each with **Only output changes** costs USD 10 for the first (baseline) run; after that you pay per changed product, so a day on which 200 products change costs USD 0.20.

Set a maximum cost per run in the run options and the actor stops cleanly when it reaches it; everything saved before that is kept.

### Limits

- It reads the public WooCommerce Store API (`/wp-json/wc/store/v1/products`), which ships with WooCommerce itself (version 5 and later) and powers the store's own block-based product grids, cart and checkout. It also tries the older `/wp-json/wc/store/products` route and the `?rest_route=` form for sites without pretty URLs.
- The Store API must be reachable. Some stores turn it off, close the WordPress REST API to visitors, or put it behind a bot check (such as a Cloudflare challenge). Those are reported free with a clear error. The actor also tries the `www.` or bare host before giving up; a proxy under **Advanced** often gets past a data-center block.
- Only published products appear. Drafts and private products do not.
- Stock is in stock, out of stock or on backorder. Exact inventory counts are not public; `lowStockRemaining` appears only when the store itself shows a low-stock count.
- Prices are what a visitor sees: they may include tax depending on the store's settings, and stores with a currency switcher may convert them to the visitor's currency based on location (see the note under change tracking).
- Category and tag page URLs are recognized with WooCommerce's default `product-category` and `product-tag` bases. If a store renamed them, put the slug in the **Category** or **Tag** field instead.
- Descriptions are plain text capped at 2,000 characters (short descriptions at 500), and up to 10 images are listed per product.
- Pages of 100 products are read one at a time per store with a short pause between them, and rate limits (HTTP 429) and server errors are retried up to 3 times. A 500-product store takes about 10 to 20 seconds; very large catalogs (10,000+ products) take a few minutes each.

### FAQ

**Is it legal?** It reads the Store API, a public endpoint that every WooCommerce store serves to its own storefront so shoppers' browsers can load products, the same product data shown on the store's pages. It does not log in, bypass protections, or collect personal data. As with any data, check how you use it against the store's terms and your local rules.

**Do I need a WooCommerce API key or store access?** No. The Store API needs no keys, apps or login. (It is not the admin REST API at `/wc/v3`, which does need keys.)

**How do I know which sites run WooCommerce?** Just add them. Sites that are not WooCommerce come back free with an error. To filter a big list first, run it through a tech stack detector.

**Why do I see a different price in my browser?** The store may show prices with or without tax depending on your location or login, or convert them to your local currency. This actor reports what an anonymous visitor from the proxy's location sees, with the currency on every row.

**Why are there no prices per variation?** The Store API's product list gives each variation's ID and options but not its own price or stock. The row's `priceMin` and `priceMax` cover the range across all variations, and `change` tracks that range.

**Can I get an alert when a competitor drops a price?** Yes. Schedule a task with **Only output changes** on and add an email, Slack or webhook integration. Each run then sends only the changed products.

**Something looks wrong for a store?** Open an issue with the store domain.

# Actor input Schema

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

Store domains or URLs, one per line. Examples: lunchconcept.com, https://www.nalgene.com. A category page URL (https://store.com/product-category/sale/) reads only that category, and a tag page URL (https://store.com/product-tag/summer/) only that tag. Duplicates are removed automatically.

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

Stop after this many products from each store. 0 means no limit.

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

Compare with the previous run and add a "change" field to each row: new, price\_drop, price\_increase, back\_in\_stock, sold\_out or unchanged. Products that disappeared are reported as removed (free). Snapshots are kept in your "woocommerce-price-tracker" key-value store. Schedule the actor daily to use this.

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

Skip unchanged products, so you pay only for new, removed, repriced and restocked or sold-out products. Turns on change tracking. On the first run every product counts as new.

## `category` (type: `string`):

Read only one category from every store: its slug (the part after /product-category/ in the URL, for example "sale") or its numeric ID. Separate several with commas to get products from any of them. Leave empty to read the whole catalog.

## `tag` (type: `string`):

Read only products with this product tag: its slug (the part after /product-tag/ in the URL) or its numeric ID. Separate several with commas.

## `search` (type: `string`):

Only products that match this text, the same as the store's own search box.

## `onSaleOnly` (type: `boolean`):

Only products that have a sale price right now.

## `stockStatus` (type: `string`):

Only products with this stock status.

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

How many stores to read at the same time. Pages within one store are always read one by one, with a short pause, to be polite.

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

Optional. Only needed if a store blocks requests from data centers (HTTP 403 or 429 errors, or a bot check), or to fix the country for stores that convert prices to the visitor's currency.

## Actor input object example

```json
{
  "stores": [
    "lunchconcept.com"
  ],
  "maxProductsPerStore": 100,
  "trackChanges": false,
  "onlyChanges": false,
  "onSaleOnly": false,
  "stockStatus": "any",
  "maxConcurrency": 5
}
```

# Actor output Schema

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

All output rows in the default dataset (JSON, CSV, Excel via the format parameter).

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

Counts and per-input status for this run.

# 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": [
        "lunchconcept.com"
    ],
    "maxProductsPerStore": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("jtpalms/woocommerce-store-products").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": ["lunchconcept.com"],
    "maxProductsPerStore": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("jtpalms/woocommerce-store-products").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": [
    "lunchconcept.com"
  ],
  "maxProductsPerStore": 100
}' |
apify call jtpalms/woocommerce-store-products --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jtpalms/woocommerce-store-products"
        }
    }
}
```

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/vQztkrvnYhFao75Aq/builds/FR3OOHO8zDGjNdbMe/openapi.json
