Shopify Store Scraper - Prices, Stock & Change Tracking avatar

Shopify Store Scraper - Prices, Stock & Change Tracking

Pricing

from $0.80 / 1,000 products

Go to Apify Store
Shopify Store Scraper - Prices, Stock & Change Tracking

Shopify Store Scraper - Prices, Stock & Change Tracking

Extract the full public product catalog of any Shopify store: products, variants, SKUs, prices, compare-at prices, stock and images. Built-in change tracking flags new, removed, price-changed and restocked products between runs.

Pricing

from $0.80 / 1,000 products

Rating

0.0

(0)

Developer

Cedarfetch Labs

Cedarfetch Labs

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

42 minutes ago

Last modified

Categories

Features

Limited permissions

Share

Extract the complete public product catalog of any Shopify store: products, variants, SKUs, prices, compare-at (sale) prices, stock status, images, tags, vendors and descriptions. Run it on a schedule with change tracking on and every row tells you what happened since the last run: new product, price changed, back in stock, out of stock, unchanged, or removed.

It reads only the public catalog endpoints that every Shopify storefront serves to every visitor (/products.json, /collections/.../products.json, /products/<handle>.js, /meta.json, /sitemap.xml). No browser, no login, no proxies needed for normal stores, so runs are fast and cheap.

What you can use it for

  • Price monitoring: track competitor prices and discounts per SKU, get only the changes each day.
  • Stock and restock alerts: see which products sold out or came back.
  • New product launches: catch new arrivals the day they appear.
  • Catalog research and dropshipping: export a full store catalog to a spreadsheet in one click.
  • Market and assortment analysis: vendors, product types, tags, price ranges across many stores.
  • AI agents and pipelines: clean, typed JSON with stable field names.

Why this one

  • Change tracking built in. Other Shopify scrapers give you a snapshot and leave the diffing to you. This Actor keeps a compact snapshot per store in your account and labels every row, including rows for products that disappeared. Turn on "Output only changed products" and you pay only for what changed.
  • Complete catalogs. Shopify serves at most 25,000 products through /products.json (100 pages of 250). Above that, and on stores that disable /products.json, the Actor automatically continues through the store's sitemap and per-product endpoints.
  • Clean, typed output. Prices are numbers, not strings. Descriptions come as plain text and as the original HTML. Sale price, discount percent and an availability summary are computed for you.
  • Spreadsheet mode. One row per variant with options in columns (option1Name, option1Value ...), tags as text, no nested objects.
  • Honest failures. Password-protected, blocked, non-Shopify and misspelled stores are reported per store with a plain explanation, never as a silent empty dataset. If no products at all could be extracted, the run fails with the reason.
  • Polite by default. 2 parallel requests per store, a pause between requests, backoff on rate limits.

Input

Only storeUrls is required. Paste stores in any form:

You pasteWhat is read
allbirds.com, https://www.allbirds.com/The whole store
store-name.myshopify.comThe whole store (redirects to its real domain are followed)
https://kith.com/collections/footwearOnly that collection
https://shop.com/products/blue-shirtOnly that product

Example: whole catalog of two stores

{
"storeUrls": ["allbirds.com", "www.deathwishcoffee.com"]
}

Example: daily price and stock feed, changes only, one row per variant

{
"storeUrls": ["allbirds.com", "kith.com/collections/footwear"],
"outputMode": "variants",
"trackChanges": true,
"changesOnly": true
}

Example: products updated in the last 7 days, with collections

{
"storeUrls": ["www.deathwishcoffee.com"],
"updatedAfter": "7 days",
"includeCollections": true
}

All options

FieldDefaultWhat it does
storeUrls(required)Stores, collections or products to read.
maxProductsPerStore0 (all)Stop after this many products per store.
outputModeproductsproducts: one row per product, variants nested. variants: one row per variant, flat.
collectionHandlesnoneRead only these collections (handles from the URL) in every store.
updatedAfternoneKeep products with updated_at on or after an ISO date or a relative value (7 days, 24 hours).
trackChangesfalseCompare with the previous run and label every row. See below.
changesOnlyfalseWith tracking on, skip unchanged rows.
snapshotStoreNameshopify-store-scraper-snapshotsNamed key-value store that holds the snapshots. Use different names for independent tracking jobs.
includeCollectionsfalseAttach the list of collections each product belongs to. Extra requests.
maxCollectionsPerStore100Cap on collections mapped when the above is on.
enrichVariantsfalseOne extra request per product to add barcodes and, where the store publishes it, inventoryQuantity.
concurrency2Parallel requests per store.
requestDelayMs300Pause after each request to the same store.
maxConcurrentStores3Stores crawled at the same time.
requestTimeoutSecs30Timeout for one request.
maxRetries4Retries for 429, 5xx and network errors.

Output

Rows go to the default dataset. Download them as JSON, CSV, Excel, XML or RSS from the Output tab or the API. The dataset has four views: Products, Changes, Catalog details and Stores that could not be read.

