# E-commerce Product Scraper & Price Monitor - Any Store (`neverempty/ecommerce-product-price-monitor`) Actor

Paste product page URLs from any online store and get price, currency, stock, sale price, rating, SKU/GTIN and brand, read from the structured data in the page, with the source of every value. Monitor mode returns only products whose price or stock changed. Unreadable pages are free.

- **URL**: https://apify.com/neverempty/ecommerce-product-price-monitor.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 product returneds

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

## E-commerce Product Scraper & Price Monitor

Paste product page URLs from **any online store** and get one row per product: name, brand, SKU / GTIN / MPN, **price, currency, stock status**, original (pre-sale) price, rating and review count, one image URL, seller, and **where each value was read from**. Turn on monitor mode and later runs return **only the products whose price, stock or review count changed**, with the previous values next to the new ones.

It reads the structured data that stores put in their HTML for search engines: JSON-LD `Product` / `Offer` / `AggregateOffer`, microdata, RDFa, OpenGraph `product:price` tags, and on Shopify stores the product file `/products/<handle>.js`. It does not guess prices from visible text.

### Why this one

- **Only what changed.** A daily price watch over 500 products usually has a handful of changes. Monitor mode returns those rows only; an unchanged product costs one small `product-checked` event instead of a full product row.
- **You can see where every number came from.** `sources` says, per column, `json-ld`, `microdata`, `rdfa`, `opengraph` or `shopify-js`. When two places in the page disagree on the price, `priceCandidates` lists both.
- **No wrong price is better than a wrong price.** If the variants have different prices and the URL does not say which one is selected, `price` is `null` and `priceMin` / `priceMax` give the range. If the page describes several products and none matches the page URL, none is chosen. Each case comes with a reason in `warnings` and `note`.
- **Pages without a readable price are free.** Blocked pages, pages that robots.txt does not allow, pages with no product data and products with no readable price come back as free rows that say why.

### Input

```json
{
  "productUrls": [
    "https://www.sonos.com/en-us/shop/era-100",
    "https://www.deathwishcoffee.com/products/white-chocolate-pistachio-coffee?variant=42446944174135"
  ],
  "onlyChanges": false
}
```

| Field | What it does |
|---|---|
| `productUrls` | Product pages, up to 1,000 per run. For a product with variants, paste the URL of the variant you care about. |
| `onlyChanges` | Monitor mode. First run: records the starting point, free, no product rows. Later runs: only changed products. |
| `watchName` | Optional. A separate memory per name (for example one per schedule). |
| `resetMonitoringState` | Forget what this watch remembers and start again. |
| `useShopifyJs` | On Shopify stores also read `/products/<handle>.js` for per-variant price and stock (default on). |
| `maxConcurrency` | Pages read at the same time, 1 to 8 (default 4). |
| `requestTimeoutSecs` | Seconds to wait for one page, 5 to 60 (default 20). |

### Output

```json
{
  "status": "ok",
  "url": "https://www.example-store.com/products/trail-shoe?variant=4411",
  "name": "Trail Shoe",
  "brand": "Example",
  "sku": "TS-42-BLK",
  "gtin": "4006381333931",
  "price": 89.0,
  "currency": "USD",
  "priceType": "selected-variant",
  "priceMin": 89.0,
  "priceMax": 99.0,
  "originalPrice": 120.0,
  "availability": "InStock",
  "inStock": true,
  "variantCount": 6,
  "variantsInStock": 4,
  "ratingValue": 4.6,
  "reviewCount": 312,
  "imageUrl": "https://www.example-store.com/img/trail-shoe.jpg",
  "matchedBy": "url",
  "sources": { "name": "json-ld", "price": "shopify-js", "availability": "shopify-js", "currency": "json-ld", "reviewCount": "json-ld" },
  "changeType": "price-drop",
  "previousPrice": 99.0,
  "previousAvailability": "InStock",
  "previousCheckedAt": "2026-10-04T06:00:11.000Z",
  "warnings": null,
  "note": null
}
```

`changeType` is one of `price-drop`, `price-increase`, `price-range-changed`, `back-in-stock`, `out-of-stock`, `availability-changed`, `review-count-changed` (all of them in `changeTypes`).

Free rows have a `status` that says why: `no-price`, `no-product-data`, `blocked`, `robots-disallowed`, `robots-unreachable`, `login-required`, `not-found`, `not-html`, `unreachable`, `unreadable`, `bad-input`, `budget-reached`, and in monitor mode `baseline-recorded` and `no-change`.

### What it cannot do (please read)

