WooCommerce Scraper avatar

WooCommerce Scraper

Pricing

from $1.20 / 1,000 products

Go to Apify Store
WooCommerce Scraper

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

Richard Feng

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

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 os
from apify_client import ApifyClient
client = 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().items
products = [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
ResponseMeaning
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 pagePossibly 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" pageFirewalled. 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:

  • woocommerce in the <body class="…"> list. A product page also has single-product and postid-<number>; a category page has tax-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 a woocommerce-product-gallery element.

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.com or a *.myshopify.com domain: Shopify with a WordPress front, not WooCommerce;
  • edd- classes and /edd- URLs: Easy Digital Downloads;
  • an embedded ecwid or bigcommerce widget: 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:

URLWhat 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 pagethe products with that tag
a search page, https://store.com/?s=knife&post_type=productthe products matching the search
a product pagethat 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

FieldDefaultDescription
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/.
maxProducts20Product 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.
maxProductsPerStore0Product 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.
includeVariationstrueFetch 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.
includeReviewsfalseSave customer reviews as separate Review records. Billed per review.
maxReviewsPerProduct20Newest 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.
allowHtmlFallbacktrueLet 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.
maxHtmlDetailRecords200Cap on products produced through that fallback in one run. 0 = no cap.
maxConcurrency2Parallel 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.
proxyApify proxyDatacenter 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.id is the store's own product id. It is unique within one store only; use it together with extraInfo.storeHost. source.canonicalUrl is the public product page.
  • price.current is the selling price, price.previous the regular price (equal to current when 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.stockStatus is InStock, LowInStock or OutOfStock. price.stockCount is a unit count when the store reveals one; extraInfo.stockCountSource says whether it did (low_stock_remaining, exact), whether the count was inferred from the purchase maximum (add_to_cart_maximum, an estimate), or unknown, in which case stockCount is 0 for 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, and variant.options[i] is the chosen value on axis options[i]. A variant's sku can equal a sibling's (WooCommerce falls back to the parent SKU), so it is not a key.
  • extraInfo.dataSource is store_api or html. 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.productType is the store's own product type string (simple, variable, external, subscription, bundle and whatever plugins add). Branch on variants rather than on this value.
  • extraInfo.attributes[] holds specifications and variation axes, including the store's custom ones. extraInfo.extensions holds 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-detail surcharge. maxHtmlDetailRecords bounds it per run; allowHtmlFallback: false skips 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's host) 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_PROFILES under 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, dimensions or brands; those fields are absent, not empty, when the store does not have them.
  • Category records carry publishedUTC: 0 and updatedUTC: 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:

ActorWhat it's for
Shopify ScraperThe other half of self-hosted ecommerce - same record shape, so the two datasets merge
Shopify Store LeadsFind and qualify stores before you scrape them
Schema Markup Scraper & SEO AuditorAudit a store's structured data, Open Graph tags and canonical setup
Universal Web PrinterRender any product page to PDF/PNG for archiving or evidence

All of them: apify.com/autofacts