Products mode (one row per product)

{
"recordType": "product",
"storeDomain": "www.allbirds.com",
"productId": 7215082405968,
"handle": "womens-wool-runner-nz-shadow-blue-natural-white-sole",
"url": "https://www.allbirds.com/products/womens-wool-runner-nz-shadow-blue-natural-white-sole",
"title": "Women's Wool Runner NZ - Shadow Blue (Natural White Sole)",
"vendor": "Allbirds",
"productType": null,
"tags": ["11/3/26 DROP", "..."],
"createdAt": "2025-08-27T09:16:36-07:00",
"updatedAt": "2026-11-11T08:31:27-08:00",
"publishedAt": "2026-11-04T15:32:48-08:00",
"descriptionText": "(the product description as plain text)",
"descriptionHtml": "<p>(the original description HTML)</p>",
"options": [{ "name": "Size", "position": 1, "values": ["5", "5.5", "6", "..."] }],
"images": ["https://cdn.shopify.com/s/files/1/1104/4168/files/A12799W-SHADOWBLUE_SIDE_TRANSPARENT.png?v=1793835066", "..."],
"featuredImage": "https://cdn.shopify.com/s/files/1/1104/4168/files/A12799W-SHADOWBLUE_SIDE_TRANSPARENT.png?v=1793835066",
"imageCount": 4,
"currency": "USD",
"priceMin": 120,
"priceMax": 120,
"compareAtPriceMin": null,
"compareAtPriceMax": null,
"onSale": false,
"discountPercent": 0,
"available": true,
"availabilityStatus": "partially_available",
"variantCount": 13,
"availableVariantCount": 12,
"variants": [
{
"variantId": 41314103427152,
"title": "5",
"sku": "A12799W050",
"options": { "Size": "5" },
"option1": "5", "option2": null, "option3": null,
"price": 120,
"compareAtPrice": null,
"onSale": false,
"discountPercent": 0,
"available": true,
"grams": 1000,
"barcode": null,
"requiresShipping": true,
"taxable": true,
"position": 1,
"imageUrl": null,
"createdAt": "2025-08-27T09:16:36-07:00",
"updatedAt": "2026-11-11T08:31:27-08:00"
}
],
"source": "products_json",
"scrapedAt": "2026-11-11T16:42:56.406Z"
}

(Values shortened for the example.) availabilityStatus is in_stock (every variant purchasable), partially_available or out_of_stock. discountPercent is the largest discount among variants that have a compare-at price above their price. barcode stays null unless enrichVariants is on and the store exposes it.

Variants mode (one row per variant)

Same product fields, flattened: productTitle, variantTitle, sku, barcode, option1Name / option1Value (up to 3), price, compareAtPrice, onSale, discountPercent, available, grams, imageUrl, tags (comma-separated), collections (comma-separated), url with ?variant=<id>. No nested objects, so CSV and Excel exports are clean.

Stores that could not be read

A row with recordType: "store_status" is added for each store that could not be read, with a status and a plain-language message:

statusMeaning
password_protectedThe store is behind Shopify's storefront password. Not supported.
blockedThe store refused access (401/403) or showed a bot-protection page. Skipped, never worked around.
rate_limitedThe store kept answering 429 after polite retries. Try later or lower concurrency.
not_shopifyThe site does not serve Shopify's public catalog endpoints.
store_unavailableShopify says the shop is closed or the domain is not connected.
unreachableThe domain does not resolve, or TLS/network failed.
invalid_inputThe value is not a usable domain or URL.
no_productsA Shopify store with an empty public catalog (or an empty collection selection).

These rows are never charged.

Run summary

The default key-value store record OUTPUT holds a summary: per store the status, products found and delivered, rows, requests, retries, duration, currency, whether the catalog was read completely, warnings, and change counts. Use it to monitor scheduled runs.

Change tracking

Turn on Track changes between runs and schedule the Actor (for example daily). Each run compares every product with the snapshot from the previous run of the same store and adds:

FieldMeaning
changeTypeThe most important change: new, price_changed, back_in_stock, out_of_stock, unchanged or removed.
changesEvery change that applies, e.g. ["price_changed", "out_of_stock"]. Products mode also lists variant-level events: variants_added, variants_removed, variant_sold_out, variant_restocked.
previousPriceMin, previousPriceMaxLast run's price range (products mode).
previousPriceLast run's price (variants mode).
previousAvailableWhether it was purchasable last run.
changeDetailsProducts mode: the variant IDs behind each change.