- **Pages that fill in the price with JavaScript cannot be read.** This Actor reads the HTML the server sends and does not run a browser. Such pages come back as `no-product-data` or `no-price`, free.
- **It does not go past sign-in pages, check pages (CAPTCHA) or refusals.** It identifies itself honestly, follows each site's robots.txt, and does not switch to proxies to get around a 403. Several large marketplaces refuse it; those rows are `blocked` and free.
- **The currency and price can depend on the country of the connection.** Runs come from Apify's servers (usually in the United States), so a store that localises prices shows the price it shows to that country. The `currency` column says which currency was read. If the currency of a product changes between two monitor runs, it is not reported as a price change; the starting point is recorded again.
- **The price is the one in the page's data, as the store published it.** Tax-inclusive or exclusive follows the store (`taxIncluded` is filled only when the page says so). A store can leave an old price in its data for a product it no longer sells.
- **A product with several variant prices has no single price** unless the URL selects a variant; you get the range.
- Review count and rating are the store's own figures from the page data.

### How it was checked

On 2026-10-05, 42 real product pages (Shopify stores, WooCommerce stores, large retailers, brand stores, non-English stores) were read with this Actor's reader and each price that was read was compared by eye with the price a browser shows: 24 pages gave a price; of the 23 that could be compared, 22 matched (including one honest range and one page where the browser showed another currency for another country). The one that did not match was a discontinued product whose page no longer shows a price while its data still carries one. 18 pages gave no price (refused, no product data in the HTML, or page not found) and were free rows.

### Pricing

- `product-returned`: one event per product row with a readable price.
- `product-checked`: one small event per product that was compared in monitor mode and had no change.
- No start fee. The first monitor run (starting point) is free. Rows without a readable price are free.

### Related

- [Shopify Products & Price Monitor](https://apify.com/neverempty/shopify-products-price-monitor): a whole Shopify store's catalogue with price changes, from a store domain instead of product URLs.
- [Embedded Page Data Extractor](https://apify.com/neverempty/embedded-page-data-extractor): every JSON block a page ships in its HTML, for pages this Actor has no product schema for.

### Thanks for using this Actor

We build these tools for people who run them every day, and we improve them from what users tell us.

- **Missing a field, or need another filter?** Tell us in the **Issues** tab. If the data is there, we add it.
- **Found a bug or a wrong value?** Post the run ID in the **Issues** tab. Wrong data is the thing we fix first.

If this Actor saved you time, a short review helps other people find it.

# Actor input Schema

## `productUrls` (type: `array`):

Product pages from any online store, one URL per line (up to 1,000 per run). For a product with variants, paste the URL of the variant you care about (for example ...?variant=123) to get that variant's price. If you leave this empty, one example product page is read.

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

Remembers the price, stock status and review count of every URL. The first run records the starting point (free, no product rows). Later runs with the same URLs return only the products whose price, stock status or review count changed, with the previous values. Unchanged products cost one small product-checked event each.

## `watchName` (type: `string`):

Keeps a separate memory per name, so two schedules with different URL lists or purposes do not mix. Letters, digits, dot, dash and underscore, up to 40 characters.

## `resetMonitoringState` (type: `boolean`):

Monitor mode only: forget the remembered values of this watch name first, so this run records a new starting point.

## `useShopifyJs` (type: `boolean`):

For product pages on Shopify stores, also read /products/<handle>.js (if the store's robots.txt allows it) for per-variant prices, stock and compare-at prices. Turn off to read only the page itself.

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

How many pages are read in parallel (1 to 8).

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

How long to wait for a page before asking again (5 to 60).

## Actor input object example

```json
{
  "productUrls": [
    "https://www.sonos.com/en-us/shop/era-100"
  ],
  "onlyChanges": false,
  "resetMonitoringState": false,
  "useShopifyJs": true,
  "maxConcurrency": 4,
  "requestTimeoutSecs": 20
}
```

# Actor output Schema

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

One row per product page: name, brand, SKU/GTIN/MPN, price, currency, stock status, original price, rating and review count, one image URL, seller, and where each value was read from. In monitor mode only the products whose price, stock or review count changed, with the previous values. A page that robots.txt does not allow, that shows a check page, or that has no readable price comes back as a free row that says why.

# 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 = {
    "productUrls": [
        "https://www.sonos.com/en-us/shop/era-100"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/ecommerce-product-price-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 = { "productUrls": ["https://www.sonos.com/en-us/shop/era-100"] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/ecommerce-product-price-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 '{
  "productUrls": [
    "https://www.sonos.com/en-us/shop/era-100"
  ]
}' |
apify call neverempty/ecommerce-product-price-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/ecommerce-product-price-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/PGBenLpLzkIpGX1ur/builds/BruD3fCUaR4vqUHuL/openapi.json
