# Shopify Products Data, Shopify Collection Scraper, Shopify CSV (`fetchsmith/shopify-products-scraper`) Actor

Every product from any Shopify store or collection: prices, variants, SKUs, barcodes, stock counts, availability, images, tags. Filter by price, sale, brand, type or stock. Overlapping collections deduped: one row, one charge per product. Watch a store: drops, restocks, new, delisted. No browser.

- **URL**: https://apify.com/fetchsmith/shopify-products-scraper.md
- **Developed by:** [Fetch Smith](https://apify.com/fetchsmith) (community)
- **Categories:** E-commerce, Lead generation, AI
- **Features:** Limited permissions
- **Stats:** 5 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.85 / 1,000 products

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Shopify Products Data Scraper

Extract the full product catalog of any Shopify store (or a single collection or product) into JSON, CSV or Excel: title, vendor, type, tags, prices, compare-at prices, availability, images, options, variants with SKUs, and description. Works on any Shopify storefront, no login, no browser. Pay only per product returned.

It doubles as a Shopify catalog API and a general product feed API: query any storefront's public JSON feed and get back a clean, per-product priced dataset. Pull Shopify inventory data (stock counts, barcodes, quantities) or set up ongoing Shopify competitor monitoring for price drops, restocks and new launches.

### Use cases

- Competitor price and assortment monitoring
- Building product feeds, catalogs and dropshipping research datasets
- Tracking new launches and stock changes on a schedule
- Feeding product data into AI agents and recommendation systems

### Input

| Field | Type | Description |
|---|---|---|
| `storeUrls` | array | Store homepages, `/collections/<handle>` URLs, or `/products/<handle>` URLs |
| `searchQuery` | string | Only keep products whose title, vendor, product type or tags contain every word in the query (case-insensitive). Applied client-side after fetching — Shopify's public feed has no server-side search — so it still scans the whole store at the same request cost as an unfiltered run, but needs no collection URL guessing. Ignored on direct product URLs. |
| `vendors` | array | Only keep products whose vendor/brand contains one of these (case-insensitive OR — e.g. `["Allbirds","Rothy's"]`). Combines with every other filter via AND. Ignored on direct product URLs. |
| `productTypes` | array | Only keep products whose product type contains one of these (case-insensitive OR), e.g. `["Shoes","Sandals"]`. Combines with every other filter via AND. Ignored on direct product URLs. |
| `maxProductsPerStore` | integer | Cap per store (default 500) |
| `includeDescription` | boolean | Plain-text description (default true) |
| `includeVariants` | boolean | Full variants array with SKU, price, options, availability (default true). Leaving it `true` also matters when `detailLevel` is `"full"`: the variants array is the only container the paid per-variant inventory fields can be returned in (see below) |
| `onlyAvailable` | boolean | Skip sold-out products |
| `minPrice` | integer | Only keep products with a variant at or above this price (matches when the product's own price range overlaps the window — a $40-$120 hoodie matches `minPrice: 100`). Store's own currency, no FX conversion. Filtered-out products are never charged. |
| `maxPrice` | integer | Only keep products with a variant at or below this price (same range-overlap rule). Store's own currency, no FX conversion. Filtered-out products are never charged. |
| `onSaleOnly` | boolean | Only keep products currently discounted — a compare-at price set above the current price, the same condition as the `isOnSale` output field. Filtered-out products are never charged. |
| `minDiscountPercent` | integer | Only keep products discounted by at least this percentage (checks the `discountPercent` output field). Stricter than `onSaleOnly`, which accepts any discount; products with no discount at all (`discountPercent: null`) are excluded, not treated as 0%. Filtered-out products are never charged. |
| `maxResults` | integer | Total cap |
| `proxyConfiguration` | object | Route storefront requests through Apify Proxy. **Default OFF, and usually best left off** — since 2026-11-04 many storefronts answer 403 to Apify's shared datacenter pool while serving a direct connection normally, so enabling it can lose you products. Turn it on for per-IP rate limiting (429), or with a country-specific residential group to read that country's prices and currency. |
| `detailLevel` | string | `"basic"` (default) reads only the products feed. `"full"` also fetches each product's live page **and** its per-product `.js` route for `barcode`, `inventoryQuantity`, `inventoryManagement`, `inventoryPolicy`, `quantityRule`, `totalInventory`, `seoTitle`, `seoDescription`, `ratingValue`, `reviewCount`, `hasSubscriptionOption` and `subscriptionPlans` — none of which the bulk `products.json` feed carries — priced as one extra event per product (see Pricing). Both requests run in parallel, so it costs no extra wall-clock. |
| `watchLabel` | string | Turn the run into a **catalog watch**. The first run under a label records every product's price, availability and sale status and returns nothing (charges nothing); later runs under the same label return only products that are new, changed price, started/ended a sale, changed stock status, or have been removed from the catalog. See *Watch mode* below. |
| `watchEvents` | array | Restrict a watch to some change types only: `new`, `priceDrop`, `priceIncrease`, `wentOnSale`, `saleEnded`, `backInStock`, `outOfStock`, `delisted`. Empty (default) reports all eight. `wentOnSale`/`saleEnded` fire only when a compare-at price is added/removed with the current price unchanged — a price move that also flips the sale flag is reported as `priceDrop`/`priceIncrease` instead (the row's `previousIsOnSale` still shows the sale-state change). Ignored without `watchLabel`. The platform's own select editor only ever offers these eight, but a value fed in some other way is **not** rejected: an unrecognized entry is skipped with a log warning, and if every entry given is unrecognized the run falls back to reporting all eight — same as leaving it empty. |
| `webhookUrl` | string | Optional http(s) URL that receives a small JSON POST when the run finishes (products pushed, per-change-type counts, dataset id, and a per-URL `sources` outcome list — see the FAQ). Best-effort — a failing webhook never fails the run or changes the bill. |

### Output (one item per product)

```json
{
  "id": 6543210987,
  "title": "Men's Wool Runner",
  "handle": "mens-wool-runners",
  "url": "https://www.allbirds.com/products/mens-wool-runners",
  "vendor": "Allbirds",
  "productType": "Shoes",
  "tags": ["men", "runners"],
  "currency": "USD",
  "priceMin": 98,
  "priceMax": 98,
  "compareAtPriceMin": 125,
  "compareAtPriceMax": 125,
  "isOnSale": true,
  "discountPercent": 21.6,
  "available": true,
  "availableVariantCount": 9,
  "variantCount": 12,
  "totalInventory": 149,
  "images": [{ "src": "https://cdn.shopify.com/s/files/....jpg", "alt": "Men's Wool Runner in Natural Grey" }],
  "imageUrl": "https://cdn.shopify.com/s/files/....jpg",
  "imageCount": 6,
  "variants": [{ "id": 1, "title": "8 / Natural Grey", "sku": "WR-8-NG", "price": 98, "compareAtPrice": 125, "available": true, "grams": 340, "requiresShipping": true, "taxable": true, "position": 1, "featuredImage": "https://cdn.shopify.com/s/files/....jpg", "barcode": "196942208243", "inventoryQuantity": 15, "inventoryManagement": "shopify", "inventoryTracked": true, "inventoryPolicy": "deny", "quantityRule": null }],
  "description": "Our classic everyday sneaker ...",
  "descriptionHtml": "<p>Our classic everyday sneaker ...</p>",
  "createdAt": "2025-01-10T12:00:00Z",
  "updatedAt": "2026-08-01T09:30:00Z",
  "store": "https://www.allbirds.com",
  "sourceUrl": "https://www.allbirds.com/collections/mens",
  "seoTitle": "Men's Wool Runner Shoes | Allbirds",
  "seoDescription": "Our best-selling sneaker, made from sustainably sourced merino wool.",
  "ratingValue": 4.6,
  "reviewCount": 51,
  "hasSubscriptionOption": true,
  "subscriptionPlans": [{ "name": "Subscribe & Save 20%", "plans": [{ "name": "Delivered every 30 days", "recurringDeliveries": true, "discountPercent": 20 }] }]
}
```

`barcode`/`inventoryQuantity`/`inventoryManagement`/`inventoryTracked`/`inventoryPolicy`/`quantityRule` (per variant), `totalInventory`/`hasSubscriptionOption`/`subscriptionPlans` (per product), `seoTitle`, `seoDescription`, `ratingValue` and `reviewCount` are only present when `detailLevel` is `"full"` (see Input) — Shopify's bulk `products.json` feed carries none of them.

**`detailLevel:"full"` needs `includeVariants:true` to deliver the per-variant half.** The six per-variant fields above live inside the `variants` array, so `includeVariants:false` leaves them with nowhere to go and they appear in no row at all — while every product is still charged one `productDetail` event. The per-product half (`seoTitle`, `seoDescription`, `ratingValue`, `reviewCount`, `hasSubscriptionOption`, `subscriptionPlans`, `totalInventory`) does still land, so the combination is legitimate if that is all you want — `totalInventory` in particular is rolled up from the detail route's own variant data and is correct even with no `variants` array in the row (verified live, cycle 2600: the two runs charge identically and every per-product field is byte-identical to a run with variants on). The run warns in its log when you ask for this pairing. If you want `barcode`/`inventoryQuantity`/stock policy per SKU, keep `includeVariants` at its `true` default.

`quantityRule` (`{min, max, increment}`) is only populated for a real, non-default rule — a minimum order quantity, a case-pack increment, or a per-order cap — Shopify's own implicit default (`min:1, max:null, increment:1`) is reported as `null` instead so it doesn't look like every product has a "rule". `hasSubscriptionOption`/`subscriptionPlans` cover both Shopify's native subscriptions and app-backed ones like Recharge (verified live on both); `subscriptionPlans[].plans[].discountPercent` is `null` when a plan doesn't use a flat percentage discount (e.g. a fixed-price override).

**How complete the stock fields are is a per-store setting, not something any scraper controls.** Measured live: allbirds.com publishes real per-variant quantities *and* UPC barcodes; brooklinen.com publishes a barcode but no quantity; rothys.com publishes barcodes but no quantities. Missing values come back as `null` rather than a guess. `barcode` is whatever the merchant typed into that field — a GTIN/UPC on most stores, an internal SKU on some. `totalInventory` counts only variants Shopify actually tracks stock for, so untracked items (`inventoryManagement: null`, which report a 999999 sentinel) can't inflate it; it is `null` — not `0` — when a store tracks none, keeping "sold out" distinguishable from "this store doesn't publish stock". Each variant also carries `inventoryTracked` as an explicit boolean (`true`/`false`, or `null` on a `detailLevel:"basic"` row that has no inventory data at all), so that same guardrail survives flattening `variants[]` into CSV rows, where an implicit `inventoryManagement: null` is easy to drop and a 999999 sentinel is then easy to sum by mistake.

`watchLabel`, `watchChange`, `watchChanges`, `previousPriceMin`, `previousAvailable`, `previousIsOnSale` and `priceChange` are only present on rows returned by a run with `watchLabel` set (see *Watch mode* below); a normal one-off scrape never emits them.

### Watch mode — pay only for what changed

Scraping a competitor's catalog every morning normally means re-buying the same 500 products every morning. Set `watchLabel` and you buy the diff instead:

```json
{ "storeUrls": ["https://www.allbirds.com"], "maxProductsPerStore": 5000,
  "watchLabel": "allbirds-prices", "watchEvents": ["priceDrop", "backInStock"] }
```

- **Run 1** records each product's price, availability and sale status and returns **0 products, charged $0** — it's a baseline.
- **Every run after that** returns only the products that moved. Each row is a normal product row plus `watchChange` (`new` / `priceDrop` / `priceIncrease` / `wentOnSale` / `saleEnded` / `backInStock` / `outOfStock` / `delisted`), `watchChanges` (all changes on that product), `previousPriceMin`, `previousAvailable`, `previousIsOnSale` and `priceChange` (the signed difference).
- Unchanged products are never pushed and **never charged**, so a daily watch on a stable catalog costs nothing on quiet days.
- Adding a store URL to an existing label baselines **just that store**, so you aren't billed for its whole catalog as "new". Changing a filter (`onlyAvailable`, `searchQuery`, `vendors`, `productTypes`, `minPrice`, `maxPrice`, `onSaleOnly`, `minDiscountPercent`) starts a fresh baseline, since it's a different watched set.
- In watch mode `maxProductsPerStore` caps how many products are **scanned** per store (the diff has to walk the feed), so set it above the store's catalog size; the log warns when the sweep was cut short.

#### `delisted` — products that are gone

A `delisted` row means a product that was in the baseline is no longer in the store's catalog at all: pulled, discontinued or sold out permanently. It is the one watch event with a coverage precondition, because "absent" and "we didn't look" are the same observation from the outside:

- It is only produced for a store whose feed this run walked **end to end**. If `maxProductsPerStore` cut the sweep short, `maxResults` or your PPE budget stopped it mid-flight, the run ran out of time, or the store errored, **no `delisted` rows are produced for that store** — the run never guesses, so you are never charged for a catalog that is still there.
- **A watched single product URL (`/products/<handle>`) has no "page 2" to still hold it, so it works differently: a clean 404 on a URL this label already successfully watched before is treated as delisted immediately, in the same run that first sees it 404** (not the password/frozen/blocked/rate-limited cases, which report their own reason and are not treated as delisted).
- Products that are still on the store but no longer match your filters (e.g. a price rose above `maxPrice`) are **not** delisted. Coverage is measured on the raw feed, before any filter runs.
- If more than half a store's baseline (and more than 25 products) goes missing at once, the run reports **none** of it and logs why — a collection being emptied or re-scoped looks identical to a mass delist from the outside. They stay in the baseline, so a genuine shutdown still surfaces as the catalog shrinks past the guard.
- A delisted row carries `id`, `handle`, `url`, `store`, `previousPriceMin`, `previousAvailable` and `previousIsOnSale`; the product-detail fields are `null`, because there is no longer a product to read them from. The `url` is the product's old URL — it 404s by definition, and is there to identify the product.
- Once reported, the product leaves the baseline, so you are charged for it **once**. If it comes back, it returns as `new`.
- Pair it with `webhookUrl` and Apify's scheduler to get a ping only when something actually changes.

### Pricing

`result` — charged per product returned, **$0.001/product on the Free plan, $0.00095 on Bronze, $0.0009 on Silver, tapering to $0.00085/product on Gold and above** (full live ladder, verified against the live Actor record 2026-10-04; the comparisons below quote the "$0.001 → $0.00085" ends of it for brevity). No per-run "Actor Start" fee at all — ours is genuinely zero (re-verified live 2026-10-03). Many rivals in this niche do charge one, from $0.00005 up to $0.05 per run before any data is delivered, but **we no longer claim most of them do**: live-pricing the whole unnamed cohort on 2026-10-07 found 31 of 64 with no start fee at all, so a start fee is roughly a coin flip in this niche rather than the norm. Among the rivals named here the sweeps have found nine that also charge no start fee — `pintostudio/shopify-product-search` (41 users, $0.002/result), `igview-owner/shopify-scraper` ($0.015 Free → $0.005 Gold+), `rl1987/shopify-api-scraper` ($0.001 per *variant* rather than per product), `lergassy/shopify-store-intel`, `scrapesage/shopify-store-scraper`, `kalirobot/shopify-scraper`, `dami_studio/shopify-products-scraper`, `technicaldost/shopify-products-exporter` ($0.002/product) and `scrapebench/shopify-change-tracker` (58 users, $0.01 per change) — plus three Actors on Apify's FREE model that charge nothing at all (see below). Zero start fee is a real advantage over most of the niche, not over all of it. Stores that block the public catalog return nothing and cost nothing.
`productDetail` — charged **once** per product (not once per extra request) only when `detailLevel` is `"full"` **and** the extra fetches actually found stock, SEO or rating data, at the same tiered rate as `result` ($0.001 Free, $0.00095 Bronze, $0.0009 Silver, $0.00085 Gold+). A product that yields none of them — no rating app installed, no meta description, store doesn't publish stock — costs nothing.

The biggest rival in this niche is `autofacts/shopify` (**2,467 users** as of 2026-11-09, was 2,374 at the 2026-10-23 sweep — nearly three times the next listing). It is a genuine head-on competitor: whole catalogs, single collections, single product URLs or a keyword search, with variants, prices, images, tags and optional per-variant live stock counts. **It cut its price since our last check (was $0.0012 Free → $0.0008 Gold+ on 2026-10-02): it is now tiered $0.001/product on Free down to $0.0007/product on Gold and above** ($0.0009 Bronze, $0.0008 Silver), plus the same $0.00005 Actor Start fee, a cheaper $0.0004 collection event (was $0.0008), unchanged $0.001 recommended-product / $0.001 "Real inventory" events, and one new event not seen before, "Content Sections" ($0.001/row). **So we now only tie on the Free plan ($0.001 = $0.001) and it is cheaper at every paid tier** (Bronze $0.0009 vs our $0.00095, Silver $0.0008 vs our $0.0009, Gold+ $0.0007 vs our $0.00085) — still a sub-cent-per-ten-products gap, so still a features decision more than a price one, but the Free-tier edge we used to have is gone (verified live 2026-10-23).

The next-largest rival, `trovevault` (843 users as of 2026-11-09, was 758 at the 2026-10-23 sweep, `trovevault/shopify-products-scraper`), matches our per-product rate ($0.001 Free → $0.00085 Diamond, re-verified live 2026-10-23 — 0 price drift since 2026-09-23) but adds two charges we don't have: a one-time Actor Start fee every run tiered the same $0.001 → $0.00085 (1 GB default memory), and — since 2026-09-23, after a promotional free period ended — a second $0.001-tier "Inventory Enrichment" event on top of the base product charge for stock counts, barcodes and SEO fields (the equivalent of our `productDetail`, which stays a single combined charge). At Free-tier pricing, their full-detail product now costs $0.002 + a per-run start fee versus our $0.002 with no start fee. Their entire live input schema is six properties — `domains`, `maxProducts`, `includeInventoryDetails`, `proxyConfiguration` and two internal plumbing fields — so it has no equivalent of our `vendors`/`productTypes`/`searchQuery`/`minPrice`/`maxPrice`/`onSaleOnly`/`minDiscountPercent` filtering, watch mode, or webhooks (checked against their 2026-09-18 build's input schema, not their Store description, verified live 2026-10-02).

`webdatalabs/shopify-product-scraper` (411 users) charges roughly 10x our rate — $0.01/product on Free tapering to $0.007/product on Gold+, plus a $0.00005 Actor Start fee — versus our $0.001 → $0.00085 with no start fee (verified live 2026-10-03). The same account also runs `webdatalabs/shopify-store-intelligence` (186 users, confusingly titled "Shopify Store Scraper" in the Store), which is **priced per store, not per product** — $0.01 → $0.004 per store analysed plus a $0.006 → $0.003 Actor Start fee — so it is not comparable to us per row and is a store-profile tool rather than a catalog export (verified live 2026-10-03). `nexgendata/shopify-store-analyzer` is the same shape at $0.05 per store-analysis record plus $0.01/$0.02 competitor-pricing and tech-stack add-ons. Two more of this same store-profile shape (verified live 2026-11-14): `apivault_labs/shopify-store-analyzer` (472 users) prices per "Store Analyzed" — $0.013 FREE down to $0.005 GOLD-and-above, plus a $0.0005 start fee — for revenue/app-signal estimates, emails and phone numbers per store, not a per-product catalog row; `clearpath/shopify-store-leads` (2,007 users, the single biggest listing we found still undisclosed in this niche) is priced per "Lead" ($0.00599 FREE down to $0.00499 GOLD-and-above) for B2B contact details (email, phone, address, socials) by keyword or category — a store-leads/contact-scraping product, not a catalog export, so neither is comparable to us per row.

**What we do not claim: we are not the cheapest in this niche, at any tier.** A 12-term Store sweep on 2026-10-03 (re-run full-list 2026-10-05, 131 matched, 362 seen) saw hundreds of Shopify-mentioning listings — the large majority are Shopify **App Store**, lead/email-finder or product-review scrapers that do not export a store's catalog at all. Ruled out by scope, not priced: `lurkapi/shopify-product-reviews-scraper-api` (80 users as of 2026-10-25, scope re-verified live 2026-10-16), `lurkapi/shopify-loox-reviews-scraper-api` (verified live 2026-10-05) and `cleanscrape/shopify-reviews-scraper` all sell shopper **review** text/ratings pulled from a store's review app, not the product catalog itself — a different product entirely, not a substitute at any price. Priced all 52 genuine product-catalog, store-profile and price-watch rivals live. Several cost less than we do:

- `codescraper/fast-shopify-products-scraper` (the most-used free rival found so far), `novus/shopify-scraper`, `bercikgroup/shopify-store-products-scraper` and `streaked_patty/mercadolibre-scraper` (mislabeled slug — its title and input schema are plain Shopify, nothing to do with MercadoLibre) are all on Apify's **FREE pricing model — $0 per product at any volume**, with no start fee either. All four are real product scrapers reading `products.json`. No price we could set beats free; what we offer over them is the filtering (`vendors`/`productTypes`/`searchQuery`/price/discount), watch mode, webhooks, `detailLevel: "full"` enrichment and the per-source `complete`/`incompleteReason` completeness contract — none of which any of the four advertises (`codescraper`'s entire input is `startUrls`/`maxItems`/`proxyConfiguration`). `streaked_patty` is also the narrowest of the four: a single `shop_domain` per run (no multi-store, no collection or product-URL targeting) hard-capped at **250 products total**, so it cannot export a catalog bigger than that at any price. (`bercikgroup`/`novus` re-verified live 2026-10-03, `streaked_patty` newly found and verified live 2026-10-05, `codescraper` newly found and verified live 2026-10-06: `bercikgroup` carries no pricing record at all, `novus` was auto-converted off Apify's sunsetting rental model on 2026-10-02 and landed on FREE, `streaked_patty` has run a single build on the FREE model since 2026-07-18, and `codescraper` was a flat $3/month rental until it was auto-migrated off the sunsetting rental model on 2026-10-06 and landed on FREE too — none of the four has a per-product charge today.)
- `fetch_cat/shopify-products-scraper` charges $0.0000281/product on Free (~35x under our rate) behind a $0.005 start fee, so it is cheaper than us on any run past roughly 6 products.
- `sleek_waveform/universal-shopify-scraper` charges $0.0005/product plus $0.00001/result and a $0.00005 start fee — about half our rate at every tier.
- `shahidirfan/Shopify-Product-Scraper` (41 users) charges a flat $0.0009/product regardless of plan, beating our $0.001 Free-tier rate (it charges a $0.00005 start fee we don't, and loses to us on Gold+ at $0.0009 vs $0.00085).
- `lergassy/shopify-store-intel` prices products at $0.0005 → $0.00035, under our rate, but bills $0.005 per store profile on top and is scoped as store intelligence (identity, email, apps, catalogue size) rather than a full catalog export.
- `kalirobot/shopify-scraper` charges a flat **$0.00049/product with no start fee at all** — about half our Free rate and still under our $0.00085 Gold+ rate, so it is cheaper than us at **every** plan tier with no crossover volume. It is a real catalog extractor; what we add over it is the `vendors`/`productTypes`/`searchQuery`/price/discount filtering, watch mode, webhooks, `detailLevel: "full"` enrichment and the `complete`/`incompleteReason` contract (newly named 2026-10-03).
- `rover-omniscraper/shopify-scraper` ("Ultra Fast Shopify Scraper") charges a flat **$0.0009/product plus a $0.0003 start fee**, so it beats our $0.001 Free rate on any run past roughly 3 products, while our $0.00085 Gold+ rate beats it at any volume. It is the closest new rival on features as well as price — multi-store auto-detection, price/tag/sale filtering and new-arrival flags, pure HTTP, no browser — though it advertises no watch mode, webhooks or per-source completeness contract (newly named 2026-10-03).
- `scrapesage/shopify-store-scraper` is **tiered the wrong way round from us**: $0.002/result on Free (double our rate) falling to $0.00076 on Platinum and **$0.0005 on Diamond**, with no start fee. We are cheaper up to and including Gold; it undercuts our flat $0.00085 on the top two plans only (newly named 2026-10-03).
- `autofacts/shopify` undercuts us on Gold and above, as above.

We also checked these rival Actors, none of which undercut us — `clearpath/shop-by-shopify-product-scraper` (149 users as of 2026-11-06, user count re-verified 2026-11-06, $0.00249 → $0.00179/product plus a separate $0.00001/result event and an Actor Start fee — pricier than us at every tier), `khadinakbar/shopify-all-in-one-scraper` (35 users, $0.003/product flat plus separate collection/review events), and `pintostudio/shopify-products-scraper` (34 users, $0.0037/result flat plus start fee). A 2026-10-05 pass found four more flat-rate catalog scrapers, all dearer than us at every tier: `cg_nguyen/shopify-store-scraper` ($0.002/product + $0.00005 start), `hichemdev/shopify-product-scraper` ($0.002/product + $0.002 start), `mighty_monk/shopify-product-scraper` ($0.003/product + $0.00005 start) and `hello.datawizards/shopify-product-script` ($0.008/result + a $0.05 start fee, the dearest start fee found in this niche). The same sweep priced ten more Shopify catalog rivals live, all dearer than us: `khadinakbar/ecommerce-store-scraper` (122 users, $0.003/product + $0.005 start), `automation-lab/shopify-scraper` (113 users as of 2026-11-09, $0.003 Free → $0.00084 Diamond + $0.005 start — its Diamond per-product rate shades ours by $0.00001, but the start fee puts it above us on any real run), `thirdwatch/shopify-store-scraper` (55 users, $0.002 → $0.0012 + start), `benthepythondev/shopify-store-scraper` (23 users, $0.004 → $0.0028 + start), `crawlerbros/shopify-scraper` ($0.002 → $0.001 + a $0.01 start fee, delisted as of 2026-11-08), `makework36/shopify-products-scraper` ($0.001 flat + start), `parseforge/shopify-stores-scraper` ($0.01599 → $0.01199 + $0.005 start), `prodiger/shopify-scraper` ($0.003 + $0.005 start), `apivault_labs/shopify-product-scraper` (verified live 2026-10-05, $0.003 flat + start) and `automly/shopify-products-scraper` ($0.00155 + a $0.05 start fee) (all re-verified live 2026-10-03 with zero price drift since 2026-10-02).

The 2026-10-03 sweep priced thirteen further catalog rivals that had never been named here, **all of them dearer than us** — `memo23/dtc-product-scraper` (37 users, verified live 2026-10-16, $0.001/product, a tie with our Free rate, but behind a $0.005 start fee and a second $0.001 "Additional data" event), `agenscrape/shopify-intelligence-scraper` ($0.002 + a $0.01 start fee), `flash_scraper/shopify-store-scraper` ($0.003 + start), `cloud9_ai/shopify-scraper` ($0.0025 + start), `crawlerbros/shopify-scraper-pro` ($0.002 Free → $0.001 Gold+ plus a $0.005 start fee, delisted as of 2026-11-08 — a second, also-delisted listing from the same account as `crawlerbros/shopify-scraper` above, delisted as of 2026-11-08), `piotrv1001/shopify-product-scraper` ($0.0015 + start), `fortuitous_pirate/shopify-store-scraper` ($0.00465 Free → $0.00186 Gold+ on **both** the result and the start event), `happitap/shopify-product-scraper` ($0.002 + start), `aiscraperdev/shopify-product-inventory-scraper` ($0.004 + start), `muhammadafzal/shopify-product-scraper` ($0.01 Free → $0.007 Gold+ plus a $0.005 start fee), `straightforward_hydra/shopify-product-feed-scraper` ($0.002 + start), `technicaldost/shopify-products-exporter` ($0.002 flat, no start fee) and `gauzy_penguin/Shopify-Scraper` ($0.001 flat + a $0.00005 start fee — a tie with our Free rate before its start fee, and dearer than our $0.00085 on Gold+) (all verified live 2026-10-03).

A **fleet-oldest full-cohort sweep on 2026-10-05** (niche grown to 374 seen, 136 matched — up from 131/362 on 2026-10-03) found **102 matched listings never named here**, almost all sitting at 1-2 users (only one, `f0rty7even/shopify-products-scraper`, which is itself dearer at $0.00099 → $0.00079 — see below). Every one was live-priced via its own Actor record, reading `isPrimaryEvent` rather than the cheapest-looking event (the same trap as `trovevault`/`memo23` above): four listings' real headline price turned out to be a *dearer* flat $0.002-$0.01/row event while a non-primary "enrichment"/"change-alert" sub-event sat at a misleadingly cheap $0.0001-$0.00001 (`zentrafoundry/shopify-woocommerce-dtc-catalog-intelligence`, `zentrafoundry/sku-price-drop-alert-api`, `zentrafoundry/supplier-stock-price-monitor`, `darknezz/shopify-products-scraper`, `cancap/shopify-products-monitor` — the last two land as a tie or dearer-than-Gold+ once the real $0.001 primary event is read, not an undercutter). Four more are a different product entirely, not priced: `charlieakan/shopify-product-image-marketplace-photo-audit`, `charlieakan/shopify-product-seo-listing-quality-audit` and `charlieakan/shopify-google-shopping-feed-readiness-audit` are listing-quality **audit** tools (flag products with weak images/SEO/feed issues), and `overbifrost/shopify-csv-import-checker-catalog-diff`, `mathe_carv/shopify-csv-change-review`, `chartreuse_oast/catalogops-shopify-csv-preflight` and `biztoolsus/shopify-catalog-import-check` bill per CSV comparison/report ($0.05-$0.5 one-time), not per product — none of the eight scrapes a live catalog. `tidytools/shopify-store-products-scraper` prices its `product` event at $0.001 Free → $0.0008 Gold+, a tie on Free and dearer than us from Bronze up, so it is not an undercutter either (all verified live 2026-10-05).

Of the rest, **19 genuinely undercut us at every tier**, all flat or near-flat per-product rates with no crossover volume: `scrapesignal_labs/shopify-product-catalog-scraper` ($0.0003, the new niche floor among full catalog scrapers), `eiv/shopify-product-scraper` ($0.0004), `ahmed_jasarevic/shopify-product-scraper`, `steadyscrape/shopify-products-scraper`, `highbrow_qualification_z7w/shopify-product-price-monitor`, `springlike_meadowland/shopify-products-scraper` (all $0.0005), `itzcloud/shopify-products` ($0.0005 → $0.00035), `scrape_pad/shopify-products-scraper` ($0.0006), `tinyrex/shopify-products-scraper` ($0.0007), and at $0.0008 flat (or tapering a little below it): `ghostgrid/shopify-competitor-product-monitor`, `datamule/shopify-product-catalog-scraper`, `tenfoldfleet/shopify-products-scraper`, `coughins/shopify-multi-store-catalog-exporter`, `nexo_data/shopify-sku-price-stock-changes`, `datalantern/shopify-products`, `beike_r/shopify-products`, `ceddl/shopify-catalog-delta` ($0.0008 → $0.00068), `luminar/shopify-products-price-stock-monitor` ($0.0008 → $0.00072, delisted as of 2026-11-08) and `getascraper/shopify-store-product-monitor` ($0.00088 → $0.00066). **11 more cross under only from a paid tier up** (dearer or tied on Free): `hodayfaell/shopify-product-scraper` and `bovi/shopify-products-scraper` (both ~$0.0009, cross under at Bronze/Silver), `lightmoon/shopify-product-scraper` and `parsebird/shopify-products-scraper` (both $0.00099 → $0.00079, cross under from Silver up), `dododata/shopify-products-scraper` ($0.001 → $0.0007), `steadydata/shopify-products-catalog` ($0.001 → $0.00065), `accountable_eel/shopify-product-monitor` ($0.001 → $0.0005), `eliai/shopify-products-scraper` and `datascraperes/shopify-products-scraper` (both $0.001 → ~$0.0008/$0.00075) all cross under from Bronze up, `tacps126/shopify-products` ($0.001 → $0.0008, Gold+ only) and `montyburrows/shopify-prices` ($0.001 → $0.0007/*variant*, not product — a different unit, caveat'd). All 30 are 1-2 user listings, all read via the Actor's own `pricingInfos`/`isPrimaryEvent` record on 2026-10-05, not inferred from Store titles (verified live 2026-10-05).

**On watch mode specifically**, the sweep found three dedicated Shopify change-trackers, which is the feature set our `watchLabel` mode competes with directly rather than on catalog export: `scrapebench/shopify-change-tracker` (58 users — price increases/decreases, back-in-stock and out-of-stock flips, products added or removed) charges a flat **$0.01 per change detected**, with no start fee; `technicaldost/shopify-price-delta-monitor` charges $0.005/product; and `redfoxxie/shopify-product-price-stock-monitor` (newly named 2026-10-05) charges a flat **$0.005 per delivered price/stock/availability/variant change**, no start fee. We charge our ordinary $0.001 → $0.00085 `result` rate per *changed* product and nothing for products that did not change, so we are roughly 10x, 5x and 5x cheaper per reported change respectively; what they offer over us is nothing we lack, but none of the three is a catalog exporter, so a buyer who wants both a one-off export and a watch feed needs two Actors there and one here (verified live 2026-10-03/2026-10-05).

A **fleet-oldest re-sweep on 2026-10-06** (niche grown to 385 seen, 142 matched — up from 374/136 on 2026-10-05) found only two genuinely unnamed listings worth pricing, both well above the usual 1-2-user noise floor: `codescraper/fast-shopify-products-scraper` (added to the FREE-pricing list above) and `vulnv/shopify-products-scraper`, newly moved today from a flat $5/month rental to pay-per-event — a flat **$0.0015/product**, no start fee, dearer than our $0.001 Free rate and our $0.00085 Gold+ rate at every tier, so not an undercutter. The remaining 62 unnamed matches were judged by title and shape rather than priced one by one — **a shortcut the 2026-10-07 sweep below overturned**, so read that paragraph and not this sentence for the small-listing cohort. `check-pricing`/`check-charges` re-confirm our own rate unchanged ($0.001 → $0.00085, no start fee).

A **fleet-oldest re-sweep on 2026-10-07** (388 seen, 143 matched, 64 unnamed — essentially flat against 385/142/64 on 2026-10-06) found no unnamed listing above the 1–2-user noise floor for the first time in this niche's audit history, so on the 2026-10-06 reasoning there would have been nothing to price. **Every one of the 64 was live-priced anyway, individually, off its own Actor record — and four of them do undercut us**, which is the direct refutation of the "no tiered pricing record suggesting anything below our rate" shortcut above: a 2-user listing's price is not predictable from its title, and three of these four publish a full tiered ladder:

- `glidepath/shopify-products-scraper` is the real find — a tiered **$0.0008312/product on Free and Bronze, $0.00075 on Silver, $0.00065 on Gold and above**, behind only a $0.00005 start fee. That undercuts us at **every tier from the first product onwards** (no crossover volume), and at ~24% below our Gold+ rate it is the deepest full-ladder undercut in the niche outside the flat-rate floor listings named further up.
- `funny_ground/shopify-products-scraper` charges a flat **$0.0008/product — under our rate at every tier — but hides a $0.005 Actor Start fee** behind it, which is the whole story: we are cheaper on any run up to 25 products on Free, or up to 100 products on Gold+, and it only wins above those volumes. A single-store catalog pull usually clears 100 products, a watch-mode delta run usually does not.
- `sourcing-data-studio/shopify-products-api` prices at $0.001 Free/Bronze → $0.0009 Silver → **$0.0008 Gold+**, plus a $0.00005 start fee. We are cheaper on Bronze ($0.00095 vs $0.001) and tie on Free and Silver; it undercuts us by $0.00005/product on Gold and above, which its start fee cancels out only for the first product of each run.
- `snow_leo_data/shopify-inventory-scraper` is dearer than us on Free, Bronze and Silver ($0.0012 → $0.00098) and undercuts our $0.00085 by a single hundredth of a cent at **Gold+ ($0.00084)**, with a $0.00005 → $0.00004 start fee that keeps us cheaper on any run of four products or fewer.

Also worth naming from the same sweep: `titan_coder/shopify-products-delta-tracker` publishes **no pricing record at all**, so it is free to run today — the cheapest it can possibly be — but an Actor with no pricing record is one the author has not finished monetizing rather than a committed free tier, and it can acquire one without notice. None of the other 59 listings priced below our ladder at any tier.

A **fleet-oldest re-sweep on 2026-10-23** (niche grown to 1,273 seen, 213 matched on the 17-term hand-curated sweep — up from 516/148 at the last audit) found nine newly-unnamed listings above the usual 1–2-user noise floor, all live-priced individually off their own Actor record rather than judged by title: `thescrapelab/shopify-store-opportunity-intelligence` (10 users, $0.002/store-record), `openharvest/ecommerce-product-scraper` (7 users, $0.0015/product), `foo121/shopify-change-events` (3 users, $0.005/result-item), `kaz_kakyo/shopify-catalog` (1 user, $0.005/change-event), `enezli/shopify-ebay-product-research` (3 users, $0.005/item-scraped), `automation_buddy/shopify-product-price-scraper` (3 users, $0.005/product), `noahadler/shopify-products-store-leads` (3 users, $0.002/row) and `ef12/shopify-scraper` (3 users, $0.005/result) are all flat-rate and dearer than our $0.001 → $0.00085 ladder at every tier — none is an undercutter. `spool/shopify-store-products-scraper` (3 users, $0.002/product-enriched) is the same: dearer at every tier. The same sweep caught a real price change on the niche leader — see the `autofacts/shopify` paragraph above, now updated — and re-confirmed `trovevault`, `webdatalabs/shopify-product-scraper` and `clearpath/shop-by-shopify-product-scraper` at 0 price drift (user counts refreshed, all verified live 2026-10-23). `check-pricing`/`check-charges` re-confirm our own rate unchanged ($0.001 → $0.00085, no start fee).

### FAQ

**Does it work on custom domains, not just `*.myshopify.com`?** Yes — pass any storefront domain that runs Shopify; no need to resolve it to the `myshopify.com` backend first.
**What if the same store ends up in `storeUrls` twice?** Deduped automatically by the resolved endpoint (so `https://store.com` and `https://store.com/` count as one) — you're never charged twice for the same store.
**What if I pass several collections that share products?** Overlapping collections are normal — `/collections/all` plus a category, or two categories with the same product in both. **Each product is returned, and charged, once per run**, under the first URL that returned it; the repeats are skipped before any filter, before watch mode and before billing, and the log says how many there were. Every row carries `sourceUrl`, the input URL it came from, so several collections of one store stay distinguishable (`store` is the same for all of them). In watch mode this also means a product that merely left one collection but is still in another of your URLs is **not** reported as `delisted`.
**Can I tell which products are on sale?** Yes, each item includes `isOnSale` (true when **some single variant** has its own compare-at price above its own price) and `discountPercent` — the percentage off the cheapest variant's own list price, so it never mixes two different variants — plus store `currency`. Both are deliberately per-variant: comparing `compareAtPriceMin` against `priceMin` reads two numbers that can belong to different variants, so a stale compare-at left below a raised price can report a sale no variant is actually running.
**Does `searchQuery` handle accented words correctly?** Yes, as of v0.1.29 — the query and the product text it's matched against are Unicode-normalized before comparing, so an accented word (e.g. "café") matches regardless of which of Unicode's two equivalent representations (composed vs. decomposed) you typed it in.
**Do I get the description as HTML?** Both: `description` is plain text and `descriptionHtml` is the store's raw `body_html`, so you can keep the formatting when re-publishing a catalog.
**Can I get the product's SEO title/description and star rating?** Yes, set `detailLevel: "full"`. Those aren't in Shopify's `products.json` feed at all — they only live on the rendered product page (`<title>`/meta description tags and, when the store runs a review app like Judge.me or Yotpo, a `ratingValue`/`reviewCount` in the page's structured data), so getting them costs one extra HTTP request per product and is priced as its own event.
**Can I get real inventory counts and barcodes?** Yes, set `detailLevel: "full"`. Shopify's bulk `products.json` feed strips `barcode`, `inventory_quantity`, `inventory_management` and `inventory_policy` out of every variant, but the per-product JSON route still serves them on the same variant ids, so they merge back in exactly. How much a given store exposes is that store's own setting (see the note under Output) — you get real values where they are published and `null` where they are not, never a guess. No login or Admin API token is involved; this is the same data a logged-out shopper's browser receives. Every variant also gets `inventoryTracked` (`true`/`false`) so "this quantity is real" is a named field rather than something you infer from `inventoryManagement` being non-null — untracked variants report a 999999 sentinel quantity, and that trap is easy to walk into once you flatten `variants[]` into a CSV and drop a column. It is `null`, not `false`, on rows that carry no inventory data at all (`detailLevel: "basic"`), because "not tracked" and "not asked" are different answers.
**Can I pass one product URL instead of a whole store?** Yes — a `/products/<handle>` URL returns exactly that one product, with the same fields (and the same per-result price) as it would have from a full-store run. Fixed in v0.1.33: Shopify serves single products through a different, older serializer than the bulk feed, and two of its quirks used to corrupt that path — a comma-joined `tags` string instead of a list, and no per-variant availability flag at all.
**When is `available` `null` instead of `true`/`false`?** Only when the store publishes nothing to decide it from: stock tracking is on for the variant but quantities are withheld. Untracked variants and stores that allow overselling are reported `true`, tracked ones as `inventoryQuantity > 0`, and `detailLevel: "full"` resolves the remaining unknowns from the storefront's own `.js` route at no extra request. `null` always means "this store doesn't say", never "sold out".
**Can I tell if a product is sold as a subscription, and how big the discount is?** Yes, set `detailLevel: "full"` — `hasSubscriptionOption` and `subscriptionPlans` cover both Shopify's own native subscriptions and app-backed ones (e.g. Recharge), including each plan's delivery frequency and percentage discount, read straight from the storefront's own `.js` route.
**Can I tell if a product has a minimum order quantity or case-pack size (wholesale/B2B)?** Yes, set `detailLevel: "full"` — each variant's `quantityRule` reports a real `{min, max, increment}` only when the store has actually configured one; Shopify's meaningless universal default (order 1 at a time, no cap) comes back as `null`, not a fake "rule".
**Does it bypass password-protected or dev stores?** No — only publicly reachable catalogs are read, same as a logged-out shopper would see.
**What happens if a store's storefront has a transient connection blip mid-scan?** Every request is retried up to 3 times on a connection-level failure (measured at roughly 1 fresh request in 4 for HTTP/2 faults, 2026-09-21) before that store is marked errored in the status message — a single blip no longer silently truncates a multi-page store to whichever page it reached.
**A store returned nothing — how do I tell "empty catalog" from "wrong URL"?** The run log says which, by name. Shopify answers `products.json` with a status code that identifies the problem, and as of v0.1.43 each one gets its own message: **401** the storefront is password-protected ("Opening soon" page up), **402** the store is frozen or closed (lapsed plan), **404** there is no Shopify store at that domain (usually a typo), **403** a bot check or geo/IP block (we already auto-retry through a few residential IP sessions before reporting this — see the FAQ below), **429** rate-limited. Only a genuine `200` carrying `{"products":[]}` is reported as an empty store or collection, and that one is re-checked on three separate attempts before we believe it. Nothing is charged in any of these cases.

**Every store returns 403 — should I turn the proxy on?** Usually it is not your URLs and not the store: Shopify's edge blocks on the *reputation of the requesting IP*, and that verdict changes from run to run on shared cloud IPs. Measured 2026-11-05 on one build inside four minutes, proxy OFF (the default): `allbirds.com`, `brooklinen.com`, `rothys.com` and `nativecos.com` returned 403; `bombas.com` returned 429; `deathwishcoffee.com` returned products on two runs and then 403 on a third three minutes later, with no input change. So a fleet-wide 403 does **not** reliably tell you anything about the stores you passed — the same URL can succeed and fail minutes apart. (An earlier version of this FAQ said a direct connection reliably got through and only Apify's shared **datacenter** proxy pool was blocked. That was measured 2026-11-04 and no longer holds — as of 2026-11-05 the direct path gets blocked intermittently too, so treat both paths as subject to the same IP-reputation check.) **You don't need to do anything about it yourself**: a direct-connection 403 now auto-escalates through up to 3 fresh residential IP sessions before the run reports a failure for that store, which clears the large majority of cases where one particular IP (not the store) was the problem — verified live: 10/10 test runs against a store that 403s its direct connection every time completed with full results after escalating. A store that 403s on *every* attempt including all of those residential retries is the rarer case that is genuinely bot-protected at the store level (`gymshark.com` is, for example); re-running later sometimes clears even that, since Shopify/Cloudflare's edge rules change. Nothing is charged for a 403 or a 429 either way, including the internal retries.
**I automate this — how do I tell those cases apart in code, not by reading the log?** Every run writes a `RUN_SUMMARY` record to its own key-value store (`GET https://api.apify.com/v2/actor-runs/<runId>/key-value-store/records/RUN_SUMMARY`), and the same object is included in the `webhookUrl` POST as `sources`. It holds one entry per URL you passed, with `status` — `ok`, `empty`, `filteredOut`, `duplicate`, `error`, `badUrl`, `watchBaselined`, `watchNoChanges` or `notReached` — plus `scanned`, `delivered`, `duplicates`, `complete` and a `reason` string. `scanned` and `delivered` are separate fields on purpose: an empty dataset stops being ambiguous, and a store that errors today and succeeds tomorrow does not look like a catalog that suddenly appeared. `notReached` specifically means the run stopped (time limit, `maxResults`, charge budget) before that URL was fetched at all — never confuse it with an empty store. As of v0.1.22, a `notReached` URL is also named directly in the run's status message (`Issues: <url> (not reached — run stopped early, see RUN_SUMMARY)`) — previously it only appeared in the log and `RUN_SUMMARY`, so a run that pushed products with one URL silently skipped behind a `maxResults`/time/charge cap looked clean from the status message alone.

**Just tell me if the run got everything — one field.** The same `RUN_SUMMARY` record carries run-level `complete`, `incompleteReason`, `incompleteDetail` and `sourcesIncomplete` alongside `sources`. `complete` is `true` only when every URL you passed was read to the end *and* the run did not stop on the clock; `false` otherwise; `null` if no URL was processed at all. `incompleteReason` names the first shortfall — `max-products-per-store`, `max-results`, `time-budget`, `charge-limit`, `source-request-failed`, `bad-url`, `not-reached`, `empty-source`, `filtered-out`, `duplicate-source`, `watch-baselined` or `watch-no-changes` — and `incompleteDetail` says which URL and how many others. Note that `complete` is separate from `status` on purpose: a URL with `status: "ok"` is still incomplete if `maxProductsPerStore` (default **500**) cut its sweep short, which is the most common reason a large catalog comes back partial. This is the same `complete` / `incompleteReason` / `incompleteDetail` contract every FetchSmith Actor's `RUN_SUMMARY` carries, so one pipeline can check all of them the same way.

**Can I search a store for a keyword instead of scraping its whole catalog?** Yes, set `searchQuery` (e.g. `"wool"` or `"merino sweater"`) — it matches against title, vendor, product type and tags (all words must appear). It's a client-side filter, not a separate search API call, since Shopify's public feed has no server-side keyword search; a store with a large catalog still takes the same time to scan as an unfiltered run of the same store.
**How does a price window handle products whose variants have different prices?** A product is a price *range*, not a single price, so `minPrice`/`maxPrice` keep a product when its range overlaps your window: a $40-$120 hoodie matches `maxPrice: 50` (there is a buyable $40 variant) and also `minPrice: 100` (there is a $120 one). Prices are compared in the store's own currency — the `currency` output field tells you which — and no FX conversion is done, so the same window means different things on a USD and a EUR store. Products whose price can't be read at all are dropped rather than passed through, so a price-filtered run never returns a row the filter couldn't be evaluated against. Both bounds are applied *before* billing, so you are never charged for a product the filter removed.
**Can I get only the discounted products?** Yes, set `onSaleOnly: true` — it keeps products where the store set a compare-at price above the current price (identical to the `isOnSale` output field), with `discountPercent` on every row computed from the cheapest variant's own compare-at price. Products the store never put a compare-at price on are excluded, since there is no evidence of a discount to report.
**Can I set a minimum discount instead of "any discount"?** Yes, set `minDiscountPercent` (e.g. `30` for "30% off or more") — it reads the same `discountPercent` field `onSaleOnly` checks, just with a floor. A product with no compare-at price at all is excluded, not treated as a 0% discount that happens to fail the floor.
**Can I filter to specific brands or product types without a keyword search?** Yes — `vendors` and `productTypes` are OR-matched lists (e.g. `vendors: ["Allbirds","Rothy's"]` returns products from either brand), unlike `searchQuery`, which is AND-of-words across a single combined haystack and can't express "brand A or brand B". Both read fields already on every row, so there's no extra request cost, and both combine with every other filter (including each other) via AND — only the entries within one list are OR'd together.

**Can I monitor a store for price drops instead of re-scraping the whole catalog?** Yes — set `watchLabel` (optionally narrowed with `watchEvents`). The first run under that label is a free baseline that returns nothing; every later run returns only new products, price changes, sale starts/ends, stock flips and products that have disappeared from the catalog (`delisted`), each row carrying `watchChange`, `previousPriceMin`, `previousAvailable`, `previousIsOnSale` and `priceChange`. Products that didn't move are never pushed and never charged, so a scheduled daily watch costs nothing on days when the store doesn't change. See *Watch mode* above.

**Does the watch catch a markdown that doesn't change the listed price?** Yes — `wentOnSale`/`saleEnded` fire when a store adds or removes a compare-at ("was") price while the current price itself stays put, which `priceDrop`/`priceIncrease` alone would miss entirely. If the current price changes too, that's reported as `priceDrop`/`priceIncrease` instead (one event per row, not two) and the row's `previousIsOnSale` still shows whether the sale flag itself flipped.

**What happens if I start two runs on the same `watchLabel` at once?** As of v0.1.19, the second one fails immediately instead of running: starting a run while another run under the same `watchLabel` is still in progress used to let both runs read the same baseline, independently decide the same products were new/changed, and charge you twice for them. Now the first run to start a label takes a lock, and a second run on the same label fails fast with a clear message before reading or charging anything. The lock self-expires after 2 hours in case a run crashes without releasing it, so a stuck lock can't wedge a label forever. This only matters if you run the same `watchLabel` concurrently (e.g. two schedules on the same label, or a manual retry before the first run finished) — a normal single schedule per label is unaffected.

### Related guides

Engineering write-ups behind this Actor:

- [Every Shopify store's catalog is public JSON — no login, no browser](https://fetchsmith.com/blog/shopify-catalog-products-json-no-login)
- [Shopify's bulk products.json hides stock counts and barcodes — the per-product endpoint doesn't](https://fetchsmith.com/blog/shopify-inventory-barcode-per-product-json)
- [HTTP-only vs headless browser scraping: a timed benchmark](https://fetchsmith.com/blog/http-only-vs-headless-browser-scraping-cost)
- [Four ways an invisible character makes a scraper return zero rows](https://fetchsmith.com/blog/invisible-characters-return-zero-rows)
- [We nearly charged our own buyers twice for rows they'd already paid for](https://fetchsmith.com/blog/watch-baseline-eviction-rebilling) — a capped watch-mode baseline can silently evict old-but-current ids on a high-volume run, re-delivering (and re-billing) rows already paid for.
- [Eight ways an "only new since last run" watch mode silently stops working](https://fetchsmith.com/blog/incremental-api-watch-mode-four-traps) — the fleet-wide survey of watch-mode failure shapes across all nineteen incremental Actors, this one included.

Only publicly available data is collected. Support: support@fetchsmith.com · Hosted API: https://fetchsmith.com/tools/shopify-products-scraper

Source code: https://github.com/Fetchsmith/fetchsmith/tree/main/actors/shopify-products-scraper

# Actor input Schema

## `storeUrls` (type: `array`):

Shopify store homepages, collection URLs or product URLs, e.g. https://www.allbirds.com or https://www.allbirds.com/collections/mens.

## `searchQuery` (type: `string`):

Only return products whose title, vendor, product type or tags contain every word in this query (case-insensitive, Unicode-normalized so accented words like "café" match regardless of composed/decomposed input form). Applied client-side after fetching each store's catalog — Shopify's public feed has no server-side search, so this still walks the full store (same request cost as without it) but needs no collection URL guessing. Ignored for direct product URLs.

## `vendors` (type: `array`):

Only return products whose vendor/brand contains one of these (case-insensitive, OR logic — e.g. \["Allbirds","Rothy's"] matches either). Combines with every other filter using AND; only the entries within this list are OR'd together. Zero extra requests (vendor is already on every row). Ignored for direct product URLs.

## `productTypes` (type: `array`):

Only return products whose product type contains one of these (case-insensitive, OR logic), e.g. \["Shoes","Sandals"]. Combines with every other filter using AND; only the entries within this list are OR'd together. Zero extra requests. Ignored for direct product URLs.

## `maxProductsPerStore` (type: `integer`):

Stop after this many products for each store URL. In watch mode ("Watch label" set) this caps how many products are SCANNED per store instead — unchanged products aren't returned, so the diff has to walk the feed; raise it above your store's catalog size or changes further down the feed are missed.

## `includeDescription` (type: `boolean`):

Include the product description as plain text.

## `includeVariants` (type: `boolean`):

Include the full variants array (SKU, price, options, availability). Keep this on when detailLevel is "full": the per-variant paid fields (barcode, inventoryQuantity, inventoryManagement, inventoryTracked, inventoryPolicy, quantityRule) are returned inside this array, so turning it off drops them from every row while each product is still charged one productDetail event. The per-product detail fields (seoTitle, seoDescription, ratingValue, reviewCount, hasSubscriptionOption, subscriptionPlans, totalInventory) are unaffected.

## `onlyAvailable` (type: `boolean`):

Skip products where no variant is available.

## `minPrice` (type: `integer`):

Only return products a shopper could buy at or above this price, i.e. whose highest variant price is >= this value. Combined with "maxPrice" this is a price window, and a product matches when its own variant price range overlaps that window (a $40-$120 hoodie matches minPrice 100 because its $120 variant qualifies). In the store's own currency (see the `currency` output field) - no FX conversion. Filtered-out products are never charged. Apify's input schema has no decimal number type, so the Console editor takes whole units; API/JSON callers may pass decimals (e.g. 19.99) and they are honoured exactly.

## `maxPrice` (type: `integer`):

Only return products a shopper could buy at or below this price, i.e. whose lowest variant price is <= this value. See "Min price" for how a product's variant price range is matched against the window. In the store's own currency - no FX conversion. Filtered-out products are never charged. Apify's input schema has no decimal number type, so the Console editor takes whole units; API/JSON callers may pass decimals (e.g. 19.99) and they are honoured exactly.

## `onSaleOnly` (type: `boolean`):

Only return products currently discounted, i.e. where a compare-at price is set above the current price (the same condition as the `isOnSale` output field). Products a store never set a compare-at price on are excluded. Filtered-out products are never charged.

## `minDiscountPercent` (type: `integer`):

Only return products discounted by at least this percentage, i.e. whose `discountPercent` output field is >= this value (computed from the cheapest variant's own compare-at price). Products with no discount at all (`discountPercent: null`) are excluded, not treated as 0%. Stricter than "On sale only", which accepts any discount. Filtered-out products are never charged.

## `maxResults` (type: `integer`):

Overall cap on the number of products across all stores.

## `proxyConfiguration` (type: `object`):

Route storefront requests through Apify Proxy. OFF by default, and you should usually leave it off: as of 2026-11-04 many storefronts answer 403 to Apify's shared datacenter pool while returning products normally over a direct connection, so turning the proxy on can cost you products rather than win them. Turn it on when you actually need it — a store rate-limits you by IP (429), or you want a country-specific RESIDENTIAL group to read that country's prices and currency. If a store returns 403 either way, it is bot-protected at the store level and residential is the only thing that may help.

## `detailLevel` (type: `string`):

"Basic" (default) uses only the store's bulk products.json feed — one request per page of products. "Full" additionally fetches each product's live page and its per-product .js route to read barcodes, inventory counts (inventoryQuantity / totalInventory), quantity rules (min/max/case-pack), subscription plans, SEO title/description and the star-rating summary — none of which the bulk feed carries. The two extra requests run in parallel and are charged as a single event per product. How much stock/subscription data exists is a per-store setting; unpublished values come back as null.

## `watchLabel` (type: `string`):

Set a name (e.g. "my-competitor-prices") to turn this run into a catalog watch. The FIRST run under a given label + filter set records each product's price, availability and sale status and returns NOTHING (0 products charged). Every run after that returns ONLY products that are new, changed price, started or ended a sale, went out of stock, came back in stock or were removed from the catalog (delisted) — so you can schedule this Actor daily and pay only for what actually moved, instead of re-buying the whole catalog. Each returned row carries watchChange / watchChanges / previousPriceMin / previousAvailable / priceChange alongside the normal product fields. Adding a store URL to an existing label baselines just that store; changing a filter (onlyAvailable / searchQuery / vendors / productTypes / minPrice / maxPrice / onSaleOnly / minDiscountPercent) starts a fresh baseline. Leave empty for a normal one-off scrape.

## `watchEvents` (type: `array`):

Optional. Only report these kinds of change in watch mode — e.g. pick "Price drop" alone to build a discount tracker and never pay for anything else. Leave empty for all eight. "Went on sale"/"Sale ended" fire only when a compare-at price is added or removed with the current price unchanged — a price change that also flips the sale flag is already reported as "priceDrop"/"priceIncrease" (check the row's own `isOnSale` field for the sale state in that case). "Delisted" is only reported for a store whose whole feed this run walked end to end (see "Max products per store"). Ignored when "Watch label" is empty.

## `webhookUrl` (type: `string`):

Optional. An http(s) URL to POST a small JSON summary to when the run finishes — products pushed, the per-change-type counts if a watch label is set, and the run's dataset ID so you can fetch the results. A convenience for callers who want a completion ping without configuring an Apify platform webhook per Task. Best-effort: a failed or slow webhook is logged as a warning and never fails the run or affects charging. Leave empty to skip.

## Actor input object example

```json
{
  "storeUrls": [
    "https://www.allbirds.com"
  ],
  "maxProductsPerStore": 500,
  "includeDescription": true,
  "includeVariants": true,
  "onlyAvailable": false,
  "onSaleOnly": false,
  "maxResults": 5000,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "detailLevel": "basic",
  "watchEvents": []
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "storeUrls": [
        "https://www.allbirds.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("fetchsmith/shopify-products-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "storeUrls": ["https://www.allbirds.com"] }

# Run the Actor and wait for it to finish
run = client.actor("fetchsmith/shopify-products-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "storeUrls": [
    "https://www.allbirds.com"
  ]
}' |
apify call fetchsmith/shopify-products-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fetchsmith/shopify-products-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/YIoVduwGC2c0ag0ms/builds/IxgcS8BFojKeuYLrJ/openapi.json
