Shopify Products Scraper, Price & Stock Monitor avatar

Shopify Products Scraper, Price & Stock Monitor

Pricing

from $20.00 / 1,000 completed store checks

Go to Apify Store
Shopify Products Scraper, Price & Stock Monitor

Shopify Products Scraper, Price & Stock Monitor

Shopify products scraper API for complete public catalogues, including variants, prices, SKUs, images and availability. Completes variant pagination instead of silently stopping at 100. Export snapshots or monitor verified price, sale, stock and catalogue changes. No API key or browser.

Pricing

from $20.00 / 1,000 completed store checks

Rating

0.0

(0)

Developer

Vadim Bezrukov

Vadim Bezrukov

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

Scrape complete public Shopify product catalogues from a store URL, including variants, prices, SKUs, images and availability. Or monitor stores for price changes, sales, stockouts, restocks, new products and variant changes. After the first baseline, scheduled runs can return only what changed. No Shopify API key, login or browser required.

Pricing: $0.02 per successful store check, $0.0005 per change event and $0.30 per 1,000 full product rows. Failed, partial, blocked and skipped stores are not charged for results.

What this is for

A catalogue export answers what a store sells now. Pricing and competitor intelligence teams need the harder answer every morning:

What changed since yesterday?

Answering it from two catalogue dumps means diffing tens of thousands of rows by hand, and getting it wrong in the ways that cost money: a pagination hiccup looks like 800 discontinued products, a currency switch looks like a catalogue-wide price cut, and the first run buries you in "new product" alerts for a catalogue that has been there for years.

This Actor handles both jobs. Use snapshot as a Shopify products scraper, or give monitor a watchlist: it retrieves the current public catalogue, compares it with the last successful check, and emits deterministic change events:

PRICE_CHANGED Wool Runner $110.00 -> $ 98.00
SALE_STARTED Tree Lounger $ 79.00 -> $ 55.30 (compare-at $79.00)
STOCK_OUT Trino Tee / Large available -> sold out
RESTOCKED Wool Piper / 10 sold out -> available
NEW_PRODUCT Wool Runner Go $135.00
PRODUCT_REMOVED Trino Tubers

Why choose this Actor

What you needWhat this Actor does
A useful alert feedEmits PRICE_CHANGED, SALE_STARTED, STOCK_OUT, RESTOCKED, product and variant events with compact before/after values
No first-run alert stormThe first successful monitor run is a BASELINE, never a catalogue full of NEW_PRODUCT events
Safe removalsVerifies complete pagination and suspicious catalogue drops before emitting PRODUCT_REMOVED or VARIANT_REMOVED
Complete variant dataFollows Shopify's variant connection past the first 100 variants, up to Shopify's 2,048-variant product limit, instead of silently truncating a product
Variant-level intelligenceTracks stable Shopify variant IDs, SKUs, options, prices, compare-at prices and public availability
A watchlist that survives failuresReturns one explicit status per store and never overwrites good state after a failed or partial check
Broad public-store coveragePrefers Shopify's Storefront GraphQL product API and falls back to public JSON where needed; no dependence on /products.json alone
Predictable recurring costQuiet monitor runs charge for successful store checks, not for re-exporting every unchanged product

Use it for competitor price tracking, sale monitoring, restock alerts, assortment changes, product-feed exports and daily ecommerce warehouse feeds. The implementation has been smoke-tested on a 50-store mixed watchlist, including international stores, multi-variant products and 25,000-product catalogues.

Choose a workflow

GoalModeProduct outputState
Monitor competitors on a schedulemonitorchangesOnlyReads and updates the last successful baseline
Export a current cataloguesnapshotallDoes not read or write monitoring state
Validate a store watchlistsnapshotchangesOnlyReturns store status rows only; no product rows or monitoring state

How monitoring works

  1. The first successful monitor run stores a BASELINE and emits no change events. Existing products are not mislabeled as new.
  2. Run the same configuration again manually or on a schedule.
  3. The Actor compares against the last complete successful state and returns only changed products, individual alert-ready events and one status row per store.

That second run is the product: a quiet daily check stays small, while a busy day tells you exactly which price, sale, product or variant moved.

Quick start

Snapshot a single store

{
"storeUrls": ["https://www.allbirds.com"],
"mode": "snapshot",
"outputMode": "all"
}

