# Product Price & Stock Monitor: any shop's schema.org data (`humble-echidna/product-offers`) Actor

Get price, currency, stock, GTIN (UPC/EAN), SKU, MPN, brand, name and image from any shop's product pages, read from their schema.org Product data. Paste product URLs or whole stores (Shopify, WooCommerce and more, via sitemaps). Monitor mode returns only changed prices and stock.

- **URL**: https://apify.com/humble-echidna/product-offers.md
- **Developed by:** [Michael Costa](https://apify.com/humble-echidna) (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

from $1.05 / 1,000 products

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
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

### What does Product Price & Stock Monitor do?

**Product Price & Stock Monitor** reads **price, currency, stock, GTIN (UPC/EAN), SKU, brand, name and image** from
**any shop's product pages**, straight from the **schema.org Product data** the shop publishes for search engines
(JSON-LD or microdata). Give it product URLs, a **whole store** (found through its product sitemaps), or another
actor's results ([chained](#chain-it-after-another-actor)).

Its headline mode is a **price monitor**: run it on a schedule and get **only the products whose price or stock
changed** since the last run, with the previous price. It is **not** a marketplace scraper: it reads shops that
serve their pages to any polite visitor, and does nothing to get past bot protection (see [Limits](#limits)).

**Try it in one click:** the input comes pre-filled with three product pages of the Wikipedia Store. That's 3
products, about $0.005 (3 × $0.0015, plus $0.00005 for the run start). **Then replace them with the products or
stores you actually want to watch.**

### Monitor prices: get only changed prices and stock, in Slack, email or a webhook

With **Only changed prices and stock since the last run** on, each run compares every product with the last run of
the same task and returns only **new products** and products whose **price, price range, currency or availability**
changed, or one of their **variants'** price or availability. Each row says what changed (`"price 11.00 -> 9.50
USD"`, `"availability InStock -> OutOfStock"`) and carries `previousPrice` and `previousAvailability`. Unchanged
products are checked but not returned, at **$0.15 per 1,000** instead of $1.50.

1. Put your product URLs in **Product page URLs** (or a shop in **Or: whole stores**), turn on **Only changed prices
   and stock since the last run** (`"onlyChangedProducts": true`), and click **Start**. This first run returns every
   product and is the baseline the next runs compare with.
2. Click **Save as a new task** (top right of the actor page). The comparison belongs to that task, so each task
   compares with its own last run. Adding a product to the list doesn't start over: it's returned once as `new`.
3. In Apify Console, open **Schedules**, click **Create new**, set how often in **Schedule setup** (for example
   daily at 07:00), then **Add** your task.
4. On the task, open the **Integrations** tab and pick where the changes go:
   - **Slack**: click **Configure**, sign in, pick the workspace and channel, and the "run succeeded" event. A
     useful message: `{{resource.statusMessage}}` and a link to the results,
     `<https://console.apify.com/storage/datasets/{{resource.defaultDatasetId}}|price changes>`.
   - **Gmail**: click **Connect with Google**, set the subject and body, and attach the dataset (for example as
     CSV). It sends after each successful run.
   - **HTTP webhook**: event `ACTOR.RUN.SUCCEEDED`, your URL. Apify POSTs `{"eventType": ..., "resource": {...}}`;
     `resource.defaultDatasetId` is the run's dataset, and
     `GET https://api.apify.com/v2/datasets/<defaultDatasetId>/items?format=json` (with your API token) returns the
     changed products.

Apify's integrations fire after every successful run, including quiet ones: a quiet run's dataset is empty, and its
status message says so. The pre-filled three products, run twice with the option on (local runs, 2026-09-28): the
first returned all three; the second returned none and said "0 products returned since the run of 2026-09-29 01:57
UTC: 0 changed, 0 new, 3 unchanged (not returned)".

**A page that fails is never reported as a change.** When a page is blocked, down, disallowed by robots.txt or
suddenly states no product (a maintenance page), it isn't compared at all: it's listed in `NOT_RETURNED`, isn't
charged, and its last known price is kept for the next run.

### What data does Product Price & Stock Monitor return?

| Field | Example | Notes |
|---|---|---|
| `name`, `brand` | `Wikipedia pens (5 pack)`, `Bic Clic` | As the page states them. |
| `price`, `currency` | `11.0`, `USD` | A number and an ISO 4217 code. With several offers, the lowest price. |
| `availability` | `InStock` | Plain schema.org names: `InStock`, `OutOfStock`, `PreOrder`, `BackOrder`, `LimitedAvailability`, `SoldOut`, `Discontinued`...; with several offers, the best one. |
| `lowPrice`, `highPrice`, `offerCount` | `26.0`, `30.0`, `7` | Across the offers (variants), or from an `AggregateOffer`. |
| `sku`, `mpn` | `76647-83-OSFA` | `null` when the page doesn't state them. |
| `gtin`, `gtinFormat` | `4006381333931`, `GTIN-13` | UPC/EAN with a valid check digit; a code that fails the check is left out. |
| `image` | `https://.../Wiki70__1.jpg` | The first product image URL. |
| `offers` | `[{"sku": "76647-21-S", "price": 26.0, "availability": "InStock", ...}]` | Each offer or variant, up to 50. |
| `ratingValue`, `reviewCount` | `4.6`, `9951` | From the page's `aggregateRating`. |
| `changeType`, `changes`, `previousPrice`, `previousAvailability` | `changed`, `["price 11.00 -> 9.50 USD"]`, `11.0`, `InStock` | Only in the monitoring mode, after the first run. |
| `url`, `id`, `store` | | The page, a stable id per page, and the store it was found from. |
| `sourceTitle`, `sourceIndex` | `"Wikipedia pens"`, `12` | From the dataset item the URL came from (see [Chain it after another actor](#chain-it-after-another-actor)); `null` for URLs you type in. |

One row per product page. The full list is under [Output](#output).

### How much does it cost to monitor product prices?

You pay per product returned: **$1.50 per 1,000 products**, plus $0.00005 each time a run starts. In the monitoring
mode, a product that's checked and found unchanged isn't returned and costs **$0.15 per 1,000**. A page that gives no
product is **never charged**: blocked by the shop, disallowed by robots.txt, a page without product data, a category
page, a dead link or a failed request.

- **The example below:** 3 products × $0.0015 = $0.0045, plus the start fee.
- **A month, for example:** a daily monitor of 1,000 products where about 20 change a day: 30 × (20 × $0.0015 + 980
  × $0.00015) ≈ **$5.30**, after a first full run of $1.50.
- **A whole store:** a 500-product shop read once is 500 × $0.0015 = $0.75.
- **Caps:** **Max results per run** in the input, and **Maximum cost per run** in the run options. The run stops
  cleanly at whichever comes first, before reading pages it can't charge for.

### How to monitor product prices and stock

1. Open Product Price & Stock Monitor and click **Try for free** (or **Start** if you're signed in).
2. Paste product page URLs into **Product page URLs**, or a shop's address into **Or: whole stores**.
3. Optional: **URL patterns** and **Max pages per store** for stores, and **Only changed prices and stock since the
   last run** for scheduled monitoring.
4. Click **Start**, then open the **Output** tab and export as JSON, CSV or Excel.

### Example: three products from the Wikipedia Store

The pre-filled input:

```json
{"productUrls": ["https://store.wikimedia.org/products/wikipedia-pens",
                 "https://store.wikimedia.org/products/wikimedia-project-stickers-pack-of-14",
                 "https://store.wikimedia.org/products/black-wikipedia-globe-t-shirt-with-wikipedia-written-in-multiple-languages"]}
```

It returned 3 products: the pens at $11.00, the stickers at $10.00, and the t-shirt from $26.00 to $30.00 over 7
sizes, all in stock. One of them (real output from a local run on 2026-09-28, US time):

```json
{
  "id": "6efda45baab4c7d6c9a231ed",
  "url": "https://store.wikimedia.org/products/wikipedia-pens",
  "name": "Wikipedia pens (5 pack)",
  "brand": "Bic Clic",
  "sku": "76647-83-OSFA",
  "mpn": null,
  "gtin": null,
  "gtinFormat": null,
  "image": "https://store.wikimedia.org/cdn/shop/files/Wiki70__1.jpg?v=1690475735&width=1920",
  "price": 11.0,
  "lowPrice": 11.0,
  "highPrice": 11.0,
  "currency": "USD",
  "availability": "InStock",
  "offerCount": 1,
  "itemCondition": null,
  "offers": [{"name": null, "sku": "76647-83-OSFA", "gtin": null, "price": 11.0, "currency": "USD",
              "availability": "InStock",
              "url": "https://store.wikimedia.org/products/wikipedia-pens?variant=217722796"}],
  "ratingValue": null,
  "reviewCount": null,
  "structuredData": "json-ld",
  "changeType": null,
  "changes": null,
  "previousPrice": null,
  "previousAvailability": null,
  "previousCheckedAt": null,
  "input": "https://store.wikimedia.org/products/wikipedia-pens",
  "store": null,
  "sourceTitle": null,
  "sourceIndex": null,
  "scrapedAt": "2026-09-29T01:58:50.677802Z"
}
```

A whole store, the same day: `{"stores": ["store.wikimedia.org"]}` read the store's product sitemap (45 pages) in 47
seconds and returned 44 products; the 45th page, the store's home page, states no product and wasn't charged.

### Input

| Field | What it does |
|---|---|
| **Product page URLs** (`productUrls`) | Product pages, one per line, up to 10,000. Ignored when stores or a dataset are given. |
| **Or: whole stores** (`stores`) | Shops to read in full: a home page (`store.example.com`) or a sitemap URL, up to 50. Ignored when a dataset is given. |
| Or: product URLs from a dataset (`datasetId`) | One of your Apify datasets, e.g. another actor's results: each item's product page URL is read as if you had typed it into **Product page URLs**. See [Chain it after another actor](#chain-it-after-another-actor). |
| Field with the product URL (`datasetUrlField`) | Only with a dataset. Empty (default): found automatically among `productUrl`, `url`, `link` and `loadedUrl`, then one level down (`product.url`). |
| **URL patterns** (`urlPatterns`) | Stores only: read only product pages matching one of these, e.g. `*/products/*`. |
| **Max pages per store** (`maxPagesPerStore`) | Stores only: pages read per store, 1 to 10,000 (default 100), in the order the sitemaps list them. |
| **Only changed prices and stock since the last run** (`onlyChangedProducts`) | Scheduled monitoring: return only new and changed products. |
| **Max results per run** (`maxResults`) | Caps the products read in the run. |

#### Whole stores

A store's product pages come from its **product sitemaps**, which the big shop platforms name for products:
Shopify's `sitemap_products_1.xml`, WooCommerce/Yoast's `product-sitemap.xml`, WordPress's
`wp-sitemap-posts-product-1.xml`, BigCommerce's `xmlsitemap.php?type=products`. The actor finds them through the
shop's robots.txt `Sitemap:` lines (else `/sitemap.xml`) and reads only those, so blog posts and pages aren't
fetched. Localised copies (Shopify's `/fr/sitemap_products_1.xml`) are skipped when the main one is there, since
they list the same products again; give a localised sitemap's URL directly to read that one.

A shop without a product sitemap has its whole sitemap read in its own order. Pages that aren't products are
skipped and not charged, and if a store's first 25 pages in a row state no product, it's stopped (it most likely
doesn't publish product data); URL patterns point it straight at the product pages.

### Output

Every row has the same fields. Fields the page doesn't state are `null`.

`id` is stable across runs (one per product page), so you can use it to deduplicate. Next to the dataset, the run
writes two records:

- **`NOT_RETURNED`**: every page that gave no product, with `status` and `reason`: `blocked` (a 401/403/451 answer,
  a rate limit, or a bot check served with 200), `robots` (robots.txt disallows it), `no-product-data`,
  `listing-page` (a category or search page), `not-found`, `not-html`, `failed`, `not-tried` (skipped because
  the shop blocked 3 pages in a row, or the store stated no product on its first 25 pages), `duplicate`. None of
  these is charged.
- **`RUN_STATS`**: per input line, what it returned and why anything failed; for stores, the sitemaps read, pages
  listed and read, and the counts by status; in the monitoring mode, how many products were new, changed and
  unchanged.

### Run it on a schedule, or from your own code

1. Save your input as a **task** and add it to a **schedule** (Console → Schedules).
2. Collect results: download the dataset, call
   `GET https://api.apify.com/v2/actor-tasks/<task id>/runs/last/dataset/items?status=SUCCEEDED&format=csv` with
   your API token, add a webhook, or connect Make, Zapier or n8n.

#### Chain it after another actor

To get the price and stock of the products another actor found as soon as it finishes (an e-commerce scraper, a
[Sitemap URL Extractor](https://apify.com/humble-echidna/sitemap-urls) run, any list of product links; any actor,
ours or not), chain it with an integration:

1. Open the other actor or its saved task, go to the **Integrations** tab and add **Apify actor** →
   **Product Price & Stock Monitor** (or a saved task of it), triggered when a run **succeeds**.
2. Set `"datasetId": "{{resource.defaultDatasetId}}"` in the input (Apify fills in the finished run's dataset) and
   the rest as usual, e.g. `"onlyChangedProducts": true`. **Product page URLs** and **Or: whole stores** are ignored
   then.

Every successful run of the other actor then gives you prices and stock, with no code. From the API it's the same
input: pass a run's `defaultDatasetId` as `datasetId`. In the form, pick the dataset in **Or: product URLs from a
dataset**.

- **Which field:** the product page address is found automatically in `productUrl`, `url`, `link` or `loadedUrl`
  (the first that holds one in the first 100 items), or one level down (`product.url`). Otherwise set **Field with
  the product URL** (`datasetUrlField`).
- **Only changed prices and stock works in a chain.** The comparison belongs to the task and the URL patterns, not
  to the dataset: a chained run reads a new dataset every time and still compares with the last run, and a product
  URL from a dataset shares its memory with the same URL typed in. Point the integration at a saved task of this
  actor to keep that chain's comparison apart from your other runs.
- **Stores and URL patterns** don't apply to a dataset's links: to read only product pages from a whole sitemap,
  filter them in the other actor (Sitemap URL Extractor's **Only URLs matching**), or give the shop here in **Or:
  whole stores** instead. Pages that aren't products are skipped and not charged either way.
- **Joining back:** each row carries `sourceTitle` and `sourceIndex` (0 = the dataset's first item) from the item it
  came from.
- **Skipped, never fetched or charged:** items without a URL, values that aren't a web address, and the same page
  twice (tracking parameters such as `utm_source` are dropped first); they're counted in `RUN_STATS.dataset`. A
  dataset with no product URL at all fails the run with the reason.
- **Limits:** the first 20,000 items and 10,000 distinct URLs of a dataset per run, read with your own account's
  access, read-only. A product from a dataset costs the same as one typed in; the other actor's run is billed by it.

#### Can I use Product Price & Stock Monitor from an AI agent (MCP)?

Yes. Through Apify's MCP server an agent can call it with `productUrls` (or `stores`) and read back normalised
prices, currency codes, availability and GTINs, so it doesn't have to parse shop pages itself. Keep `maxResults`
small for interactive use. An agent that has just run an e-commerce scraper can pass its dataset instead:
`{"datasetId": "<dataset id>", "maxResults": 20}`.

### Who it's for

Brands and resellers watching their own and competitors' prices and stock on independent shops, price-comparison
and deal sites refreshing their catalogue, and anyone feeding a spreadsheet or database with product data from shops
built on Shopify, WooCommerce, Magento, BigCommerce and similar. The job repeats: a daily or hourly run of the same
list.

### Why this one?

- **One schema for any shop that publishes schema.org data**, not one actor per platform: Shopify, WooCommerce and
  the rest come back with the same fields, with prices as numbers, currencies as ISO codes and availability as plain
  names.
- **Only changed prices and stock**, with the previous values, at $0.15 per 1,000 for unchanged products: a daily
  1,000-product monitor with little change costs about $5 a month.
- **Nothing charged for pages that give nothing**: blocked, disallowed, no product data, listing pages and failures
  are listed in `NOT_RETURNED` with the reason.
- **GTINs you can match on**: UPC/EAN codes are checked against their check digit, so a mistyped code never reaches
  your join.
- **Polite and safe.** It identifies itself honestly (User-Agent `HumbleEchidnaApify`), follows each shop's
  robots.txt and Crawl-delay, reads at most one page a second from any one shop, and only requests
  public web addresses on the standard ports (80 and 443). It uses no proxies.
- **Reliable.** One failing page or store never affects the others. The log, `RUN_STATS` and `NOT_RETURNED` say
  which input had a problem and why.

### Limits

- **Bot-walled giants won't work**: big marketplaces and retailers that block automated visitors answer with a block
  page or a challenge. Those pages come back as `blocked` and aren't charged; the actor never tries to get around
  them (no proxies, no browser tricks). It's built for the long tail of independent shops.
- **Only what the page states in schema.org markup.** A shop that doesn't publish Product structured data, or builds
  it with JavaScript after the page loads, comes back as `no-product-data`. Prices shown only in the visible page,
  shipping costs, coupons and member prices aren't read.
- **One product per page.** Category and search pages are skipped as `listing-page`; use a store's sitemap to reach
  its product pages. At most 50 offers (variants) are listed per product; `offerCount` has the full number.
- Some shops state the price of the first variant only, or a price without currency: the row says exactly what the
  page says (`currency: null` rather than a guess).
- **Speed:** one page a second from any one shop, so a 1,000-product store takes about 17 minutes (300 products took
  310 seconds on Apify, 2026-09-28). Several shops are read in parallel.
- A product that disappears from a store isn't reported as removed; a product URL that stops working is listed in
  `NOT_RETURNED` as `not-found`.

### FAQ

#### Is it legal to monitor product prices this way?

It reads pages you choose, like a visitor would, and only the structured data those pages publish for machines
(schema.org markup, which shops add so search engines can show their prices). It follows each shop's robots.txt,
logs in nowhere, collects no personal data, and never works around a block. What you do with the data, and whether a
shop's own terms allow it, is up to you: check the terms of the shops you monitor. The pre-filled example reads the
Wikipedia Store, whose robots.txt says public product pages are crawlable.

#### Why did a page come back without a product?

Open the `NOT_RETURNED` record: each page has a `status` and a `reason`. `blocked` means the shop refused us (a
403, a rate limit, or a bot check); `no-product-data` means the page has no schema.org Product markup (or adds it
with JavaScript); `listing-page` means it lists several products. A shop that changes its theme can stop publishing
structured data without notice.

#### Does it work on the big marketplaces?

No. The big marketplaces and retail chains block automated visitors, and this actor doesn't get around blocks: their
pages come back as `blocked`, uncharged. Point it at the brands' own shops instead: many sell through their own
Shopify or WooCommerce shop.

#### Can I get only changed prices since my last run?

Yes: turn on **Only changed prices and stock since the last run** and save the input as a task (see [Monitor
prices](#monitor-prices-get-only-changed-prices-and-stock-in-slack-email-or-a-webhook)).

#### How is this different from a Shopify or WooCommerce scraper?

Platform scrapers read one platform's own product feed (for example Shopify's products JSON), which gives every
variant's details but only on that platform. This actor reads the schema.org data on the product page, which most
platforms publish, so one list can mix shops on different platforms and every row has the same fields. It returns
what the page states for search engines, which is usually the price and stock a visitor sees.

#### How fresh is the data?

Live: every run fetches the pages at that moment. Nothing is cached between runs except, in the monitoring mode,
the last price and stock of each product for the comparison.

### Related actors

| Actor | Use it when |
|---|---|
| [Sitemap URL Extractor](https://apify.com/humble-echidna/sitemap-urls) | You want every URL of a shop's sitemaps, not just the products; or chain it before this one (see [Chain it after another actor](#chain-it-after-another-actor)). |
| [Website Technology Detector](https://apify.com/humble-echidna/tech-stack-detector) | You want to know which shops run Shopify, WooCommerce or Magento before monitoring them. |
| [Website & Page to Markdown](https://apify.com/humble-echidna/page-to-markdown) | You want the product pages' text for an LLM or a search index. |

### Feedback and support

Found a bug, or a shop whose structured data comes back wrong? Open an issue on the **Issues** tab with the URL and
the input you used.

### Versions

Current version: **1.1**. See the Changelog tab for what changed in each version.

# Changelog

This Actor's version history is a separate document: https://apify.com/humble-echidna/product-offers/changelog.md

# Actor input Schema

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

Product pages to read, one per line, e.g. https://store.example.com/products/blue-mug, from any shop that publishes schema.org Product data (most Shopify, WooCommerce, Magento and BigCommerce shops do). Each page returns one product with its offers. Up to 10,000 URLs. Ignored when `stores` or `datasetId` is set.

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

Shops to read in full, one per line: a home page (store.example.com) or a sitemap URL. Product pages are found in the shop's product sitemaps (e.g. Shopify's sitemap\_products\_1.xml), up to Max pages per store each. Up to 50 stores. When set, `productUrls` is ignored. Ignored when `datasetId` is set.

## `datasetId` (type: `string`):

Default empty. One of your Apify datasets, picked here or given by id, e.g. an e-commerce scraper's or Sitemap URL Extractor's results; to chain this actor after another in an integration, `{{resource.defaultDatasetId}}`. Each item's product page URL is read like a line of `productUrls`; `productUrls` and `stores` are ignored. Duplicates are read once. Each row carries sourceTitle and sourceIndex from its item. Reads at most 20,000 items and 10,000 URLs, read-only.

## `datasetUrlField` (type: `string`):

Only with `datasetId`: the item field that holds the product page URL, e.g. `url`, or a dotted path such as `product.url`. Leave empty (the default) to find it automatically: the first of productUrl, url, link and loadedUrl that has a web address in the first 100 items, then the same names one level down (product.url).

## `urlPatterns` (type: `array`):

Only with `stores`: read only product pages whose URL matches one of these, e.g. `*/products/*` or `/collections/mugs/`. `*` matches anything and the pattern must then match the whole URL; without `*` it matches anywhere in the URL. Case-insensitive. Leave empty (the default) for every product page.

## `maxPagesPerStore` (type: `integer`):

Only with `stores`: the most product pages read per store, in the order its sitemaps list them, from 1 to 10,000. Default 100. Pages that give no product (not a product page, blocked) count too but aren't charged.

## `onlyChangedProducts` (type: `boolean`):

For scheduled monitoring. Each run compares every product with the last run of this task and returns only new products and those whose price, currency or availability changed (or a variant's), with what changed and the previous price. The first run returns every product and is the baseline. Unchanged products aren't returned and cost less. Changing the URL patterns starts a new baseline; a new `datasetId` doesn't, so a chained run compares with the last run too.

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

Stop after this many products in total (with Only changed products on: products checked, returned or not), e.g. 500. Minimum 1; leave empty (the default) for no limit. The run also stops cleanly at the maximum cost per run you set in the run options, whichever comes first.

## Actor input object example

```json
{
  "productUrls": [
    "https://store.wikimedia.org/products/wikipedia-pens",
    "https://store.wikimedia.org/products/wikimedia-project-stickers-pack-of-14",
    "https://store.wikimedia.org/products/black-wikipedia-globe-t-shirt-with-wikipedia-written-in-multiple-languages"
  ],
  "maxPagesPerStore": 100,
  "onlyChangedProducts": false
}
```

# Actor output Schema

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

No description

## `notReturned` (type: `string`):

No description

## `runStats` (type: `string`):

No description

# 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://store.wikimedia.org/products/wikipedia-pens",
        "https://store.wikimedia.org/products/wikimedia-project-stickers-pack-of-14",
        "https://store.wikimedia.org/products/black-wikipedia-globe-t-shirt-with-wikipedia-written-in-multiple-languages"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("humble-echidna/product-offers").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://store.wikimedia.org/products/wikipedia-pens",
        "https://store.wikimedia.org/products/wikimedia-project-stickers-pack-of-14",
        "https://store.wikimedia.org/products/black-wikipedia-globe-t-shirt-with-wikipedia-written-in-multiple-languages",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("humble-echidna/product-offers").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://store.wikimedia.org/products/wikipedia-pens",
    "https://store.wikimedia.org/products/wikimedia-project-stickers-pack-of-14",
    "https://store.wikimedia.org/products/black-wikipedia-globe-t-shirt-with-wikipedia-written-in-multiple-languages"
  ]
}' |
apify call humble-echidna/product-offers --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,humble-echidna/product-offers"
        }
    }
}
```

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/80GeIsPEihYmjEP31/builds/THC2dGRGH8WTqJpuf/openapi.json
