Shopify Store Products Scraper
Pricing
$1.00 / 1,000 products
Shopify Store Products Scraper
Export every product from any public Shopify store: prices, variants, SKUs, barcodes, stock status, images and collections. Works with custom domains and explains why when a store can't be read.
Pricing
$1.00 / 1,000 products
Rating
0.0
(0)
Developer
Om Ray
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
5 days ago
Last modified
Categories
Security
Limited permissionsShare
This actor exports the product catalog of any public Shopify store into a dataset, one row per product. Give it a store URL or a bare domain such as allbirds.com. Custom domains work, so you don't need to know the store's .myshopify.com address.
It reads the JSON feeds every Shopify storefront serves to the public: /products.json for the catalog, /collections.json and /collections/<handle>/products.json for collections, and /products/<handle>.js for stock per variant. It doesn't log in or run a browser.
Input
{"stores": ["https://www.allbirds.com"],"maxItems": 30,"includeVariantAvailability": true,"includeCollections": false}
stores: store URLs or domains. A collection URL such ashttps://store.com/collections/salelimits that store to the collection. The same store entered twice is read once, and a product is never saved or charged twice in one run.maxItems: stop after this many product rows in total, across all stores. The default is 30, so raise it when you want a whole catalog.collections: optional collection handles or URLs. When set, only products in those collections are saved, for every store in the list.includeVariantAvailability: on by default. Fetches each product's.jspage to get stock status per variant, plus barcodes and inventory counts when the store shows them.includeCollections: off by default. Walks every collection to fill thecollectionsfield on each product.maxRunMinutes: the run stops cleanly after this many minutes and keeps what it saved. The default is 60. It also stops about 45 seconds before the Apify run timeout, so the summary always gets written.
Output
Each product row has the same fields every run. This is a trimmed row from a real local run against allbirds.com on 1 November 2026 (cut down to the first variant, two images and three tags, with the description shortened):
{"status": "ok","message": null,"inputUrl": "https://www.allbirds.com","storeDomain": "www.allbirds.com","storeName": "Allbirds","id": 6858599596112,"handle": "womens-tree-dasher-relay-blizzard-hazy-cobalt","title": "Women's Tree Dasher Relay - Blizzard/Hazy Cobalt (Clarity Blue Sole)","url": "https://www.allbirds.com/products/womens-tree-dasher-relay-blizzard-hazy-cobalt","vendor": "Allbirds","productType": "Shoes","tags": ["allbirds::carbon-score => 5.47", "allbirds::cfId => color-womens-tree-dasher-relay-blzz-hzy-cblt-clrty-bl", "allbirds::complete => true"],"createdAt": "2023-07-15T21:42:13-07:00","updatedAt": "2026-11-01T08:09:07-08:00","publishedAt": "2026-09-25T16:49:33-07:00","descriptionText": "Meet The Tree Dasher Relay...","descriptionHtml": "<p>Meet The Tree Dasher Relay...","images": ["https://cdn.shopify.com/s/files/1/1104/4168/files/A10691_24Q1_Tree-Dasher-Relay-Blizzard-Hazy-Cobalt-Clarity-Blue_PDP_LEFT.png?v=1776183508","https://cdn.shopify.com/s/files/1/1104/4168/files/A10691_24Q1_Tree-Dasher-Relay-Blizzard-Hazy-Cobalt-Clarity-Blue_PDP_BACK.png?v=1776183507"],"options": [{ "name": "Size", "position": 1, "values": ["5", "5.5", "6", "6.5", "7", "7.5", "8", "8.5", "9", "9.5", "10", "10.5", "11"] }],"collections": null,"currency": "USD","priceMin": 67,"priceMax": 67,"compareAtPriceMin": 135,"compareAtPriceMax": 135,"available": false,"availabilitySource": "product.js","stockCheck": "ok","variantCount": 13,"variants": [{"id": 40234510123088,"title": "5","sku": "A10692W050","barcode": "196942084960","price": 67,"compareAtPrice": 135,"available": false,"inventoryQuantity": 0,"inventoryPolicy": "deny","inventoryManagement": "shopify","weightGrams": 638,"optionValues": { "Size": "5" },"imageUrl": null,"requiresShipping": true,"taxable": true}],"scrapedAt": "2026-11-01T16:09:10.248Z"}
Fields
| Field | Meaning |
|---|---|
status | ok on charged product rows. product-page-missing marks a free product row (see below). Any other value marks a free status row. |
message | Plain explanation on status rows. Null on product rows. |
inputUrl | The store as you typed it. |
storeDomain | Domain the products came from, after redirects. |
storeName | Store name from /meta.json, when the store publishes it. |
id, handle, title, url | Product ID, handle, title and page URL. |
vendor, productType, tags | As set by the store. |
createdAt, updatedAt, publishedAt | Dates as the store reports them, with its time zone. |
descriptionText | Description as plain text, with paragraphs and list items on their own lines. |
descriptionHtml | Description HTML exactly as served. |
images | Every product image URL. |
options | Options such as Size or Color, with their values. |
collections | Collection handles that contain the product. Null unless includeCollections is on. With a collection filter, only the filtered collections are checked. |
currency | Currency code from /cart.js, then /meta.json, then the home page. |
priceMin, priceMax | Lowest and highest variant price. |
compareAtPriceMin, compareAtPriceMax | Range of the "was" prices, among variants that have one. |
available | True when at least one variant can be bought. |
availabilitySource | product.js when stock came from the product's .js page, products.json when it came from the catalog feed. |
stockCheck | How the .js stock check went: ok, off, product-page-404, rate-limited, timeout or failed. Anything other than ok means stock came from the catalog feed. The run summary counts each value. |
variantCount | Number of variants. |
variants | Per variant: id, title, sku, barcode, price, compareAtPrice, available, inventoryQuantity, inventoryPolicy, inventoryManagement, weightGrams, optionValues, imageUrl, requiresShipping, taxable. |
scrapedAt | When the row was saved, in UTC. |
inventoryQuantity is only filled when the store actually exposes it in its .js feed. Many stores hide it, and then it stays null rather than showing a guess. The same goes for barcode. Prices are numbers in the store currency, such as 67 for $67.00.
When a store can't be read
You never get a silent empty dataset. If a store can't be read, the run still succeeds and saves one free row with a status and a message that says why:
not-shopify: the site doesn't serve Shopify's product feed and shows no Shopify code.password-protected: the storefront asks for a password.products-json-disabled: the site runs on Shopify but has turned off/products.json.store-unavailable: Shopify answers, but no open store exists at that address.blocked: the site answered 403 or 503, usually a firewall.rate-limited: the store kept answering 429 after six tries spread over about a minute.http-error: the site runs on Shopify but/products.jsongave some other error after retries.unreachable: the domain doesn't resolve or refuses connections.invalid-input: the entry isn't a URL or domain.duplicate-store: the entry is the same store as an earlier one in the list, so it isn't read or charged again.already-saved: every product this entry found, such as a collection of a store you already listed, was saved earlier in the run.collection-not-found: a requested collection handle doesn't exist.no-products: the store is readable but publishes no products.
A few more free rows explain why a store or run ended early:
incomplete:/meta.jsonreports more products than the feed and sitemap gave, or the feed stopped at 25,000 products or ignored page numbers. The message gives both counts.partial: some pages failed after retries. The products read before that are kept.max-items-reached,charge-limit,time-limit: the run hitmaxItems, your spending limit or the time limit.skipped: a later store in the list wasn't read because the run had already stopped.collections-incomplete: not every collection could be read in time for thecollectionsfield.
When www. fails the actor also tries the domain without it, and the other way round. If the home page names the store's .myshopify.com address, it tries that too. Some brands put a custom front end on their domain and keep Shopify elsewhere: www.gymshark.com is read through gymshark.com, and ruggable.com through ruggable.myshopify.com.
Some stores list products in /products.json whose product pages answer 404. Those rows are saved with status product-page-missing and stockCheck set to product-page-404, and they are not charged, since they are probably not for sale. Their stock comes from the catalog feed.
The run summary, with one entry per store and a count of each stockCheck value, is saved as OUTPUT in the key-value store.
If Apify migrates or restarts the run, the actor reads the product IDs already in the dataset and carries on from there. Products saved before the restart are not saved or charged again.
Price
You pay $0.001 per product row with status ok, which is $1 per 1,000 products. Status rows and product-page-missing rows are free. The run stops when it reaches maxItems or the spending limit you set on the run, whichever comes first.
Speed and limits
Shopify throttles each visitor to roughly one request per second once a short burst is used up. The catalog feed returns 250 products per request, so a run without the stock check is fast: all 10,410 products from gymshark.com took 82 seconds in a local test on 1 November 2026. With includeVariantAvailability on, the actor makes one extra request per product, at about one product per second after the first few dozen. All 356 Allbirds products took 6 minutes 20 seconds in the same test. Your times will vary with the store and with how busy its servers are. For catalogs in the tens of thousands, either turn the stock check off or raise maxRunMinutes.
Shopify's /products.json stops at page 100, which is 25,000 products. For bigger stores, or whenever /meta.json reports more products than the feed returned, the actor reads the store's product sitemap and fetches the missing products one by one.
includeCollections reads every collection on the store before saving products. Stores with hundreds of collections can spend several minutes on this, and the actor gives it at most half of the time limit.
Country stores such as ca.rhodeskin.com usually share one Shopify store with the main domain, so the actor treats them as the same store and its prices come from whichever address it read.
Only products published to the online store are visible. Prices are whatever the storefront shows a visitor from the run's location, which is usually the store's main market.
Run it locally
npm installnpx apify run --input-file examples/input.jsonnpm run check