Returns every product with its variants, prices, compare-at prices and availability. Reads and writes no monitoring state.

Products with more than 100 variants are not accepted as complete after the first page. The Actor follows the variant connection until Shopify says there is no next page. Shopify currently allows up to 2,048 variants per product; the Actor's bounded pagination covers that limit and fails the store explicitly if the connection stalls, repeats a cursor or exceeds the guardrail.

Watch a competitor list

{
"storeUrls": [
"https://www.allbirds.com",
"https://kith.com",
"example-store.myshopify.com"
],
"mode": "monitor",
"outputMode": "changesOnly"
}

Need your own IDs in the output? Use the advanced stores input instead and add an externalId to each URL. The ID is echoed onto every row so results join straight to your watchlist, CRM or account ID; it is never sent to the store.

Run it daily

Save the input above as a task, then schedule it:

  • Apify Console: Actor -> Schedules -> Create schedule, cron 0 6 * * *.
  • API: POST https://api.apify.com/v2/schedules with {"cronExpression": "0 6 * * *", "actions": [{"type": "RUN_ACTOR", "actorId": "<actor-id>"}]}

State lives in a named key-value store, so consecutive scheduled runs of the same Actor continue the same monitoring history automatically.

Push changes to your own system

Add a webhook on Run succeeded. Its payload gives your endpoint the run's resource.defaultDatasetId; fetch the change-oriented columns and retain rows whose record_type is change_event:

https://api.apify.com/v2/datasets/{{resource.defaultDatasetId}}/items
?view=changes&clean=true&format=json

Dataset views select and order columns; they are not server-side predicates. Filter record_type == "change_event" in your webhook consumer. This explicit step prevents product observations and the per-store status row from being mistaken for alerts.

Every change event is a complete alert payload on its own - it carries the store, your externalId, the product, the variant, and both the before and after values. Nothing has to be looked up to act on it.

Output

Three related record types share one dataset, distinguished by record_type. The dataset ships with Change event columns, Product observation columns and Store check columns views. Views keep each use case readable in the Console; API clients should filter on record_type when they need one feed.

change_event - the alert feed

{
"record_type": "change_event",
"event_type": "PRICE_CHANGED",
"source_id": "shopify:11044168:1878194389061",
"source_url": "https://www.allbirds.com/products/mens-wool-runners-natural-white",
"detected_at": "2026-09-01T06:00:04+00:00",
"store": "www.allbirds.com",
"store_id": "11044168",
"external_id": "competitor-a",
"product_id": "1878194389061",
"variant_id": null,
"handle": "mens-wool-runners-natural-white",
"title": "Men's Wool Runner - Natural White",
"currency": "USD",
"market_country": "US",
"before": { "price_min": "110.00", "price_max": "110.00" },
"after": { "price_min": "98.00", "price_max": "110.00" },
"changed_fields": ["price_min"],
"schema_version": 1,
"fingerprint": "b0f4…"
}

RESTOCKED, when a product becomes sellable again:

{
"record_type": "change_event",
"event_type": "RESTOCKED",
"source_id": "shopify:11044168:1878194389061",
"product_id": "1878194389061",
"variant_id": null,
"before": { "available": false },
"after": { "available": true },
"changed_fields": ["available"]
}

If one variant caused the restock, the run also emits a separate VARIANT_AVAILABILITY_CHANGED event with that stable variant_id.

NEW_PRODUCT:

{
"record_type": "change_event",
"event_type": "NEW_PRODUCT",
"source_id": "shopify:11044168:9912345678901",
"handle": "wool-runner-go",
"title": "Wool Runner Go",
"before": {},
"after": {
"title": "Wool Runner Go",
"handle": "wool-runner-go",
"available": true,
"on_sale": false,
"price_min": "135.00",
"price_max": "135.00",
"variant_count": 12
}
}

product_observation - the state row

One per product, with change_type set to the most significant event on that product and event_types listing all of them. In changesOnly mode only changed products appear; in all mode every product does.

