Shopify Store Product Scraper avatar

Shopify Store Product Scraper

Pricing

from $0.14 / 1,000 variants

Go to Apify Store
Shopify Store Product Scraper

Shopify Store Product Scraper

Pricing

from $0.14 / 1,000 variants

Rating

0.0

(0)

Developer

Galdrcode

Galdrcode

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Categories

Share

Shopify Product Scraper — One Row Per Variant

Scrape the full product catalog of any number of Shopify stores at once — one clean row per variant, with prices, discounts and stock status.

Paste in a list of domains. Get back a flat table you can open in Excel, Google Sheets, or load straight into a database. No API keys, no store login, no configuration.


Real sample output

Five actual rows from a live run against allbirds.com, gymshark.com and kith.com:

store_domainproduct_titlevariant_titleskupricecompare_at_pricediscount_pcton_saleavailable
allbirds.comTrino® Tubers - Navy NightS (W5-7)ST1UNNT1014.0016.0075.0truefalse
allbirds.comTrino® Tubers - Navy NightM (W8-10 / M8)ST1UNNT1024.0016.0075.0truefalse
gymshark.comGymshark Everyday Holdall Small - BlackOne SizeI3A1V-BB2J-OS44.00falsetrue
kith.comKith Baby & '47 for the New York Yankees Acrylic BeanieOS2000376136210.00falsetrue
allbirds.comFree Returns Coverage$0.80x-redo0.800.80falsetrue

Note the last row: a compare-at price equal to the price is not counted as a discount. on_sale stays false and discount_pct stays empty, so your "what's discounted" filter doesn't fill up with noise.

Every row also carries vendor, product_type, tags[], options{}, image_url, grams, product_url, published_at and updated_at.


Why this one

Most Shopify scrapers return one row per product, with variants buried in a nested array. That is unusable in a spreadsheet — you cannot sort by price, filter by SKU, or pivot on size when every product is a single cell containing twenty variants.

This Actor returns one row per SKU. In the run above, 75 products became 523 rows — the rows you actually wanted.

Bulk multi-domainPass 1 store or 500. Built for competitor sets and market scans, not one-off lookups.
Variant-level flat outputOne row per SKU. Spreadsheet-ready, no post-processing, no JSON flattening step.
Detection + graceful skipNon-Shopify, password-protected and bot-protected domains are reported in a run summary with a reason — never silently dropped, and never charged.
Discount + inventory signalsdiscount_pct and on_sale are computed for you, available is surfaced per variant. The analysis you were going to do in Excel is already done.

Input

FieldTypeDefaultWhat it does
storeUrlsarrayrequiredStore domains. example.com, https://example.com, or any page URL on the store — anything after the host is ignored.
maxProductsPerStoreinteger0Cap per store. 0 = unlimited. Counts products, not variants.
includeOutOfStockbooleantrueWhen off, out-of-stock variants are dropped and not charged.
onlyOnSalebooleanfalseWhen on, returns only discounted variants. Everything else is dropped and not charged.
includeImagesbooleantrueAdds image_url — the variant's own image, falling back to the product's first.
maxConcurrencyinteger5Requests in flight across all stores. Each store paginates one page at a time.
proxyConfigurationobjectoffUsually unnecessary — the product feed has no anti-bot. Turn on only for stores behind a checkpoint.

The example input is prefilled with three real stores, so the Actor produces output on the first click without you typing anything.


Output

One row per variant in the dataset. Alongside it, a run summary is written to the key-value store under RUN_SUMMARY:

{
"stores_requested": 4,
"stores_scraped": 2,
"stores_skipped": 2,
"total_variants": 31,
"scraped": [
{ "store_domain": "allbirds.com", "currency": "USD", "products": 3, "variants": 28 }
],
"skipped": [
{ "store_domain": "example.com", "reason": "not_shopify" },
{ "store_domain": "bombas.com", "reason": "bot_protected" }
]
}

Skip reasons: not_shopify, password_protected, bot_protected, empty_catalog, robots_disallowed, request_failed.


How it works, and its limits

The Actor reads Shopify's public products.json feed — the same data the storefront serves to anyone. No login is bypassed and no customer data is touched. robots.txt is respected and each store is paginated sequentially to stay polite.

Known limits, stated up front:

  • 25,000 products per store. Shopify's public feed refuses to page beyond this. Larger catalogs are truncated.
  • No barcode. It is not in the public feed — it exists only in Shopify's authenticated Admin API. No scraper reading products.json can return it.
  • currency comes from /meta.json. If a store blocks that endpoint, currency is null and everything else still works.
  • No collection membership. Shopify's feed does not carry it, and deriving it costs one request per collection — real stores have 250+, so it is neither cheap nor complete. Left out rather than shipped half-working.
  • Password-protected stores return nothing by design. They are reported, not charged.

Pricing

Pay-per-event. You are charged per variant row actually returned to you.

Because this Actor returns a row per SKU rather than per product, compare the per-catalog total rather than the headline unit price — a store with 300 products and 5 sizes each is 1,500 rows here versus 300 elsewhere, for the same underlying data at finer granularity.

Filtered-out variants (includeOutOfStock: false, onlyOnSale: true) and every skipped domain cost nothing.


Other Actors

Found a store that should work but doesn't? Open an issue on the Actor — the skip reason in RUN_SUMMARY tells me exactly what to look at.