Product Price & Stock Monitor: any shop's schema.org data
Pricing
from $1.05 / 1,000 products
Product Price & Stock Monitor: any shop's schema.org data
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.
Pricing
from $1.05 / 1,000 products
Rating
0.0
(0)
Developer
Michael Costa
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
9 hours ago
Last modified
Categories
Share
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).
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).
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.
- 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. - 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. - In Apify Console, open Schedules, click Create new, set how often in Schedule setup (for example daily at 07:00), then Add your task.
- 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.defaultDatasetIdis the run's dataset, andGET https://api.apify.com/v2/datasets/<defaultDatasetId>/items?format=json(with your API token) returns the changed products.
- Slack: click Configure, sign in, pick the workspace and channel, and the "run succeeded" event. A
useful message:
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); null for URLs you type in. |
One row per product page. The full list is under 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
- Open Product Price & Stock Monitor and click Try for free (or Start if you're signed in).
- Paste product page URLs into Product page URLs, or a shop's address into Or: whole stores.
- Optional: URL patterns and Max pages per store for stores, and Only changed prices and stock since the last run for scheduled monitoring.
- 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:
{"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):
{"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. |
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, withstatusandreason: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
- Save your input as a task and add it to a schedule (Console → Schedules).
- Collect results: download the dataset, call
GET https://api.apify.com/v2/actor-tasks/<task id>/runs/last/dataset/items?status=SUCCEEDED&format=csvwith 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 run, any list of product links; any actor, ours or not), chain it with an integration:
- 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.
- 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,linkorloadedUrl(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
sourceTitleandsourceIndex(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_sourceare dropped first); they're counted inRUN_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_RETURNEDwith 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_STATSandNOT_RETURNEDsay 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
blockedand 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;offerCounthas 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: nullrather 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_RETURNEDasnot-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).
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 | 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). |
| Website Technology Detector | You want to know which shops run Shopify, WooCommerce or Magento before monitoring them. |
| Website & 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.