{
"record_type": "product_observation",
"source_id": "shopify:11044168:1878194389061",
"source_url": "https://www.allbirds.com/products/mens-wool-runners-natural-white",
"scraped_at": "2026-09-01T06:00:04+00:00",
"schema_version": 1,
"fingerprint": "b0f4…",
"store": "www.allbirds.com",
"store_id": "11044168",
"store_name": "Allbirds",
"external_id": "competitor-a",
"status": "SUCCESS",
"change_type": "PRICE_CHANGED",
"event_types": ["PRICE_CHANGED", "VARIANT_PRICE_CHANGED"],
"changed_fields": ["price_min", "price"],
"product_id": "1878194389061",
"handle": "mens-wool-runners-natural-white",
"title": "Men's Wool Runner - Natural White",
"vendor": "Allbirds",
"product_type": "Shoes",
"currency": "USD",
"market_country": "US",
"market_language": "EN",
"price_min": "98.00",
"price_max": "110.00",
"compare_at_price_min": null,
"compare_at_price_max": null,
"available": true,
"on_sale": false,
"variant_count": 12,
"variants": [
{
"variant_id": "31338069262480",
"title": "Men's 10",
"sku": "ST1UNNT101",
"selected_options": [{ "name": "Size", "value": "Men's 10" }],
"price": "98.00",
"compare_at_price": null,
"currency": "USD",
"available": true,
"on_sale": false,
"image": "https://cdn.shopify.com/…"
}
],
"created_at": "2019-02-14T18:07:52Z",
"published_at": "2019-02-15T09:00:00Z",
"updated_at": "2026-08-31T20:23:38Z",
"images": [{ "url": "https://cdn.shopify.com/…", "alt_text": null }],
"description": null,
"previous": { "price_min": "110.00", "price_max": "110.00", "available": true },
"error": null
}

store_check - one row per store, always

{
"record_type": "store_check",
"source_id": "shopify:11044168",
"store": "www.allbirds.com",
"store_id": "11044168",
"store_name": "Allbirds",
"external_id": "competitor-a",
"status": "SUCCESS",
"strategy": "storefront_graphql",
"catalogue_complete": true,
"incomplete_reason": null,
"currency": "USD",
"market_country": "US",
"pages_fetched": 2,
"products_observed": 293,
"previous_products": 291,
"product_delta": 2,
"is_baseline": false,
"new_products": 2,
"removed_products": 0,
"price_changes": 7,
"sale_started": 3,
"sale_ended": 1,
"stock_outs": 4,
"restocks": 9,
"variant_changes": 21,
"change_events": 47,
"error": null
}

Change events

EventFires when
BASELINEfirst successful check of a store; no change events are emitted
UNCHANGEDnothing moved (only visible with outputMode: all)
NEW_PRODUCTa product id not present in the last successful check
PRODUCT_REMOVEDa product missing from a verified complete catalogue
PRICE_CHANGEDthe normalized selling price range moved
SALE_STARTEDno variant had a discount before; at least one does now
SALE_ENDEDthe reverse
STOCK_OUTthe product had a sellable variant and now has none
RESTOCKEDthe reverse
VARIANT_ADDEDa variant id appeared
VARIANT_REMOVEDa variant id disappeared from a verified complete catalogue
VARIANT_PRICE_CHANGEDone variant's price or compare-at price moved
VARIANT_AVAILABILITY_CHANGEDone variant's availability flipped
TITLE_CHANGEDthe product title changed
DESCRIPTION_CHANGEDonly when includeDescriptions is on

A single product can emit several at once; each is its own row, and the product's observation lists all of them in event_types.

What "on sale" means here

A sale is a compare-at price strictly above the selling price. Shopify expresses "not on sale" three different ways - null, 0.0, and a compare-at equal to the price - and real stores use all three. All three normalize to absent, so a merchant setting compare_at_price = price does not show up as a discount or as a price change.

What the first run does

The first successful check of a store writes a BASELINE: every product is reported once with change_type: "BASELINE" and zero change events. An existing 5,000-product catalogue is not 5,000 new products.

A store also re-baselines - deliberately, and with a log line saying so - when the market its prices are quoted in changes, or when you change includeDescriptions or country. Both would otherwise turn a configuration change into a catalogue of phantom price movements.

Correctness guarantees

These are the behaviours the Actor is built around, each covered by tests.

A failed check is never an empty store. NO_RESULTS and SOURCE_FAILED are different outcomes. A store that genuinely has zero products is SUCCESS with products_observed: 0. A 403, a rate limit, a CDN challenge page, malformed JSON, a GraphQL protocol error or a pagination failure is FAILED, PARTIAL or ACCESS_RESTRICTED - never an empty catalogue and never a set of removals.

