Shopify Product Catalog Scraper avatar

Shopify Product Catalog Scraper

Pricing

from $1.00 / 1,000 products

Go to Apify Store
Shopify Product Catalog Scraper

Shopify Product Catalog Scraper

Exports the complete product catalog of any public Shopify store - every product, every variant, prices, options and images - and verifies the export against the store's own published product count so you know whether you got all of it.

Pricing

from $1.00 / 1,000 products

Rating

0.0

(0)

Developer

Nyx Ward

Nyx Ward

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

13 days ago

Last modified

Categories

Share

Read the full product catalog and every price of the Shopify stores you name — one store or two hundred — and know, per store, whether you got all of it.

You give it a list of store domains. It gives you one clean row per product, with the store on every row, and a run report saying, per store, whether the export matched the number of products the store itself says it has published.

Built for watching a set of stores

storeUrls is a list, not a field. One run covers a whole watchlist, and every row carries its store, currency, minPrice, maxPrice and scrapedAt, so a single CSV export pivots straight into a price sheet.

{
"storeUrls": [
"allbirds.com",
"brooklinen.com",
"tentree.com",
"chubbiesshorts.com"
],
"maxProductsPerStore": 0
}

A store that cannot be read does not stop the run and costs you nothing per product. It comes back with a status and a reason, and the other stores finish.

Why "complete" is the point

If you are comparing today's prices to last week's, a silently short read is worse than a failure. Twelve products missing from an export look exactly like twelve products delisted. You act on a signal that never happened.

Shopify's storefront product feed makes that easy to get wrong. It has no cursor. It is paged by offset, it carries no pagination metadata, and it silently caps every page at 250 items no matter what you ask for. A catalog edited while it is being read can shift under the offset, and nothing in the response tells you it happened.

This Actor reads the store's own published product count first, deduplicates every product by id, and compares the two at the end. If the count is short it walks the catalog a second time and merges the results. Then it tells you the outcome either way:

"complete": true, "expectedProductCount": 293, "productCount": 293

No other tool in this category reports that number. Most report nothing at all. It is the difference between a price sheet you can act on and one you have to spot-check by hand.

Input

FieldRequiredWhat it does
storeUrlsyesOne or more store domains. allbirds.com, https://allbirds.com/collections/mens and weareallbirds.myshopify.com all work — only the host is used
maxProductsPerStorenoSafety cap per store. 0 (default) means no cap
proxyConfigurationnoApify proxy. If a store answers BLOCKED, re-run it with the RESIDENTIAL group

This Actor never discovers stores on its own. It reads the ones you name, and only those.

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

Output — one row per product

{
"schemaVersion": 1,
"store": "allbirds.com",
"storeUrl": "https://www.allbirds.com",
"currency": "USD",
"scrapedAt": "2026-08-30T21:00:00Z",
"productId": 7218356060240,
"handle": "mens-strider-explore",
"title": "Men's Strider Explore - Natural Black",
"url": "https://www.allbirds.com/products/mens-strider-explore",
"vendor": "Allbirds",
"productType": "Shoes",
"tags": ["classic", "mens"],
"publishedAt": "2025-10-01T09:35:00-07:00",
"createdAt": "2025-09-10T12:47:47-07:00",
"updatedAt": "2026-08-30T14:28:13-07:00",
"bodyHtml": "<p>...</p>",
"optionNames": ["Color", "Size"],
"featuredImageUrl": "https://cdn.shopify.com/.../left.png",
"imageUrls": ["https://cdn.shopify.com/.../left.png"],
"imageCount": 5,
"variantCount": 13,
"minPrice": 130.0,
"maxPrice": 130.0,
"available": true,
"variants": [
{
"variantId": 41334293889104,
"title": "Natural Black / 9",
"sku": "A11768M090",
"price": 130.0,
"compareAtPrice": null,
"available": true,
"position": 3,
"grams": 954,
"requiresShipping": true,
"taxable": true,
"options": { "Color": "Natural Black", "Size": "9" },
"imageUrl": "https://cdn.shopify.com/.../left.png"
}
]
}

The four things this saves you writing yourself:

  1. Option names resolved. The feed keeps option names on the product and option values on the variant, joined by nothing but array position. Here they arrive as {"Color": "Natural Black", "Size": "9"}.
  2. Prices as numbers. The feed sends "130.00" as a string, with no currency anywhere. Here they are floats, with the store's currency on every row.
  3. Variant images resolved. The variant→image link lives in two different places and stores use one or the other, sometimes both in the same catalog. Both are read.
  4. Deduplicated and counted. See above.

The run report

The dataset stays one shape so a CSV export is usable. Everything about a store rather than a product goes to the key-value record OUTPUT:

{
"scrapedAt": "2026-08-30T21:00:00Z",
"storesRequested": 2,
"storesRead": 1,
"productsExported": 293,
"stores": [
{ "store": "allbirds.com", "status": "OK", "complete": true,
"expectedProductCount": 293, "productCount": 293,
"duplicatesDropped": 0, "pagesFetched": 2, "passes": 1,
"message": "Exported 293 products, which matches the 293 the store itself reports published. Catalog complete." },
{ "store": "example.com", "status": "NOT_SHOPIFY",
"message": "No Shopify product feed at this domain (HTTP 404...). Check the domain." }
]
}

Every way a store can end

StatusWhat it means
OKThe catalog was read. Check complete for whether it was whole
NOT_SHOPIFYNo Shopify product feed at this domain
HEADLESS_STOREFRONTA Shopify store whose product feed is switched off — a headless or custom storefront. No tool can read it through this endpoint
PASSWORD_PROTECTEDBehind Shopify's password gate; the catalog is not public
BLOCKEDThe store refused the request (HTTP 401/403/430). Re-run with the RESIDENTIAL proxy group
RATE_LIMITEDThrottled, and still throttled after three attempts
UNREACHABLENo answer after three attempts
ROBOTS_DISALLOWEDThe store's robots.txt disallows this endpoint. Skipped, and not charged

Limits, stated plainly

  • 25,000 products per store. That ceiling is Shopify's, not ours: past it the endpoint answers HTTP 400 Page * Limit exceeds the 25000 limit. Larger catalogs come back with depthLimited: true and complete: false.
  • Public catalog fields only. Inventory quantities, cost, barcodes and the rest of the Admin API are not in this feed and are not invented here.
  • Published products only — what a visitor to the storefront can see.

What it does not do

Stated up front so you do not have to find out:

  • No change detection between runs. It exports the catalog as it stands. Diffing consecutive runs is yours to do — schedule the runs and compare the datasets, or filter on updatedAt.
  • No store discovery. It reads the domains you name and only those.
  • No headless storefronts. A Shopify store that has switched the feed off comes back HEADLESS_STOREFRONT. No tool reads those through this endpoint.

How many stores actually expose the feed

Measured 2026-09-03 on a sample of 12 domains, n = 12: 8 exposed a readable feed, 1 was a headless storefront, 3 had no Shopify feed at all.

That sample was assembled to exercise this scraper — large well-known brands, one negative control, one at the size ceiling — not to estimate the Shopify population, and it should not be read as a coverage rate for it. Run it on your own list with maxProductsPerStore: 1 and the run report answers the question for your domains, one page per store.

Pricing

You are charged per product exported. A store that cannot be read costs you nothing beyond the run itself.

Support

Open an issue on the Actor page. This Actor is maintained by Nyx Ward.