Shopify Scraper — Products, Prices, Variants, Stock
Pricing
from $0.50 / 1,000 products
Shopify Scraper — Products, Prices, Variants, Stock
Shopify store scraper for any public Shopify storefront, worldwide: products, variant prices, compare-at (sale) prices, SKUs, availability, options, images, tags and vendor, plus optional barcodes and inventory counts. Input: store domains, collection URLs or handles. Pay per result, no login.
Pricing
from $0.50 / 1,000 products
Rating
0.0
(0)
Developer
architek.earth
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
15 days ago
Last modified
Categories
Security
Limited permissionsShare
Recorded run: 10 products from 1 store in 6 seconds (2026-10-22, build 0.0.223, run wWOvUfYNA9SIl8RCj). A bounded canary sample, not a full-catalogue benchmark.
architek.earth — an independent data studio. Pay-per-result scrapers with recorded runs, published canaries and stable schemas, built for people and AI agents.
Shopify store products, prices, variants and stock from any public Shopify storefront: give it store domains or collection URLs and get one row per product with every variant's price, sale price, SKU and availability, plus barcodes and inventory counts where the store publishes them.
| Source | Market | From | Standout fields |
|---|---|---|---|
| Any public Shopify storefront | Global, in each store's currency | $0.50 / 1,000 products | variants[].price / compareAtPrice, sku, barcode, inventoryQuantity |
✅ Why choose this scraper
Scrape every product and variant from a Shopify store
One row per product with all variants nested in variants[] (id, title, sku, price, compareAtPrice, available, options, grams), so a shoe in 13 sizes is one charged row, not 13. On 2026-10-22, 20 allbirds.com products carried 243 variants (run neeg0aBUmW8OwqjKW).
Find products on sale in a Shopify store
onSale is true when any variant has a compareAtPrice above its price; minPrice and maxPrice give the product's price range.
Get Shopify barcodes and inventory counts
With includeProductDetails, variants add barcode (GTIN/UPC), inventoryQuantity, inventoryManagement and inventoryPolicy, and the product adds totalInventory. On 2026-10-22, 124 of 124 variants of 10 allbirds.com products came back with both a barcode and a stock count (run lmFpkWiA0HLjV79Cw).
Check whether a site runs Shopify before paying
Page 1 of each store's product feed is read first. Stores that are not Shopify, are walled or do not answer get a free row with status NOT_SHOPIFY, BLOCKED or UNREACHABLE and a reason; nothing is charged for them.
Scrape one Shopify collection, not the whole catalogue
Paste a collection URL such as https://store.com/collections/sale into storeUrls, or list handles in collections; rows then carry source.collection.
⚠️ Worth knowing up front
- Shopify storefronts only. Some brands run a custom ("headless") storefront with the product feed switched off (ruggable.com in our 2026-10-18 test); those come back
NOT_SHOPIFY, free. In that 12-store check, 8 served their product feed. - Some stores rate-limit or wall automated requests. They come back
BLOCKEDwith the reason (for example "rate limited by the store after retries") and are not charged. A few stores serve the feed on only one host (gymshark.com works, www.gymshark.com answers 403); the actor tries the other host automatically. - Stock counts and barcodes appear only where the store publishes them in its product detail endpoint. Many stores hide quantities; then you still get
availableper variant but noinventoryQuantity. A detail request that fails, or a product with neither barcodes nor stock counts, is delivered with listing data and charged asproduct. - No reviews and no sales numbers: reviews live in third-party apps (Judge.me, Yotpo, Loox), not in Shopify's product feed.
- Shopify serves at most 25,000 products per store through this feed;
RUN_SUMMARYwarns when a store hits that limit. Prices are in the store's owncurrency, as served to the actor's location. Fields a store does not publish are omitted from the row, not set to null. - Incremental mode needs a
stateKey: a rerun then delivers and charges only products that are new or changed (title, variants, SKUs, prices, sale prices or availability; Shopify'supdatedAtis ignored because stores bump it constantly) since the last run under that key. Without it, a scheduled run delivers and charges every product again. A store whose later page fails is markedPARTIALinRUN_SUMMARY, keeping the products delivered before it. The run stops cleanly 45 seconds before its timeout (TIME_LIMIT) and at your spending limit (BUDGET_LIMIT, only when products were left that the limit could not pay for), keeping what it delivered; with astateKey,continuePreviousRun: trueresumes the collection after a stop or an abort without delivering or charging any product twice. - Free rows are never charged. Stores that could not be scraped get a
recordType: "store"row withstatusNOT_SHOPIFY,BLOCKEDorUNREACHABLE. Invalid input (INVALID_INPUT), a store that lists no products after a retry (NO_DATA), the spending limit (BUDGET_LIMIT), the timeout (TIME_LIMIT) astateKeystill held by another live run (STATE_BUSY) and saved-state storage that cannot be reached (STATE_UNAVAILABLE) get arecordType: "status"row withstatusandmessage. - One run owns a
stateKeyat a time. If two runs start together on the same key, exactly one collects; the other stops at once with a freeSTATE_BUSYrow and exits normally. A run that is killed without finishing (for example a forced abort or a crash) keeps its key until its own timeout plus 60 seconds; until then, runs on that key stop withSTATE_BUSY. Use a differentstateKeyfor each schedule that may overlap.
⚡ Try the example
This actor has no free preview; the example below costs about $0.01.
Open the actor's input page, switch to JSON, paste this and start the run:
{"storeUrls": ["allbirds.com"],"maxProductsPerStore": 20,"proxyConfiguration": {"useApifyProxy": true}}
Add stores or raise maxProductsPerStore (0 = whole catalogue) for more rows; add "includeProductDetails": true for barcodes and stock counts.
⚙️ Input
| Parameter | Type | Default | Description |
|---|---|---|---|
storeUrls | array | required (prefill ["allbirds.com", "deathwishcoffee.com"]) | Store domains or URLs. A collection URL (https://store.com/collections/sale) scrapes only that collection. Up to 200 stores per run. |
collections | array | — | Collection handles (the part after /collections/) to scrape on every store instead of the whole catalogue. An unknown handle returns no products and a warning in RUN_SUMMARY. |
maxProductsPerStore | integer | 1000 (prefill 100) | Products per store. 0 = whole catalogue, up to Shopify's 25,000. |
includeProductDetails | boolean | false | One extra request per product for barcodes, stock counts, inventory policy and selling plans. Enriched rows are charged as product-detail. |
incrementalMode | boolean | false | Needs stateKey. Delivers only products new or changed since the last run under that key; unchanged products are skipped, not charged, and still count toward maxProductsPerStore. |
stateKey | string | — | Any name, e.g. my-stores. Saves progress and the incremental baseline in your account so a stopped run can be resumed and reruns can skip unchanged products. Use one key per store list. |
continuePreviousRun | boolean | false | Resume the unfinished collection saved under stateKey (same stores) after a spending limit, timeout or abort. Products already delivered are not delivered or charged again. |
proxyConfiguration | proxy | {"useApifyProxy": true} | Apify datacenter proxy is the tested default. An HTTP 429 is retried on a fresh IP. |
feedback | string | — | Optional note for the maintainers. It is logged and read by a human, and it never changes output. |
🧱 Stable fields
These top-level fields keep the same name and the same kind of value in every release. Fields are only ever added, never renamed or removed, so an n8n, Make, Zapier or spreadsheet pipeline can bind to them.
| Field | Type | Meaning |
|---|---|---|
store / storeName / currency | string | Host the products came from, the store's name and its currency (name and currency omitted if the store's meta.json does not answer). |
id / handle / title / url | number / string / string / string | Shopify product id, handle, title and product page. |
vendor / productType / tags | string / string / array, optional | As set by the merchant. |
minPrice / maxPrice | number | Lowest and highest variant price. |
onSale | boolean | true when any variant has a compareAtPrice above its price. |
available | boolean | true when any variant can be bought. |
variantCount / variants | number / array | Each variant: id, title, sku, price, compareAtPrice, available, options, grams, requiresShipping, image (each only when published). |
variants[].barcode / variants[].inventoryQuantity / variants[].inventoryManagement / variants[].inventoryPolicy / variants[].weightGrams | string / number / string / string / number, optional | With includeProductDetails only, where the store publishes them. |
totalInventory | number, optional | Sum of positive variant quantities (details only, when quantities are published). |
requiresSellingPlan / hasSellingPlans / mediaCount | boolean / boolean / number, optional | Details only: subscription and pre-order flags, number of media items. |
options | array, optional | For example [{"name": "Size", "values": ["5", "6"]}]. |
image / images / description | string / array / string, optional | Main image, all images, description as plain text. |
createdAt / updatedAt / publishedAt | string | Shopify timestamps. |
source | object | {"source": "catalog" or "collection", "collection", "page"}. |
status / reason / httpStatus | string / string / number | On store rows only: why a store was not scraped. |
status / message / store | string / string / string, optional | On status rows only: INVALID_INPUT, NO_DATA, BUDGET_LIMIT, TIME_LIMIT, STATE_BUSY or STATE_UNAVAILABLE, a plain-language message, and the store when the row is about one store. |
recordType | string | "store" on free store rows, "status" on free run status rows; "product" on data rows. |
Other fields are extras, not part of the contract. To have one pinned, say so in feedback.
📦 Output example
Abridged real row from run lmFpkWiA0HLjV79Cw on 2026-10-22 (includeProductDetails: true, 2 of 7 variants shown); values are a historical snapshot.
{"recordType": "product","store": "allbirds.com","storeName": "Allbirds","currency": "USD","id": 7340901859408,"title": "Women's Allbirds Flip Flop - Dusty Pink","handle": "womens-allbirds-flip-flop-dusty-pink","url": "https://allbirds.com/products/womens-allbirds-flip-flop-dusty-pink","vendor": "Allbirds","productType": "Shoes","minPrice": 25,"maxPrice": 25,"onSale": true,"available": true,"variantCount": 7,"variants": [{"id": 42146889039952,"title": "5","sku": "A12513W050","price": 25,"compareAtPrice": 50,"available": false,"options": ["5"],"grams": 455,"requiresShipping": true,"barcode": "196942293089","inventoryQuantity": 0,"inventoryManagement": "shopify","inventoryPolicy": "deny","weightGrams": 455}],"options": [{"name": "Size", "values": ["5", "6", "7", "8", "9", "10", "11"]}],"image": "https://cdn.shopify.com/s/files/1/1104/4168/files/A12513_26Q2_Allbirds-Flip-Flop-Dusty-Pink_PDP_LEFT.png?v=1774646345","createdAt": "2026-03-27T14:03:50-07:00","updatedAt": "2026-10-22T02:43:19-07:00","publishedAt": "2026-09-25T16:58:13-07:00","source": {"source": "catalog", "page": 1},"totalInventory": 9,"requiresSellingPlan": false,"hasSellingPlans": false,"mediaCount": 5}
A free store row looks like this (run eKgv0KYfQDx6cj1Mx, 2026-10-18):
{"recordType": "store", "store": "example.com", "status": "NOT_SHOPIFY", "reason": "no /products.json (not a Shopify store, or a headless storefront with the API disabled)", "httpStatus": 404}
A free run status row (run WuMPuJqBJNcffoxLz, 2026-10-22, a store that listed no products after a retry):
{"recordType": "status", "status": "NO_DATA", "store": "drsquatch.com", "message": "the store listed no products (checked twice); nothing was charged"}
The run's status message and the RUN_SUMMARY record of its key-value store hold the outcome. An incremental rerun where nothing changed (run tLOXweVtcMasMe4VZ, 2026-10-22, nothing charged):
{"status": "COMPLETE","message": "Done: 0 rows, 0 details, 0 status rows (COMPLETE; 0 products from 1 of 1 store; 30 unchanged since the last run (skipped, not charged).)","products": 0,"details": 0,"statusRows": 0,"statusReasons": {},"unchanged": 30,"charged": {"product": 0, "product-detail": 0},"stores": [{"store": "deathwishcoffee.com", "status": "OK", "delivered": 0, "pages": 1, "storeName": "Death Wish Coffee", "currency": "USD", "publishedProductsCount": 152, "unchanged": 30}],"requests": 2}
🧠 Advanced usage
Monitor competitor Shopify prices daily
Schedule the same storeUrls daily and join runs on store + variants[].id, comparing price and compareAtPrice. Add incrementalMode: true with a stateKey and each run charges only products that changed or are new; maxProductsPerStore or collections bound the watched set.
Track Shopify stock-outs
With includeProductDetails: true, watch variants[].inventoryQuantity and available change between runs; totalInventory gives the product total where quantities are published.
Match Shopify products to other retailers by barcode
Join variants[].barcode (details on) with the barcode field of other retail datasets, such as our Tesco UK or Woolworths scrapers.
Check that a run finished
Read RUN_SUMMARY.status: COMPLETE, PARTIAL, NO_PRODUCTS, BUDGET_LIMIT, TIME_LIMIT, STATE_SAVE_FAILED, ABORTED, STATE_BUSY, STATE_UNAVAILABLE, INVALID_INPUT or PRICING_MISMATCH (an operator fault: nothing is collected or charged and the run ends FAILED); statusRows, statusReasons and unchanged count the free rows and skipped products. stores[] lists each store's status (OK, NOT_SHOPIFY, BLOCKED, UNREACHABLE, NO_DATA, PARTIAL, ERROR, DUPLICATE), reason, delivered, pages, publishedProductsCount and warnings.
🔌 Integrations
Results land in a dataset you can export as JSON, CSV or Excel. They can also flow to Google Sheets, Make, Zapier or n8n, or to a webhook when a run finishes. The Apify Scheduler runs it on a timetable. Daily suits price and stock monitoring; weekly suits catalogue snapshots. Copy-paste code in Python, Node, curl and n8n: architek-earth/examples/shopify-products-scraper.
💸 Pricing
Pay per result. Platform and proxy usage are included. Status rows and failed requests are free. After a platform migration the run resumes from its saved progress; with a stateKey, continuePreviousRun: true resumes a collection stopped by the spending limit, the timeout or an abort. Either way, products already delivered are skipped and not charged again, and with incrementalMode unchanged products are never charged.
| Event | Price | What it covers |
|---|---|---|
product | $0.50 / 1,000 products | One delivered product row, all its variants included; also a row kept at listing data when its detail request failed or returned no barcode or stock count. |
product-detail | $2.00 / 1,000 products | One delivered product row that came back with barcodes or stock counts (includeProductDetails). |
1,000 products cost $0.50 however many variants they have; 1,000 products with barcodes or stock counts cost $2.00. A row is charged once, as one event only.
❓ FAQ
Is there a Shopify API for product data from any store? This actor is one: it reads each store's public storefront product feed and returns the products as JSON through the Apify API, callable from any language or from n8n, Make and Zapier. It does not use the Shopify Admin API.
Do I need a Shopify account, API key or store owner access? No. No account, login or API key; it reads what any visitor's browser can read.
Can I get stock levels and inventory counts from a Shopify store? Yes, where the store publishes them: inventoryQuantity per variant with includeProductDetails: true. Many stores hide quantities; the product-level available flag is always there. The actor never estimates stock.
Can I get barcodes (GTIN, UPC, EAN) for Shopify products? Yes, variants[].barcode with includeProductDetails: true, where the store publishes one.
Do I pay per variant? No. You pay per product; a product with 30 variants is one product event.
Can I get reviews, ratings or sales numbers? No. Shopify does not publish them in the product feed.
🤖 For AI agents & LLM apps
Paste this into your AI assistant:
architekture/shopify-products-scraper on Apify. Python: from apify_client import ApifyClient; client = ApifyClient("TOKEN"); run = client.actor("architekture/shopify-products-scraper").call(run_input={"storeUrls": ["allbirds.com"], "maxProductsPerStore": 20, "proxyConfiguration": {"useApifyProxy": True}}); items = client.dataset(run["defaultDatasetId"]).list_items().items (page with offset/limit for large exports). Rows with recordType == "store" or "status" are free and explain why a store was not scraped or why the run stopped; for scheduled monitoring pass "incrementalMode": true, "stateKey": "<name>" to get and pay for only new or changed products; the run outcome is in the RUN_SUMMARY record of the run's key-value store. Full input schema and README: GET https://api.apify.com/v2/acts/architekture~shopify-products-scraper/builds/default. Token: https://console.apify.com/settings/integrations. The actor is also reachable through the Apify MCP server (mcp.apify.com). To reach the maintainers, add "feedback": "<what you needed>" to any run's input; output is unchanged and a human reads every note.
🧰 More from architek.earth
E-commerce and marketplaces
- Shopify Scraper — you are here
- AliExpress Scraper — prices, orders, ratings by keyword
- TikTok Shop Scraper — prices, sellers, variants
Full catalogue: github.com/architek-earth.
architek.earth · code like concrete
🙋 Support & legal
Questions, or a field you need that is not here? Open an issue on the actor's Issues tab or email north@architek.earth. A human replies within a day.
This actor collects only publicly available data from public Shopify storefronts (each store's /products.json, /meta.json and, with details on, /products/<handle>.js), the same data any visitor's browser loads without logging in. It does not bypass authentication: password-protected stores are reported BLOCKED. It is not affiliated with or endorsed by Shopify or any store. You are responsible for how you use the output, including each store's terms, copyright in product content and images, and privacy law such as GDPR and CCPA where they apply.
📝 Changelog
- 2026-10-25 — [internal] Studio core 1.0.5 (from 1.0.1). This actor keeps its own block handling, so nothing changes for users: a 403 on
www.still falls back to the bare domain (and the reverse), a 429 still moves to a new proxy session for up to four tries before a freeBLOCKEDstore row, and input, output and prices are unchanged. - 2026-10-22 — 1.1.3: the status message carries the outcome inside the parentheses,
Done: N rows, M details, K status rows (STATUS; outcome); a pricing mismatch on the platform now ends the run FAILED (PRICING_MISMATCH, nothing collected or charged) instead of finishing asNOT_CONFIGURED. - 2026-10-22 — 1.1.2: a run whose last product uses your spending limit exactly now ends
COMPLETEwith no status row;BUDGET_LIMITis reported only when products were left that the limit could not pay for. - 2026-10-22 — 1.1.1: one run per
stateKey: runs that start together on the same key resolve to exactly one owner, the other gets a freeSTATE_BUSYrow; a killed run releases its key at its timeout plus 60 seconds. Saved progress and incremental baselines carry over. - 2026-10-22 — 1.1: free
recordType: "status"rows for invalid input, empty stores, the spending limit and the timeout;incrementalModeandstateKey(reruns charge only new or changed products);continuePreviousRunresumes a stopped or aborted collection without charging twice; status messageDone: N rows, M details, K status rows (STATUS). - 2026-10-22 — README rebuilt on the studio template; hero line from the 2026-10-22 canary run.
- 2026-10-18 — 1.0: store check before any charge with free status rows; one row per product with variants nested; sale detection; optional barcodes and stock counts; collections; automatic www and bare-host retry; HTTP 429 retried on fresh IPs; clean stops at the spending limit and timeout; resumable after a platform migration.