Removals need a verified complete catalogue. PRODUCT_REMOVED and VARIANT_REMOVED are emitted only after every page succeeded, the cursors advanced without repeating and no product appeared twice. If a store went from 840 products to 17 because pagination broke, you get a PARTIAL row explaining why - not 823 removal events.

A large drop is re-verified before it is believed. If a catalogue shrinks to half or less (and by at least 10 products), or empties entirely, the whole catalogue is fetched a second time. Removals are emitted only if the two fetches agree. If they disagree, or the second fetch fails, the store is PARTIAL and its stored state is left exactly as it was.

A failure never corrupts state. State advances only after a store returned a complete catalogue and its rows reached the dataset. PARTIAL, FAILED, ACCESS_RESTRICTED, skipped and password-protected stores leave the previous good snapshot untouched, so the next successful run diffs against real history rather than against a hole.

One broken store cannot break the batch. 200 stores in, 200 store_check rows out, each with its own status. The run fails as a whole only on invalid global configuration.

Availability is never inferred from a quantity. Shopify does not expose inventory quantities to unauthenticated callers - it denies the fields outright. So the output carries available: true|false and nothing that pretends to be a stock level.

Per-store statuses

StatusMeaning
SUCCESScomplete verified catalogue; the only status that is billed
PARTIALreal products retrieved but completeness could not be proven; no removals, state kept
FAILEDthe source could not be read; no products, state kept
ACCESS_RESTRICTEDa Shopify store that blocked us (CDN challenge, 403, sustained 429); counted with failures in the run summary and broken out as storesAccessRestricted
SKIPPED_ROBOTSthe store's robots.txt disallows the paths we would use
SKIPPED_PASSWORD_PROTECTEDthe store's Online Store channel is locked; out of scope
NOT_SHOPIFYno Shopify storefront and no Shopify markers on the site

A Shopify store that blocks the Actor is ACCESS_RESTRICTED, never NOT_SHOPIFY. Before calling anything "not Shopify" the Actor checks the storefront itself for independent evidence (the powered-by header, Shopify CDN assets, a myshopify.com reference).

How the data is retrieved

Plain HTTP only. No browser, no CAPTCHA solving, no login, no Shopify account, no merchant API credentials.

  1. Shopify Storefront GraphQL API, tokenless. Shopify's own documented public product surface. This is the primary path, and it is the reason coverage is better than a /products.json scraper: it works on Hydrogen and headless storefronts where that legacy route returns 404.
  2. /products.json. Used only where a host serves it but no Storefront API. In live testing this rescued a real store whose catalogue is reachable no other way.

Product identity uses Shopify's own stable ids: shopify:{shop_id}:{product_id} and shopify:{shop_id}:{product_id}:{variant_id}. shop_id is identical whether you enter a custom domain or the .myshopify.com domain, so a store that changes domains keeps its monitoring history. Never a row number, a page number, a title or a price.

On the fallback path the JSON route carries no shop id, so /meta.json is asked once for it. Where that answers, identity and currency match the primary path exactly; where it does not, identity falls back to the host and currency is reported as null rather than guessed.

Limitations

  • Password-protected stores are out of scope. They are detected, reported as SKIPPED_PASSWORD_PROTECTED, never charged, and never worked around.
  • Catalogues above 25,000 products cannot be reported complete. That is Shopify's own ceiling - its Storefront API refuses to paginate further. Such a store returns its first 25,000 products as PARTIAL with an explicit reason, and never generates removals. Two of the fifty stores tested hit this.
  • No inventory quantities. Availability only. See above.
  • A store reached only through /products.json may report no currency. That route carries none, and if the store serves no /meta.json either, prices are returned with currency: null rather than an assumed one. Check strategy on the store_check row to see which path a store used.
  • No customer, cart, checkout, order, account or admin data. Public catalogue information exclusively.
  • Descriptions are off by default. They are the largest field in a catalogue and change for editorial reasons. Turn on includeDescriptions if you want DESCRIPTION_CHANGED.
  • Image URLs are reported but never diffed. Shopify's CDN rewrites their version string without the picture changing, so diffing them would be pure noise.
  • A store's default market depends on where the run originates. Set country to pin it. The market actually applied is on every record as market_country / market_language / currency.

robots.txt

