# Product Price Monitor & Scraper - WooCommerce, Schema.org (`sophisticated_occasion/product-data-price-tracker`) Actor

Product prices, stock, GTIN, brand and variants from WooCommerce stores (public Store API) and product pages with schema.org data (JSON-LD, microdata). One normalised format; monitor mode returns only price and stock changes. $0.001 per product.

- **URL**: https://apify.com/sophisticated_occasion/product-data-price-tracker.md
- **Developed by:** [HUG0 S](https://apify.com/sophisticated_occasion) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 product (or change record) delivereds

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

## Product Price Monitor & Scraper - WooCommerce, Schema.org

Get **clean, structured product data** (price, currency, stock, GTIN, brand, variants) from shops that publish it, in **one consistent format**, and track **price and stock changes** over time.

**$0.001 per product delivered**, no start fee. Stores and pages that fail, are blocked or have no product data are **free**. In monitor mode you pay only for changes.

### Quick start: example input, output and cost

Track competitor product prices and stock: WooCommerce stores and schema.org product pages, price-drop and restock alerts. robots.txt respected.

**Example input**

```json
{
  "stores": [
    "https://woo.com"
  ],
  "productUrls": [
    "https://yithemes.com/themes/plugins/yith-woocommerce-wishlist/"
  ],
  "maxProductsPerStore": 5
}
```

**Cost example:** 500 products x $0.001 = **$0.50**; a daily check of 200 products = **$6.00/month**.

### Five ways in, one output

| Input | How it is read | Good for |
|---|---|---|
| `stores`: WooCommerce shops | The **WooCommerce Store API**, the public API the storefront itself uses: 100 products per request | Whole catalogs, fast and exact (prices in minor units, sale prices, stock) |
| `storeApis`: Magento / Shopware (opt-in) | **Magento 2 GraphQL** (anonymous catalog) or **Shopware 6 Store API** (your sales-channel key) | Your own Magento/Shopware shop, or a store you have permission for |
| `productUrls`: product pages of any shop | **schema.org Product** data the shop publishes for search engines: JSON-LD first, then microdata, then OpenGraph product tags | Competitor or supplier pages on Shopify, Magento, Shopware, PrestaShop, custom shops |
| `discoverFromSitemap` (opt-in) on `stores` | Robots-allowed **sitemaps** (incl. nested indexes); URLs matching `productUrlPattern` are fetched as product pages | Auto-finding product URLs when you only have the shop homepage |
| `productFeeds`: open product feed URLs | **Google Merchant** RSS 2.0 (`g:` namespace), Atom, or CSV/TSV (gzip OK); robots-checked | Shops that publish a comparison-shopping feed; whole catalog in one request |

### What you get (one item per product)

`source` (`woocommerce-store-api` / `magento-graphql` / `shopware-store-api` / `json-ld` / `microdata` / `opengraph` / `google-merchant-feed`), `store, productId, productKey, url, title, brand, sku, gtin, mpn, categories, price, priceMinor, currency, regularPrice, onSale, discountPct, priceMin, priceMax, availability, inStock, stockQuantity, condition, imageUrl, images, rating, variantsCount, variants[], attributes, unitPrice, shippingDetails, returnPolicy, priceValidUntil, priceEur / priceMinEur / priceMaxEur / regularPriceEur + fxDate, fxRate, fxStatus, fxNote (optional, `convertToEur`), descriptionText (optional), quality, priceCheck, matchGroup (optional, `matchAcrossStores`), changeType, changes, fetchedAt, schemaVersion`

**Normalisation:**

- **Prices**: `"1.299,00"`, `"1,299.00"`, `"€ 9"` and `12.5` all become numbers, plus `priceMinor` (cents; whole units for JPY and other zero-decimal currencies).
- **Currency**: ISO 4217 codes; symbols such as €, £, zł and CHF are mapped. Ambiguous symbols such as `$` and `kr` are flagged in `quality.warnings`.
- **Availability**: `in_stock`, `out_of_stock`, `preorder`, `backorder`, `limited`, `discontinued` or `unknown`, plus `inStock` true/false.
- **Variants**: from schema.org `ProductGroup/hasVariant`, multiple offers, or WooCommerce variations. Attributes are included; turn on `maxVariationsPerProduct` to get each variation's price.
- **Discounts**: sale prices from WooCommerce or schema.org `StrikethroughPrice` give `regularPrice`, `discountPct` and `onSale`.
- **GTIN**: the check digit is validated. **Images** become absolute URLs.

**Data-quality block (`quality`):** `score` 0-100, a `missing` field list, `warnings` (invalid GTIN, ambiguous currency, thousands separator guessed...), and `filledFrom` when a gap was filled from OpenGraph tags. Use `minQualityScore` to skip and not pay for weak records.

**Merchant fields (new in 1.1):** when the page publishes them in JSON-LD Offers, you also get `unitPrice` (from UnitPriceSpecification), `shippingDetails` (rate, destination, handling/transit times), `returnPolicy` (category, days, method, fees, country) and `priceValidUntil`. Variant attributes come from `variesBy` / `additionalProperty` and WooCommerce term slugs.

**Price cross-check (`priceCheck`, new in 1.1):** on single-product pages the JSON-LD price is compared with the microdata and OpenGraph prices on the same page. `status` is `match`, `mismatch` or `unverified`; mismatches add a quality warning and set `likelyCentsError` when the two differ by exactly x100 (a common feed bug). Useful for auditing your own shop's structured data.

### Monitor mode (price & stock tracking)

Set `monitorName` (e.g. `competitors-daily`) and schedule the actor.

- The first run returns everything as `new`.
- Later runs return only `price-drop`, `price-increase`, `back-in-stock`, `out-of-stock`, `changed`, `new` and `removed`, with `changes` holding the old and new values and the % change.
- Removed products are reported only after a complete catalog read, or when a product page answers 404/410.
- **Change thresholds (new in 1.2):** `minPriceChangePercent` (e.g. `2`) and/or `minPriceChangeAmount` (e.g. `0.50`, in the product's currency) ignore small price moves, so you are not charged for noise. If both are set, a move must pass both. The baseline stays at the last *reported* price, so a slow drift (1 % a day) is reported once it adds up. **Stock changes are always reported**; when a stock change comes with a small price move, `changes.price.belowThreshold` is `true`. The same thresholds apply to `regularPrice`. Defaults (0) report every change, exactly as before. Ignored moves are counted in SUMMARY `belowThreshold`.
- **Price history (new in 1.2):** set `keepPriceHistory` to `true` and every returned change is also appended as one compact row (price, old price, % change, stock, time, run ID) to a named dataset `product-tracker-<monitorName>-history` in your account. Run the monitor on a schedule and you get a full price history for charts or spreadsheets. History rows are **free**; the dataset counts toward your own Apify storage. Off by default.
- **ALERTS record (new in 1.2):** every monitor run writes an `ALERTS` record to the run's key-value store with back-in-stock products and price drops of at least `alertMinDropPercent` % (0 = every drop). Point a webhook, Make, n8n or Zapier step at it to get only the changes worth acting on. Free.

### Sitemap discovery (new in 1.2)

Set `discoverFromSitemap` to `true` and put the shop homepage (or a sitemap URL) under `stores`. The actor:

1. Reads `Sitemap:` lines from robots.txt (or falls back to `/sitemap.xml` / `/sitemap_index.xml`).
2. Walks nested sitemap indexes, robots-checked on every file.
3. Keeps URLs matching `productUrlPattern` (default regex `product|/p/|/products/`) on the same host, up to `maxSitemapUrls` (default 500).
4. Fetches those pages for schema.org Product data like `productUrls`.

**Billing:** sitemap downloads are **free**. You pay only for products (or change records) delivered, same as always. Off by default so existing runs are unchanged.

### Magento GraphQL & Shopware Store API (new in 1.3)

Put entries under `storeApis` (opt-in; existing `stores` WooCommerce path is unchanged):

```json
{
  "storeApis": [
    { "url": "https://your-magento.example", "platformApi": "magento-graphql" },
    { "url": "https://your-shopware.example", "platformApi": "shopware-store-api" }
  ],
  "shopwareAccessKey": "YOUR_SALES_CHANNEL_KEY",
  "maxProductsPerStore": 100
}
```

- **Magento 2 / Adobe Commerce GraphQL**: anonymous `POST /graphql` catalog query (no login, no customer/cart mutations). Page size capped at 100. ConfigurableProduct variants included.
- **Shopware 6 Store API**: `GET /store-api/context` + `POST /store-api/product`. Requires your **sales-channel `sw-access-key`** (Admin > Sales Channels > API access) or a key the vendor publishes for a public demo. Prefer the top-level `shopwareAccessKey` secret field (per-entry key also works); it is **never** taken from page source or JavaScript.
- **robots.txt**: fail-closed for `/graphql`, `/store-api/context` and `/store-api/product` (POST with no query string, so `Disallow: /*?` does not block them).
- **Auto**: `platformApi: "auto"` (default) probes Magento GraphQL; if `shopwareAccessKey` is set it uses Shopware first.
- **Monitor mode**: `productKey` is stable (store + platform product id).
- **Billing**: API pages are **free**. You pay only for products (or change records) delivered, same $0.001, no start fee.
- **Compliance**: use only on stores you own or have permission for (or clear vendor demos). Identifying User-Agent with publisher contact details, ≤1 req/s/host, no proxies, no logins, no anti-bot bypass, no personal data.

### Open product feeds (Google Merchant, new in 1.3)

Put public feed URLs under `productFeeds` (RSS 2.0 with the Google `g:` namespace, Atom, or CSV/TSV with Google Merchant column names: `id`, `title`, `description`, `link`, `image_link`, `price`, `sale_price`, `availability`, `brand`, `gtin`, `mpn`, `condition`, `item_group_id`). Gzip is fine. The actor:

1. Checks **robots.txt for every feed URL** (404 = allow; 401/403/network fail = disallow), same fail-closed rule as pages.
2. Parses items into the same normalised product records (`sale_price` becomes `price` with `regularPrice`; availability `in_stock` / `out_of_stock` / `preorder` / `backorder`; `item_group_id` groups variants).
3. Caps at `maxFeedItems` (default 1000) per feed.
4. Works with **monitor mode** (`productKey` is stable across runs).

**Billing:** feed downloads are **free**. You pay only for products (or change records) delivered. Descriptions redact e-mails and phone numbers. XML is parsed with defusedxml (XXE/DTD blocked).

### EUR prices with ECB reference rates (optional)

Set `convertToEur` to `true` to add `priceEur`, `priceMinEur`, `priceMaxEur` and `regularPriceEur` to every product, converted with the latest **European Central Bank euro foreign exchange reference rates** (about 30 currencies, published each working day around 16:00 CET). One free fetch per run, no extra charge. Each converted item carries `fxDate`, `fxRate` (units of the shop currency per 1 EUR), `fxStatus` and `fxNote`. Currencies without an ECB rate get `fxStatus: "no-ecb-rate"`; nothing is guessed. If the ECB file can't be reached, products are still delivered without EUR fields and the problem is listed in ERRORS.

Source: European Central Bank. The EUR fields are computed by this actor (price divided by the reference rate, rounded to 2 decimals). The reference rates are published for information only, are not meant for transactions, and are available free of charge at [ecb.europa.eu](https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html).

### Compare the same product across stores (optional, new in 1.4)

Set `matchAcrossStores` to `true` when you read several shops in one run (any mix of `stores`, `storeApis`, `productUrls`, sitemap discovery and `productFeeds`). After all products are read, the actor groups offers of **the same item from different stores** and writes a free `MATCHES` record to the run's key-value store.

- **Strong identifiers only:** a valid **GTIN** (GTIN-8/12/13/14 normalised to GTIN-14 with the check digit verified, so a UPC-12 and an EAN-13 of the same item match), or, for products without a valid GTIN, the exact **brand + MPN** (case and spaces ignored; placeholders such as `N/A` are ignored). Titles are **never** compared, so there are no fuzzy false matches. GTINs with a wrong check digit are not matched.
- A group needs **at least 2 different stores** (same-shop duplicates do not count).
- Each product gets `matchGroup` (e.g. `gtin:00036000291452` or `mpn:bosch|gsr 18v-55`, stable across runs) or `null` when no other store sells it.
- `MATCHES`: `{groupCount, matchedProducts, productsConsidered, productsWithIdentifier, note, groups: [{matchGroup, matchedBy, gtin, brand, mpn, title, storeCount, offerCount, currencies, priceComparable, priceBasis, lowestPrice, highestPrice, priceSpreadPct, cheapest, cheapestInStock, offers[]}]}`, sorted by the biggest price spread first.
- **Prices are compared honestly:** in EUR when `convertToEur` gave every offer an ECB EUR price, otherwise only when all offers share one currency. Mixed currencies without EUR conversion are listed but **not compared** (`priceComparable: false` with a note).
- Works in **monitor mode** too: change records carry `matchGroup`, and `MATCHES` covers the current offers of the whole monitor (changed and unchanged).
- **Billing unchanged:** matching and `MATCHES` are **free**; you pay only for products (or change records) delivered. `MATCHES` lists only products you were charged for (now or as an earlier monitor baseline), so the maximum cost per run is respected. With matching on, products are saved at the end of the run (after all shops are read) instead of shop by shop. Off by default: output is exactly as before.

### Own-shop price position (optional, new in 1.4)

Add your own shop's host to `ownStoreHosts` (for example `["myshop.example"]`; a full URL works too, `www.` and paths are ignored, a port is kept) together with `matchAcrossStores: true`, and read your shop plus the competitors you are allowed to monitor in the same run. The actor then writes a free **`PRICE_POSITION`** record: for every matched product your shop sells, **where you are the cheapest or the most expensive, and by how much**.

- One row per match group in which your shop has an offer: `matchGroup, matchedBy, gtin, brand, mpn, title, position, priceComparable, priceBasis, currencies, ownPrice, own, ownOfferCount, ownInStock, competitorStoreCount, competitorOfferCount, pricedCompetitorStoreCount, rank, tiedCompetitors, cheapestCompetitor, cheapestInStockCompetitor, highestCompetitor, deltaToCheapestAmount, deltaToCheapestPct, deltaToHighestAmount, deltaToHighestPct, competitors[]` (plus `note` when prices cannot be compared).
- `position` is one of `cheapest`, `tied-cheapest`, `mid`, `most-expensive`, `only-offer` (no competitor price) or `no-comparable-price` (your offer has no price, or mixed currencies without `convertToEur`). `deltaToCheapestAmount` = your price minus the cheapest competitor's price, so a negative value is how far below the next competitor you are. `rank` 1 = cheapest store.
- Top level: `ownStoreHosts, ownProducts, ownProductsMatched, ownProductsUnmatched, rowCount, positions` (count per position), `fxDate, note, rows` (sorted with the most over-priced products first) and `schemaVersion`. Plain JSON with stable field names, so a webhook on run success can fetch `PRICE_POSITION` from the run's key-value store and act on it.
- Same matching and price rules as `MATCHES` (GTIN or brand + MPN only; EUR when `convertToEur` gave every offer an ECB EUR price, otherwise only within one currency). Several own hosts (e.g. your `.nl` and `.com` shop) count as one seller; your lowest own offer is used.
- Needs `matchAcrossStores: true` (otherwise the run stops with a free input error). If no delivered product comes from your host(s), a warning is added to ERRORS. Works in monitor mode (the report covers the monitor's current offers).
- **Free; billing unchanged.** Only products you were charged for are used, so the maximum cost per run is respected. SUMMARY gains `pricePositionRows`, `ownCheapest` (cheapest + tied-cheapest) and `ownMostExpensive` when on. Off by default: output is exactly as before.

### Export a Google Merchant Center feed for your own shop (optional, new in 1.4)

Set `merchantFeedExport` to `true` (and `includeDescription` to `true`, because Google requires a description) to turn the products of a run into a ready-to-upload **Google Merchant Center product feed**. Meant for **your own shop**: read it with `stores`, `storeApis`, `productUrls`, sitemap discovery or `productFeeds`, then upload or schedule-fetch the file in Merchant Center.

- **MERCHANT_FEED** (key-value store, tab-separated text): Google attribute names `id, item_group_id, title, description, link, image_link, additional_image_link, availability, price, sale_price, brand, gtin, mpn, identifier_exists, condition, product_type, color, size`. Prices are written as `49.90 EUR`; a discount becomes `price` = regular price plus `sale_price`. Variants become one row each with `item_group_id`, `color` and `size`.
- **MERCHANT_FEED_ISSUES** (key-value store, JSON): every product or variant Google would reject (missing description, image, link, price or a mappable availability, duplicate id, unknown variant price), with the reason, plus warnings (GTIN with a wrong check digit left out, no GTIN and no brand + MPN so `identifier_exists` = `no`, titles over 150 characters). Rejected rows are **not** put in the feed.
- **Nothing is guessed:** no invented descriptions, GTINs, conditions or variant prices.
- **Free; billing unchanged.** The feed covers only products you were charged for in this run (or, in monitor mode, the whole current catalogue of the monitor), so the maximum cost per run is respected. Off by default: output is exactly as before. SUMMARY gains `merchantFeedRows` / `merchantFeedRejected` when on.

### Limits

- Only shops that publish product data openly (WooCommerce/Magento/Shopware public APIs, schema.org markup, open feeds). Amazon, eBay and other marketplaces are not supported.
- Prices are what the shop shows to an anonymous visitor; customer-group or logged-in prices are not visible.

### Responsible use (public product data only)

- Only **public product data** from shops that allow automated access. **robots.txt is checked for every URL** (RFC 9309, fail-closed). Disallowed stores and pages are skipped and listed in ERRORS, free of charge.
- About **1 request per second per site**, with an honest User-Agent (`ProductFeed/1.0`; [digivibetech.com/support](https://digivibetech.com/support/)). **No proxies, no cookies, no logins.** HTTP 401/403/429 blocks are reported and never bypassed.
- **No personal data**: reviews, review authors and Person nodes are dropped (only the aggregate rating is kept). Descriptions are optional, with e-mails and phone numbers redacted.
- Check each shop's terms of use before monitoring it; using the data is your responsibility. Google Shopping, Amazon and other sites that forbid automated access are not supported.

### Pricing

| Event | Price |
|---|---|
| Product (or change record) delivered | $0.001 |

Example: a daily monitor of 2,000 products where 3% change each day costs about $0.06 per day.

### Output

- **Dataset**: one item per product or change, with an "overview" table view.
- **ERRORS** (key-value store): every input that failed, with the reason (not charged).
- **SUMMARY** (key-value store): counts of stores, pages, feeds / feedItems, products seen, delivered, unchanged, below-threshold (monitor mode), history rows, alerts, sitemaps / sitemapUrls (discovery), matchGroups / matchedProducts (with `matchAcrossStores`), pricePositionRows / ownCheapest / ownMostExpensive (with `ownStoreHosts`), filtered and billed.
- **MATCHES** (key-value store, only with `matchAcrossStores`): cross-store match groups with lowest/highest price, cheapest store and price spread (free).
- **PRICE_POSITION** (key-value store, only with `ownStoreHosts`): your own shop's price position per matched product: cheapest / most expensive and by how much (free).
- **MERCHANT_FEED** / **MERCHANT_FEED_ISSUES** (key-value store, only with `merchantFeedExport`): Google Merchant Center TSV feed and the list of rejected products with reasons (free).
- **ALERTS** (key-value store, monitor mode): `{monitorName, alertMinDropPercent, count, alerts: [{alert: "price-drop" | "back-in-stock", productKey, title, url, store, oldPrice, price, pctChange, currency, availability}]}`.

### Roadmap

Requests are welcome via Issues.

*Covered by tests (unit + end-to-end with a mock store).*

# Actor input Schema

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

Store domains or URLs of WooCommerce shops. The whole public catalog is read from the WooCommerce Store API (the storefront's own public API). Up to 500 per run.

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

Product pages of any shop that publishes schema.org Product data (JSON-LD, microdata or OpenGraph product tags), which most shop systems do. Up to 10,000 per run.

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

Stops reading a store after this many products.

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

Remembers every product in a named key-value store. The first run returns all products (changeType 'new'); later runs return only price drops/increases, back-in-stock/out-of-stock, new and removed products. You pay only for what is returned.

## `monitorName` (type: `string`):

Use different names for separate monitors (lowercase letters, digits, dashes). Setting it turns on monitor mode.

## `minPriceChangePercent` (type: `number`):

Monitor mode only. Price moves smaller than this percentage of the last reported price are not returned (and not charged). The baseline stays at the last reported price, so slow drifts are reported once they add up. Stock changes are always reported. 0 = report every change (default).

## `minPriceChangeAmount` (type: `number`):

Monitor mode only. Price moves smaller than this absolute amount (in the product's own currency, e.g. 0.50) are not returned. If both thresholds are set, a change must pass both. Stock changes are always reported. 0 = off (default).

## `keepPriceHistory` (type: `boolean`):

Monitor mode only. Appends one compact row per returned change (price, old price, % change, stock, time) to a named dataset 'product-tracker-<monitorName>-history' in your account, so you get a full price history across runs. Free (not charged); the dataset counts toward your own Apify storage.

## `alertMinDropPercent` (type: `number`):

Monitor mode only. Each run writes an ALERTS record (key-value store) listing back-in-stock products and price drops of at least this percentage, ready for a webhook, Make, n8n or Zapier step. 0 = every price drop (default).

## `discoverFromSitemap` (type: `boolean`):

Opt-in. For each site under 'stores', read robots-allowed sitemaps (and nested indexes), keep URLs matching productUrlPattern, then fetch those product pages for schema.org Product data. Sitemap fetches are free; you are charged only for products delivered. Off by default (existing behaviour unchanged).

## `productUrlPattern` (type: `string`):

Regular expression matched against each sitemap URL. Default 'product|/p/|/products/' keeps typical product paths and drops about/blog/category pages. Only used when discoverFromSitemap is on.

## `maxSitemapUrls` (type: `integer`):

Stops sitemap discovery for a store after this many matching URLs. Only used when discoverFromSitemap is on.

## `productFeeds` (type: `array`):

Opt-in. Public Google Merchant product feeds (RSS 2.0 with g: namespace, Atom, or CSV/TSV with Google Merchant column names; gzip OK). robots.txt is checked for every feed URL (fail-closed). Feed downloads are free; you are charged only for products delivered. Up to 100 feeds per run.

## `maxFeedItems` (type: `integer`):

Stops reading a product feed after this many products (after item_group_id grouping). Only used when productFeeds is set.

## `storeApis` (type: `array`):

Opt-in. Magento 2 GraphQL (anonymous catalog reads) or Shopware 6 Store API (needs your sales-channel sw-access-key). robots.txt is checked fail-closed for /graphql and /store-api/\*; API pages are free; you pay only for products delivered. Existing stores\[] WooCommerce path is unchanged. Up to 100 entries. Use only on stores you own or have permission for (or vendor demos).

## `shopwareAccessKey` (type: `string`):

Default Shopware 6 sales-channel access key for storeApis entries that need it (Admin > Sales Channels > API access, or a vendor-published demo key). Stored as a secret. Per-entry shopwareAccessKey overrides this. Never scraped from page source.

## `includeVariants` (type: `boolean`):

Variants with attributes (size, colour...), SKU, GTIN, price and availability where the source provides them.

## `maxVariationsPerProduct` (type: `integer`):

The WooCommerce catalog lists variations without prices. Set > 0 to fetch each variation's price and stock (one extra request per variation, 1 per second per store).

## `onlyInStock` (type: `boolean`):

Skip products that are known to be out of stock.

## `minQualityScore` (type: `integer`):

Skip (and do not charge for) products whose quality.score is lower, e.g. 70 drops records without a price or currency.

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

Product description as plain text (HTML removed, e-mail addresses and phone numbers redacted, max 20,000 characters).

## `convertToEur` (type: `boolean`):

Adds priceEur / priceMinEur / priceMaxEur / regularPriceEur, fxDate and fxRate to every product, converted with the latest European Central Bank euro reference rates (about 30 currencies, updated each working day). One free fetch per run. Rates are for information only, not for transactions; source and free availability are stated in fxNote.

## `matchAcrossStores` (type: `boolean`):

Opt-in. After all inputs are read, groups products from different stores that are the same item, by valid GTIN (UPC/EAN normalised to GTIN-14, check digit verified) or else exact brand + MPN. Titles are never compared. Adds matchGroup to each product and writes a free MATCHES record with lowest/highest price, cheapest store and price spread (prices compared in EUR with convertToEur, otherwise only within one currency). Free; billing unchanged. Products are saved at the end of the run.

## `ownStoreHosts` (type: `array`):

Opt-in, needs matchAcrossStores. Host(s) or URL(s) of your own shop (e.g. myshop.example; www. and paths are ignored, a port is kept). Writes a free PRICE_POSITION record: for every matched product your shop sells, your price vs the cheapest and the highest competitor, the difference in amount and %, and a position (cheapest, tied-cheapest, mid, most-expensive, only-offer, no-comparable-price). Ready for a webhook. Free; billing unchanged.

## `merchantFeedExport` (type: `boolean`):

Opt-in. Writes a free MERCHANT_FEED record (tab-separated, Google Merchant Center attribute names: id, title, description, link, image_link, availability, price, sale_price, brand, gtin, mpn, item_group_id, ...) from the products of this run, plus MERCHANT_FEED_ISSUES listing every product Google would reject and why. Meant for your own shop. Turn on includeDescription, because Google requires a description. Free; billing unchanged.

## Actor input object example

```json
{
  "stores": [
    "https://woo.com"
  ],
  "productUrls": [
    "https://yithemes.com/themes/plugins/yith-woocommerce-wishlist/"
  ],
  "maxProductsPerStore": 5,
  "onlyChanges": false,
  "minPriceChangePercent": 0,
  "minPriceChangeAmount": 0,
  "keepPriceHistory": false,
  "alertMinDropPercent": 0,
  "discoverFromSitemap": false,
  "productUrlPattern": "product|/p/|/products/",
  "maxSitemapUrls": 500,
  "productFeeds": [],
  "maxFeedItems": 1000,
  "storeApis": [],
  "includeVariants": true,
  "maxVariationsPerProduct": 0,
  "onlyInStock": false,
  "minQualityScore": 0,
  "includeDescription": false,
  "convertToEur": false,
  "matchAcrossStores": false,
  "ownStoreHosts": [],
  "merchantFeedExport": false
}
```

# Actor output Schema

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

All result items (overview view)

## `errors` (type: `string`):

Inputs that could not be processed, with the reason

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

Counts of processed, billed and failed items

# 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": [
        "https://woo.com"
    ],
    "productUrls": [
        "https://yithemes.com/themes/plugins/yith-woocommerce-wishlist/"
    ],
    "maxProductsPerStore": 5,
    "productFeeds": [],
    "storeApis": [],
    "ownStoreHosts": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("sophisticated_occasion/product-data-price-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": ["https://woo.com"],
    "productUrls": ["https://yithemes.com/themes/plugins/yith-woocommerce-wishlist/"],
    "maxProductsPerStore": 5,
    "productFeeds": [],
    "storeApis": [],
    "ownStoreHosts": [],
}

# Run the Actor and wait for it to finish
run = client.actor("sophisticated_occasion/product-data-price-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": [
    "https://woo.com"
  ],
  "productUrls": [
    "https://yithemes.com/themes/plugins/yith-woocommerce-wishlist/"
  ],
  "maxProductsPerStore": 5,
  "productFeeds": [],
  "storeApis": [],
  "ownStoreHosts": []
}' |
apify call sophisticated_occasion/product-data-price-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sophisticated_occasion/product-data-price-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/u1c2e1XImHWhSJtm2/builds/4Jqm0gLtoWXFaq4X2/openapi.json