Rules, so you can rely on the labels:

  • The first tracked run labels everything new. It creates the baseline.
  • Priority when several things changed: price_changed, then back_in_stock, then out_of_stock. The changes list always has all of them.
  • Product-level stock means "any variant purchasable". If one size sells out but others remain, the product stays unchanged and changes contains variant_sold_out. In variants mode each variant gets its own label.
  • Removed rows are emitted for products (or variants) that were in the snapshot but are no longer in the store, with their previous price. They are reported only after a complete read of the same scope. If a run was capped by maxProductsPerStore, hit an error, or stopped at the charge limit, nothing is labelled removed, and the next complete run reports it.
  • Nothing is lost on a budget stop. The snapshot is updated only for rows actually delivered, so a change that did not fit in this run is reported in the next one.
  • Scopes are tracked separately. A whole-store run and a run on collections/sale of the same store keep separate snapshots.
  • updatedAfter filters the output but still reads the whole catalog, so removals are still detected.
  • Snapshots live in the named key-value store snapshotStoreName in your Apify account (compressed, one record per store and scope). Delete that store, or pick a new name, to start over.

Pricing

This Actor uses pay-per-event pricing: you pay a small fee per run start and per result row delivered to the dataset. See the Pricing tab for the current prices.

  • Products mode is charged per product row, variants mode per variant row.
  • With change tracking, removed rows count as results; with "Output only changed products" unchanged rows are not delivered and not charged.
  • Rows describing stores that could not be read are free.
  • Set Maximum cost per run in the run options to cap spending. The Actor stops cleanly when the cap is reached, keeps everything delivered so far, and notes it in OUTPUT.

Use from an AI agent or via API

Ready-to-run Python, Node.js and cURL examples: github.com/cedarfetch/shopify-store-scraper-examples.

Run synchronously and get the rows back in one HTTP call:

curl -X POST "https://api.apify.com/v2/acts/<username>~shopify-store-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"storeUrls":["allbirds.com"],"maxProductsPerStore":50,"outputMode":"variants"}'

JavaScript:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('<username>/shopify-store-scraper').call({ storeUrls: ['allbirds.com'], trackChanges: true, changesOnly: true });
const { items } = await client.dataset(run.defaultDatasetId).listItems();

Python:

from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("<username>/shopify-store-scraper").call(run_input={"storeUrls": ["allbirds.com"]})
items = client.dataset(run["defaultDatasetId"]).list_items().items

The Actor is also available to agents through the Apify MCP server, which exposes Store Actors as tools. Tips for agents: keep maxProductsPerStore small for exploratory calls, use outputMode: "variants" when you need per-SKU prices, and read recordType to separate product rows from store_status rows.

Limits and honest caveats

  • Public data only. It reads what any shopper's browser can read without logging in. Unpublished products, wholesale/B2B catalogs, customer-specific prices and admin data are out of reach by design.
  • Password-protected stores are not supported and are reported as password_protected.
  • Bot protection is not bypassed. A few stores put Cloudflare, Vercel or similar challenges in front of their catalog. Those are reported as blocked.
  • Headless Shopify stores (custom frontend on another platform, e.g. Hydrogen on Vercel) usually do not expose the catalog endpoints on their main domain and are reported as not_shopify. If you know the store's *.myshopify.com domain, try that.
  • Currency is the store's base currency from /meta.json. Stores selling in several currencies through Shopify Markets show base-currency prices; currency is null if a store hides /meta.json.
  • Stock counts (inventoryQuantity) appear only with enrichVariants and only when the store publishes them. Otherwise you get available true/false per variant.
  • Stores above 25,000 products are read beyond that limit through the sitemap, one request per extra product, so they take longer.
  • Shopify's updated_at changes on many edits (including stock), so updatedAfter is a "recently touched" filter, not "price changed". Use change tracking for exact changes.

FAQ

How do I find out if a site is a Shopify store? Just add it. Non-Shopify sites are reported as not_shopify within a couple of requests and are not charged.

Can I scrape several stores in one run? Yes, up to 1,000 per run. Three are crawled at a time by default.

How fast is it? About one request per 250 products. A 1,000-product store takes a few seconds of network time.

Why do I see fewer products than the store's homepage suggests? Only published products visible to the public catalog are included; check the OUTPUT summary for warnings such as a capped run or a collection that returned nothing.

Do I need proxies? No. Shopify's public endpoints answer plain requests. Stores that block plain requests are reported, not worked around.

Can I track only one collection? Yes, paste the collection URL. Its snapshot is kept separately from the whole-store snapshot.

Is this legal? The Actor collects publicly available product information and no personal data. You are responsible for how you use the data and for respecting each site's terms. If in doubt, ask a lawyer.

Support

Found a store that behaves oddly? Open an issue on the Issues tab with the store domain and the run ID; the OUTPUT record usually shows what happened.

More Actors from Cedarfetch Labs

Built the same way: public data only, typed output, per-source error reporting, change tracking for scheduled runs.

  • ATS Jobs Scraper: open jobs from Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Recruitee and Personio boards, with new and removed postings flagged.
  • Remote Jobs Scraper: remote jobs from Remote OK, We Work Remotely and five more boards in one deduplicated feed.
  • Substack Scraper: public posts from any Substack newsletter as Markdown, with a new-posts feed.
  • Google Trending Searches: what is trending on Google right now, per country, with alerts on new trends.