Every entry in a watchlist is a different merchant's domain, so robots.txt is fetched once per host per run and honoured with correct user-agent grouping. If the paths the Actor would use are disallowed, the store is reported SKIPPED_ROBOTS and nothing is fetched. Crawl-delay is applied when the matching group sets one. This is not hypothetical: one of the fifty stores tested disallows /api/ for all crawlers, and is skipped.

Requests identify themselves as ShopifyPriceStockMonitor.

Proxy

Off by default, and a proxy will probably not help. Every store that responded at all during testing responded over direct HTTP, and the ones that did not were re-tested through Apify's residential proxy: all four came back ACCESS_RESTRICTED again, with the same errors.

That result is not surprising once you look at why those stores fail. Most are headless storefronts that simply do not serve the Storefront API at the domain you typed, so there is no route for a different IP to reach. The rest answer with a bot challenge rather than an IP-reputation block. Neither is something a proxy fixes.

proxyConfiguration is still exposed, because a proxy is occasionally the right tool for a specific store or a specific network, and because some users are required to route egress through one. But treat ACCESS_RESTRICTED as "this host does not serve us", not as "try harder with a better IP".

The Actor parallelises across stores (five at a time) and stays strictly sequential within a store, with a minimum interval per host. It never bursts a single merchant.

Performance and cost

Two watchlists, run end to end with the shipped defaults - direct HTTP, no proxy, five stores at a time:

WatchlistStoresProductsRequestsTrafficWall clock
10 mid-size stores104,6743624 MB13 s
50-store mixed fleet5084,551454438 MB9 min

The fleet number is worth unpacking, because two outliers dominate it. Two 25,000-product stores took 1,050 of the run's 1,454 store-seconds, and because the run is only as fast as its slowest member, they set the whole 9-minute wall clock. The other 48 stores - 34,551 products between them - took 405 store-seconds, about 80 seconds of wall clock at five-way concurrency. Catalogue sizes are heavily skewed: across the fleet the median store had 293 products and the 90th percentile 2,624, so a watchlist normally costs far less than its largest member suggests.

Per-unit figures, measured over 74,871 products in 35 stores:

MetricValue
Requests per 1,000 products4.2 (250 products per request, the API maximum)
Traffic per 1,000 products0.9 - 8.3 MB, median 2.6 MB
Time per 1,000 products (one store, sequential)~10 s
Retries / sustained throttling across the fleet3 retries, none

Traffic scales with variants, not products. A 1,000-product store with one variant each costs 0.9 MB; one with 24 variants each costs 8.3 MB. Product count alone will mislead you when sizing a run. Two settings move it further:

SettingTraffic per 1,000 products
includeImages: false~2.0 MB
default (images on)~4.4 MB
images + includeDescriptions: true~5.0 MB

Memory tracks the largest catalogues in flight, not the length of the watchlist: each store's rows are written out and released as it finishes, so a 200-store run holds no more than a 5-store one. Measured peaks were 77 MB for the 10-store watchlist and 1.4 GB for the fleet, the latter set by two 25,000-product catalogues being processed at the same time. The default 2 GB covers that worst case; 1 GB is comfortable for watchlists without extreme-catalogue stores.

A monitoring run costs the same to fetch whether anything changed or not - Shopify offers no "changed since" filter on the public API - but far less to deliver, because changesOnly emits nothing for products that did not move. A second live pass over five stores minutes after the first returned five store rows, zero product rows and zero events.

Pricing

Pay per event, priced on what was actually delivered:

EventPriceCharged for
store-check$0.02one per store that returned a complete, verified catalogue
product-change$0.0005one per change event delivered
product-observations-1k$0.30one per 1,000 full product rows delivered
apify-actor-start$0.00005 per GBonce per run, for the container start

The three result events are charged only for a store that returned a complete, verified catalogue. Nothing else is billed: not retries, and not PARTIAL, FAILED, ACCESS_RESTRICTED, password-protected, robots-skipped or non-Shopify stores - including their products and their change events, which are still delivered in full. A store named twice in one watchlist is fetched once, charged once, and returned under both external IDs.

The start event is the one exception, and it is the platform's standard run overhead: at the default 2 GB it is $0.0001 per run. A run in which every single store failed therefore costs a hundredth of a cent, and nothing else.

