Shopify Products Scraper, Price & Stock Monitor
Pricing
from $20.00 / 1,000 completed store checks
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
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.00SALE_STARTED Tree Lounger $ 79.00 -> $ 55.30 (compare-at $79.00)STOCK_OUT Trino Tee / Large available -> sold outRESTOCKED Wool Piper / 10 sold out -> availableNEW_PRODUCT Wool Runner Go $135.00PRODUCT_REMOVED Trino Tubers
Why choose this Actor
| What you need | What this Actor does |
|---|---|
| A useful alert feed | Emits PRICE_CHANGED, SALE_STARTED, STOCK_OUT, RESTOCKED, product and variant events with compact before/after values |
| No first-run alert storm | The first successful monitor run is a BASELINE, never a catalogue full of NEW_PRODUCT events |
| Safe removals | Verifies complete pagination and suspicious catalogue drops before emitting PRODUCT_REMOVED or VARIANT_REMOVED |
| Complete variant data | Follows 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 intelligence | Tracks stable Shopify variant IDs, SKUs, options, prices, compare-at prices and public availability |
| A watchlist that survives failures | Returns one explicit status per store and never overwrites good state after a failed or partial check |
| Broad public-store coverage | Prefers Shopify's Storefront GraphQL product API and falls back to public JSON where needed; no dependence on /products.json alone |
| Predictable recurring cost | Quiet 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
| Goal | Mode | Product output | State |
|---|---|---|---|
| Monitor competitors on a schedule | monitor | changesOnly | Reads and updates the last successful baseline |
| Export a current catalogue | snapshot | all | Does not read or write monitoring state |
| Validate a store watchlist | snapshot | changesOnly | Returns store status rows only; no product rows or monitoring state |
How monitoring works
- The first successful
monitorrun stores aBASELINEand emits no change events. Existing products are not mislabeled as new. - Run the same configuration again manually or on a schedule.
- 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/scheduleswith{"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
| Event | Fires when |
|---|---|
BASELINE | first successful check of a store; no change events are emitted |
UNCHANGED | nothing moved (only visible with outputMode: all) |
NEW_PRODUCT | a product id not present in the last successful check |
PRODUCT_REMOVED | a product missing from a verified complete catalogue |
PRICE_CHANGED | the normalized selling price range moved |
SALE_STARTED | no variant had a discount before; at least one does now |
SALE_ENDED | the reverse |
STOCK_OUT | the product had a sellable variant and now has none |
RESTOCKED | the reverse |
VARIANT_ADDED | a variant id appeared |
VARIANT_REMOVED | a variant id disappeared from a verified complete catalogue |
VARIANT_PRICE_CHANGED | one variant's price or compare-at price moved |
VARIANT_AVAILABILITY_CHANGED | one variant's availability flipped |
TITLE_CHANGED | the product title changed |
DESCRIPTION_CHANGED | only 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
| Status | Meaning |
|---|---|
SUCCESS | complete verified catalogue; the only status that is billed |
PARTIAL | real products retrieved but completeness could not be proven; no removals, state kept |
FAILED | the source could not be read; no products, state kept |
ACCESS_RESTRICTED | a Shopify store that blocked us (CDN challenge, 403, sustained 429); counted with failures in the run summary and broken out as storesAccessRestricted |
SKIPPED_ROBOTS | the store's robots.txt disallows the paths we would use |
SKIPPED_PASSWORD_PROTECTED | the store's Online Store channel is locked; out of scope |
NOT_SHOPIFY | no 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.
- 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.jsonscraper: it works on Hydrogen and headless storefronts where that legacy route returns 404. /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
PARTIALwith 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.jsonmay report no currency. That route carries none, and if the store serves no/meta.jsoneither, prices are returned withcurrency: nullrather than an assumed one. Checkstrategyon thestore_checkrow 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
includeDescriptionsif you wantDESCRIPTION_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
countryto pin it. The market actually applied is on every record asmarket_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:
| Watchlist | Stores | Products | Requests | Traffic | Wall clock |
|---|---|---|---|---|---|
| 10 mid-size stores | 10 | 4,674 | 36 | 24 MB | 13 s |
| 50-store mixed fleet | 50 | 84,551 | 454 | 438 MB | 9 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:
| Metric | Value |
|---|---|
| Requests per 1,000 products | 4.2 (250 products per request, the API maximum) |
| Traffic per 1,000 products | 0.9 - 8.3 MB, median 2.6 MB |
| Time per 1,000 products (one store, sequential) | ~10 s |
| Retries / sustained throttling across the fleet | 3 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:
| Setting | Traffic 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:
| Event | Price | Charged for |
|---|---|---|
store-check | $0.02 | one per store that returned a complete, verified catalogue |
product-change | $0.0005 | one per change event delivered |
product-observations-1k | $0.30 | one per 1,000 full product rows delivered |
apify-actor-start | $0.00005 per GB | once 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
| Field | Type | Default | Notes |
|---|---|---|---|
storeUrls | string list | empty | Fast start: paste storefront URLs or domains, one per line |
stores | object array | empty | Advanced entries shaped as { "url": …, "externalId": … }; combined limit with storeUrls is 200 |
mode | monitor | snapshot | monitor | snapshot neither reads nor writes state |
outputMode | changesOnly | all | changesOnly | In monitor, returns the baseline, changes and store rows. In snapshot, returns store rows only. all returns every product. |
includeImages | boolean | true | up to 10 image URLs per product; never diffed |
includeDescriptions | boolean | false | enables DESCRIPTION_CHANGED; changing it re-baselines |
country | ISO 3166-1 alpha-2 | none | pins the market prices are quoted in; changing it re-baselines |
proxyConfiguration | object | off | only 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 productoutput 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.
Legal and data handling
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.