WooCommerce Scraper
Pricing
from $1.20 / 1,000 products
WooCommerce Scraper
Scrape products, variations, categories and customer reviews from any WooCommerce store. Detects each store's available data surface and adapts, so it works whether the JSON API is open, disabled, or behind a firewall.
Pricing
from $1.20 / 1,000 products
Rating
0.0
(0)
Developer
Richard Feng
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Extracts products, variations, categories and customer reviews from any WooCommerce store. Give it a domain or any store URL. It also handles stores whose JSON API is disabled or firewalled, by falling back to the WordPress REST API, the product sitemap and the product pages themselves.
🚀 Quick start
The smallest useful input is a domain and a product cap:
{"domains": ["mystore.com"],"maxProducts": 100}
To include customer reviews and keep several stores from starving each other:
{"domains": ["mystore.com", "otherstore.co.uk"],"startUrls": [{ "url": "https://third-store.com/product-category/shoes/" }],"maxProducts": 500,"maxProductsPerStore": 200,"includeReviews": true,"maxReviewsPerProduct": 20}
Run it from the Apify Console, or from your own code with the Apify client:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('autofacts/woocommerce-scraper').call({domains: ['mystore.com'],maxProducts: 100,includeReviews: true,});const { items } = await client.dataset(run.defaultDatasetId).listItems();const products = items.filter((r) => r.variants);const reviews = items.filter((r) => r.reviewer);const categories = items.filter((r) => r.productsCount !== undefined);const summaries = items.filter((r) => r.host);
import osfrom apify_client import ApifyClientclient = ApifyClient(token=os.environ["APIFY_TOKEN"])run = client.actor("autofacts/woocommerce-scraper").call(run_input={"domains": ["mystore.com"],"maxProducts": 100,"includeReviews": True,})items = client.dataset(run["defaultDatasetId"]).list_items().itemsproducts = [r for r in items if "variants" in r]reviews = [r for r in items if "reviewer" in r]
curl -X POST "https://api.apify.com/v2/acts/autofacts~woocommerce-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H 'Content-Type: application/json' \-d '{ "domains": ["mystore.com"], "maxProducts": 100 }'
The dataset mixes four record kinds (see Output below). Tell them apart by the
field only that kind has: variants means a product, reviewer a review,
productsCount a category, host a store summary. In the Console the dataset has
Products, Variants and Reviews tabs.
❓ Is the site a WooCommerce store?
The Actor only works on WooCommerce (WordPress) stores, and it tells you when a URL is not one. To check a site yourself first, in order of reliability:
1. Ask the store's API. Open this in a browser, replacing the domain:
https://store.com/wp-json/wc/store/v1/products?per_page=1
| Response | Meaning |
|---|---|
a JSON array with one product (id, name, prices, …) | WooCommerce, API open. The best case. |
{"code":"rest_no_route", …} | WordPress, but no WooCommerce Store API. Either not WooCommerce, or the API is disabled. Check the page markers below. |
| a 404 HTML page | Possibly WordPress with plain permalinks. Try https://store.com/?rest_route=/wc/store/v1/products&per_page=1. |
| 403, a captcha or a "checking your browser" page | Firewalled. It may still be WooCommerce; the Actor tries the other surfaces. |
| anything else (a normal HTML page, connection error) | Probably not WordPress at all. |
2. Look at the page source (right click → View Page Source, or the browser's developer tools) of the home page or any product page. WooCommerce leaves these marks, and the Actor itself relies on the same ones:
woocommercein the<body class="…">list. A product page also hassingle-productandpostid-<number>; a category page hastax-product_cat.<meta name="generator" content="WooCommerce x.y.z">in the<head>(many themes remove it, so its absence proves nothing).- Asset URLs containing
/wp-content/plugins/woocommerce/. - On a product page, a
<form class="cart">or<form class="variations_form">, an?add-to-cart=<id>link, or awoocommerce-product-galleryelement.
3. URL shapes are only a hint. /product/…, /product-category/…, /shop/,
/cart/, /checkout/ and /my-account/ are WooCommerce defaults, but every one of
them can be renamed, and other platforms use some of the same words. Do not decide on
the URL alone.
4. Rule out look-alikes. A WordPress site can sell through something else:
- cart or checkout on
checkout.shopify.comor a*.myshopify.comdomain: Shopify with a WordPress front, not WooCommerce; edd-classes and/edd-URLs: Easy Digital Downloads;- an embedded
ecwidorbigcommercewidget: those platforms, not WooCommerce.
5. Or let the Actor check. Run it with the domain and "maxProducts": 1. A
WooCommerce store yields a free store summary record (with productCount, and
detail telling you whether it was read through the API or the HTML fallback) and one
product. Anything else ends with not a WooCommerce catalog URL: <url> or
no usable data surface on <host>: … in the log, and nothing is billed.
Browser extensions such as Wappalyzer or BuiltWith also detect WooCommerce, and are usually right when they say yes. When they say no, still try step 1: a headless store (a custom front end on a WooCommerce back end) shows none of the page markers but answers the API.
🔗 What to put in startUrls
Any of these, for any WooCommerce store, in any permalink style the store uses:
| URL | What is crawled |
|---|---|
https://store.com/ (or a bare domain in domains) | the whole catalog, plus its categories |
a category page, e.g. https://store.com/product-category/shoes/ or https://store.com/shoes/ | the products in that category, plus one category record |
| a tag page | the products with that tag |
a search page, https://store.com/?s=knife&post_type=product | the products matching the search |
| a product page | that one product |
You do not need to know which kind a URL is. The Actor asks the store. Several stores can be mixed in one run; two URLs on the same store share that store's cap and never store the same product twice.
📋 Input
| Field | Default | Description |
|---|---|---|
startUrls | — | Store, category, tag, search or product URLs. See above. |
domains | — | Bare domains, one per line. mystore.com is the same as a start URL of https://mystore.com/. |
maxProducts | 20 | Product records to save in the whole run. 0 = no limit; bound spend with the run's Max total charge instead. Categories and reviews do not count against this. |
maxProductsPerStore | 0 | Product records per store. 0 = no per-store limit. Set this whenever a run has more than one store, so one large catalog cannot use up the whole budget before the next store is reached. |
includeVariations | true | Fetch every variation of a variable product (its own SKU, price, stock and options), nested inside the product record. Variations are not billed separately but do cost requests. When off, the record still carries one variant and extraInfo.variationsOmitted says how many were skipped. |
includeReviews | false | Save customer reviews as separate Review records. Billed per review. |
maxReviewsPerProduct | 20 | Newest reviews kept per product. 0 = all of them. Some stores carry hundreds of reviews per product, so keep this small unless reviews are what you came for. |
query | — | Search term. Every store root or domain in the run is searched for it instead of crawled whole; a category or tag URL is searched within that category or tag. Needs a store with an open API. |
allowHtmlFallback | true | Let the Actor read product pages directly when a store has no open API. Turn off to restrict the run to stores with an open API. |
maxHtmlDetailRecords | 200 | Cap on products produced through that fallback in one run. 0 = no cap. |
maxConcurrency | 2 | Parallel requests per store. Most WooCommerce stores are small self-hosted sites and start failing under load; raise this only for stores you know are on solid hosting. |
proxy | Apify proxy | Datacenter proxies are fine. The Actor switches to residential on its own when a store's firewall blocks it. |
maxRequestsPerCrawl, the name this field had before version 0.2.0, still works as an
alias of maxProducts.
📤 Output
Product
One record per product, variations nested. price values are the store's major
currency unit times 100: 2000 with "currency": "USD" is $20.00, and
currentFormatted carries the store's own rendering.
{"source": {"id": "1251","canonicalUrl": "https://www.shoprootscience.com/shop/sample-kit","retailer": "shoprootscience.com","language": "en","currency": "USD"},"title": "Sample Kit","brand": "","categories": ["Samples"],"price": {"current": 2000,"currentFormatted": "$20.00","previous": 2000,"stockStatus": "InStock","stockCount": 0},"stats": {"rating": 4.75,"reviewCount": 4},"options": [{"type": "Sample 1","values": [{"id": "Bare Facial Serum","name": "Bare Facial Serum"},{"id": "Youth Facial Serum","name": "Youth Facial Serum"},{"id": "Restore Facial Serum","name": "Restore Facial Serum"},{"...": "6 more"}]},{"type": "Sample 2","values": [{"id": "Bare Facial Serum","name": "Bare Facial Serum"},{"id": "Youth Facial Serum","name": "Youth Facial Serum"},{"id": "Restore Facial Serum","name": "Restore Facial Serum"},{"...": "6 more"}]},{"type": "Sample 3","values": [{"id": "Bare Facial Serum","name": "Bare Facial Serum"},{"id": "Youth Facial Serum","name": "Youth Facial Serum"},{"id": "Restore Facial Serum","name": "Restore Facial Serum"},{"...": "6 more"}]}],"variants": [{"id": "232607","options": ["Bare Facial Serum","Arctic-C Vitamin C Serum","Bright Eye Serum"],"price": {"current": 2000,"currentFormatted": "$20.00","previous": 2000,"stockStatus": "InStock","stockCount": 0},"title": "Sample 1: Bare Facial Serum, Sample 2: Arctic-C Vitamin C Serum, Sample 3: Bright Eye Serum","sku": "SAM-BAR-ARCC-BRI"},{"id": "232144","options": ["Bare Facial Serum","Arctic-C Vitamin C Serum","Detox Facial Mask"],"price": {"current": 2000,"currentFormatted": "$20.00","previous": 2000,"stockStatus": "InStock","stockCount": 0},"title": "Sample 1: Bare Facial Serum, Sample 2: Arctic-C Vitamin C Serum, Sample 3: Detox Facial Mask","sku": "SAM-BAR-ARCC-DET"},{"...": "13 more variants"}],"extraInfo": {"scrapedAt": "2026-09-05T06:11:16.431Z","dataSource": "store_api","productType": "variable","storeHost": "shoprootscience.com","stockCountSource": "unknown","attributes": [{"name": "Sample 1","isVariationAxis": true,"values": ["Bare Facial Serum","Youth Facial Serum","Restore Facial Serum","Bright Eye Serum","Botanic-A Retinol Alternative","Arctic-C Vitamin C Serum","Reborn Facial Mask","Detox Facial Mask","Polish Facial Exfoliant"]},{"...": "2 more"}]}}
Fields worth knowing:
source.idis the store's own product id. It is unique within one store only; use it together withextraInfo.storeHost.source.canonicalUrlis the public product page.price.currentis the selling price,price.previousthe regular price (equal tocurrentwhen the product is not on sale). For a variable product the record's price is the lowest variant price; each variant carries its own.price.stockStatusisInStock,LowInStockorOutOfStock.price.stockCountis a unit count when the store reveals one;extraInfo.stockCountSourcesays whether it did (low_stock_remaining, exact), whether the count was inferred from the purchase maximum (add_to_cart_maximum, an estimate), orunknown, in which casestockCountis0for want of data.variants[]always has at least one entry. A simple product carries one variant with the product's own id and price.options[]lists the variation axes, andvariant.options[i]is the chosen value on axisoptions[i]. A variant'sskucan equal a sibling's (WooCommerce falls back to the parent SKU), so it is not a key.extraInfo.dataSourceisstore_apiorhtml. Records read through the HTML fallback have no rating, no weight or dimensions, and only the attributes that drive variations. Filter on this before comparing records across stores.extraInfo.productTypeis the store's own product type string (simple,variable,external,subscription,bundleand whatever plugins add). Branch onvariantsrather than on this value.extraInfo.attributes[]holds specifications and variation axes, including the store's custom ones.extraInfo.extensionsholds raw plugin data when a store publishes any. Long and short descriptions (description,details.short_desc) are HTML as the store renders it.
Review
One record per review, only when includeReviews is on. Only the newest
maxReviewsPerProduct reviews of each product are kept, and only reviews the store
has approved. Join to the product on extraInfo.storeHost + productId.
{"id": "137626","productId": "1251","productTitle": "Sample Kit","productUrl": "https://www.shoprootscience.com/shop/sample-kit","rating": 4,"text": "98% love. My only two complaints are:\n\n1. It says you get to choice your minis, which is not entirely true. I had to purchase two whole kits to try the things I wanted since you’re only allowed to choose one serum. I really was in between two serums but couldn’t select them as trial sizes unless I purchased two kits separately with a bunch of other products I wasn’t wanting.\n\n2. The serum tubes are complicated. Droppers would be better, but I get not wanting to be wasteful. It’s just that it’s impossible to get the 2-4 drops recommend out of the vial without touching it to your skin or getting way to much product.\n\nOther than that, love this and have already made full sized purchases!","reviewer": "kmgbutler","verified": true,"createdAt": "2023-11-30T05:32:52.000Z","extraInfo": {"scrapedAt": "2026-09-05T06:11:18.385Z","storeHost": "shoprootscience.com","dataSource": "store_api"}}
text is plain text with paragraph breaks kept. verified is WooCommerce's
verified-owner flag. createdAt is UTC.
Category
One record per category, with id, title, handle (slug), description,
productsCount, parentId and canonicalUrl. Emitted for a category start URL (that
category) and for a whole-store crawl (the store's non-empty categories, at most 50
and never more than a fifth of maxProducts).
Store summary
One free record per store per run, written before its products: host, currency,
language, productCount, variationCount, priceRange, stockStatusCounts,
productTypeCounts, and how the store was read (enumerate, detail, degraded).
Use productCount and variationCount to size a run before paying for it, and
detail: "html" to spot a store that was read through the fallback.
💳 What you are charged for
Every product record, every category record and every review record is a billed
result. Two surcharges can apply to a product: html-detail when it was read through
the HTML fallback, and real-inventory when the store published an exact stock count
for it. Variations and the store summary are never billed. Prices are on the actor's
pricing tab.
⚠️ Notes and limitations
- Category, tag and search URLs need a store with an open API. On a store without one the Actor reports an error for that URL instead of silently crawling and billing the whole catalog. Point it at the store root to crawl everything.
- Reviews come from the store's API only. A store read through the HTML fallback yields none; the run log says so for that store.
- The HTML fallback is slow and capped. It costs one page fetch per product and
carries the
html-detailsurcharge.maxHtmlDetailRecordsbounds it per run;allowHtmlFallback: falseskips such stores entirely. - Stores that expose neither an API nor a product sitemap cannot be crawled. The
error names every surface that was tried, for example
no usable data surface on example.com: store_api=403/waf, wp_rest=403/waf, sitemap=403/waf. A store that lists its products but blocks every way of reading one produces no records and no charges (can be enumerated but not read). - Not a WooCommerce store is reported as
not a WooCommerce catalog URL: <url>. - All ids are per store. Products, categories and reviews from two stores can share
an id. Key on
extraInfo.storeHost(or the store summary'shost) as well. - Keep concurrency low. The default of 2 exists because small stores start returning errors after a few hundred fast requests. The Actor backs off and retries, but a lower rate is cheaper than recovery.
- What the Actor learned about a store is remembered for 30 days (24 hours if the
store misbehaved), in a key-value store named
WOO_STORE_PROFILESunder Storage in your Apify account. If a store changes its setup and you want it re-examined now, delete that store's entry there. - Descriptions are raw HTML. Strip them yourself if you need text.
- Older WooCommerce versions do not publish
weight,dimensionsorbrands; those fields are absent, not empty, when the store does not have them. - Category records carry
publishedUTC: 0andupdatedUTC: 0: WooCommerce categories have no timestamps.
🤖 Use with AI agents
This Actor is callable as a tool by any MCP-capable agent — Claude, Cursor, VS Code — or by your own code, with no wrapper and nothing extra to deploy.
Connect over MCP
https://mcp.apify.com?tools=autofacts/woocommerce-scraper
In a client that reads an mcpServers configuration block:
{"mcpServers": {"apify": {"url": "https://mcp.apify.com?tools=autofacts/woocommerce-scraper","headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }}}}
The agent reads this Actor's parameters and their descriptions straight from the input schema, and the hosted server infers the result field types from the dataset schema — so a model knows what to send and what comes back before it ever calls anything.
Or call the API directly
curl -X POST "https://api.apify.com/v2/acts/autofacts~woocommerce-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H 'Content-Type: application/json' \-d '{"startUrls": [{"url": "https://godinguitars.com/"}], "maxProducts": 20}'
The response body is the dataset records described above.
🧰 Other Actors by autofacts
Apify only auto-recommends Actors in the same category, so here are the ones that actually pair with this scraper:
| Actor | What it's for |
|---|---|
| Shopify Scraper | The other half of self-hosted ecommerce - same record shape, so the two datasets merge |
| Shopify Store Leads | Find and qualify stores before you scrape them |
| Schema Markup Scraper & SEO Auditor | Audit a store's structured data, Open Graph tags and canonical setup |
| Universal Web Printer | Render any product page to PDF/PNG for archiving or evidence |
All of them: apify.com/autofacts