Why this shape rather than per product: a successful check is worth something even when nothing changed, and a daily monitor should not cost the same as a full export every single day. Two illustrative runs:

  • 50-store daily monitor, quiet day - 50 checks, ~120 changes: $1.00 + $0.06 + $0.0001 = $1.06.
  • 50-store full snapshot, 40,000 products - 50 checks, 40,000 observations: $1.00 + $12.00 + $0.0001 = $13.00.
  • A run where every store was blocked - 0 checks: $0.0001.

Why change-only monitoring costs less

This pricing is designed around the recurring job, not around winning a short-lived "cheapest scraper" claim. A quiet 50-store daily watchlist costs about $1.05 in the default changesOnly mode: successful store checks plus roughly 100 actual changes. Returning all ~15,000 products from the same watchlist would cost about $5.50 instead. That is roughly 80% less when what you need is the change feed, using the same Actor and the same current prices.

A one-off snapshot remains inexpensive at $0.30 per 1,000 product rows. The larger saving comes from not buying those rows again every day when nothing in them changed.

One thing to be deliberate about: outputMode: all in monitor mode returns every product on every run, so it re-bills the whole catalogue daily and costs about five times the default. That is the pattern this Actor exists to replace. Use all for a one-off export, and leave the default in place for monitoring.

Input reference

FieldTypeDefaultNotes
storeUrlsstring listemptyFast start: paste storefront URLs or domains, one per line
storesobject arrayemptyAdvanced entries shaped as { "url": …, "externalId": … }; combined limit with storeUrls is 200
modemonitor | snapshotmonitorsnapshot neither reads nor writes state
outputModechangesOnly | allchangesOnlyIn monitor, returns the baseline, changes and store rows. In snapshot, returns store rows only. all returns every product.
includeImagesbooleantrueup to 10 image URLs per product; never diffed
includeDescriptionsbooleanfalseenables DESCRIPTION_CHANGED; changing it re-baselines
countryISO 3166-1 alpha-2nonepins the market prices are quoted in; changing it re-baselines
proxyConfigurationobjectoffonly for stores reported ACCESS_RESTRICTED

Call it from your own code

Run it and get the rows back in one request:

curl -X POST "https://api.apify.com/v2/acts/automa-flow~shopify-price-stock-monitor/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"stores": [
{ "url": "https://www.allbirds.com", "externalId": "competitor-a" }
],
"mode": "monitor",
"outputMode": "changesOnly"
}'

Fetch the change-oriented columns from a finished run:

curl "https://api.apify.com/v2/datasets/DATASET_ID/items?view=changes&clean=true&format=json" \
-H "Authorization: Bearer $APIFY_TOKEN"

Then filter client-side on record_type == "change_event". In the Console, the Change event columns view selects the useful alert fields but intentionally keeps record_type visible for the same reason. Every record carries source_id, scraped_at, schema_version and fingerprint, so rows from different runs join to each other without any extra bookkeeping on your side.

Use it from an AI agent through MCP

Apify's hosted MCP server can expose this public Actor as a tool with structured input and output. Add this server URL to an MCP-compatible client and sign in through Apify when prompted:

https://mcp.apify.com?tools=automa-flow/shopify-price-stock-monitor

Example prompt for a one-off acquisition workflow:

Run automa-flow/shopify-price-stock-monitor in snapshot mode with all product
output for these Shopify store domains. Compare price ranges, sale status,
availability and variant counts, and cite each product's source_url.

For a recurring agent workflow, use mode: monitor and outputMode: changesOnly, then act only on record_type: change_event. A first run is a BASELINE, not evidence that every product was newly launched.

Run summary

RUN_SUMMARY in the default key-value store carries per-run economics and counts: stores requested, successful, partial, failed and skipped; products observed; every event type's count; suspicious catalogue drops; requests, retries, 429s, 403s and bytes; and how many state snapshots were saved versus retained.

The Storefront API is Shopify's documented public product surface, and this Actor reads public catalogue data only: products, variants, prices, compare-at prices, availability, images. It collects no personal data of any kind. It does not touch customer, cart, checkout, order, account or admin endpoints, does not authenticate, and does not bypass any access control - password-protected stores are detected and skipped rather than opened. Per-merchant robots.txt is fetched and honoured for every host on every run.

You are responsible for your own use of the data, including any terms you have agreed to with a specific merchant.