# WooCommerce Products API — Prices, Variants & Stock (`insight.solutions/woocommerce-products-api`) Actor

Scrape any WooCommerce store's catalogue as data from its public Store API: name, SKU, price, sale price, stock count, variations, attributes, categories, tags and images. Give it a store URL — no API key, no browser, no plugin.

- **URL**: https://apify.com/insight.solutions/woocommerce-products-api.md
- **Developed by:** [Insight Solutions](https://apify.com/insight.solutions) (community)
- **Categories:** E-commerce, Developer tools, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.60 / 1,000 products

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## WooCommerce Products API — Prices, Variants & Stock

**Every WooCommerce store exposes a public, read-only Store API; this Actor reads it — no key, no browser.** Give it a list of store URLs and get back one row per product: name, SKU, type, price, regular and sale price in real currency units, stock status and stock count, rating, categories, tags, brands, attributes, images, the product page and the description. Ask for one row per variation and each size or colour arrives on its own line with its own price and stock. Ask for the category tree and you get that too.

No API key. No plugin to install in anyone's store. No WooCommerce REST keys, no login, no browser. **$1 per 1,000 products, proxy included.** Sites that are not WooCommerce stores are free, and a run that returns nothing costs nothing at all.

**Shopify store?** Use the sibling [Shopify Products API](https://apify.com/insight.solutions/shopify-products-api) — same input vocabulary (`stores`, `maxProductsPerStore`, `rowPerVariant`, `includeDescriptionHtml`, `includeImages`, `onlyAvailable`), the same kind of rows.

### Try it in 30 seconds

**Input** — this is the Store prefill; paste it and run:

```json
{
  "stores": ["https://barefootbuttons.com"],
  "maxProductsPerStore": 100
}
```

A bare domain works, and so does a link to any page of the shop — a product page, a category page, a link with tracking parameters. A store installed in a subdirectory, like `https://example.com/shop`, is tried there first and at the root second.

**Output** — one `product` row per product, plus one free `store` summary row per store. The fields you will use most are `name`, `sku`, `price`, `regularPrice`, `currency`, `stockStatus`, `stockQuantity` and `url` (full list under *Output reference*). Anything that could not be read comes back as a free diagnostic row (`ok: false`, `errorType`, `error`) instead of a charge.

**Price** — $1 per 1,000 products on the FREE tier (+$0.001 per run); a site that is not a WooCommerce store, a blocked store, an empty catalogue and a run that returns nothing are all free. Pay-per-event, no API key, no browser, limited permissions — works over the Apify MCP server (`mcp.apify.com`) and with agentic (x402) payments.

**From code** — `client.actor("insight.solutions/woocommerce-products-api").call(run_input={"stores": ["https://barefootbuttons.com"]})` with `apify-client`, or `POST https://api.apify.com/v2/acts/insight.solutions~woocommerce-products-api/run-sync-get-dataset-items`.

### What comes back

A product row, exactly as the Actor writes it from the captured barefootbuttons.com catalogue (description shortened, one image shown):

```jsonc
{
  "ok": true,
  "rowType": "product",                 // "product" | "category" | "store" | "diagnostic"
  "input": "https://barefootbuttons.com",
  "storeUrl": "https://barefootbuttons.com",
  "currency": "USD",

  "productId": 1296,
  "parentId": null,
  "variationId": null,                  // set on variation rows (rowPerVariant)
  "name": "Barefoot Buttons Max Bore Standard Silver- Guitar Pedal Footswitch Topper",
  "slug": "max-bore-standard-silver-footswitch-topper",
  "sku": "20-V1-MB-SV",
  "type": "simple",                     // simple | variable | grouped | external | variation
  "url": "https://barefootbuttons.com/product/max-bore-standard-silver-footswitch-topper/",

  "price": 9.95,                        // major units — the Store API sends "995"
  "regularPrice": 9.95,
  "salePrice": null,                    // only when onSale is true
  "priceMin": 9.95,
  "priceMax": 9.95,
  "onSale": false,
  "discountPercent": null,

  "inStock": true,
  "stockStatus": "instock",             // instock | outofstock | onbackorder
  "stockText": "141 in stock",
  "stockQuantity": 141,
  "isPurchasable": true,
  "hasOptions": false,

  "averageRating": null,                // null until the product has a review
  "reviewCount": 0,
  "categories": ["Max Bore"],
  "categorySlugs": ["max-bore"],
  "tags": ["Max Bore Standard", "Silver", "Standard"],
  "brands": [],
  "attributes": { "Color": ["Silver"], "Size": ["Standard"] },
  "variationAttributes": null,
  "variationCount": 0,

  "imageUrl": "https://barefootbuttons.com/wp-content/uploads/2026/05/barefoot-buttons-max-bore-standard-silver-footswitch-topper.png",
  "images": [{ "src": "https://barefootbuttons.com/wp-content/uploads/2026/05/barefoot-buttons-max-bore-standard-silver-footswitch-topper.png",
               "alt": "Barefoot Buttons Max Bore Standard Silver guitar pedal footswitch topper top view for large footswitches 1/2\" (13mm)" }],
  "shortDescriptionText": null,
  "descriptionText": "The Max Bore Standard Silver delivers the premium look, feel, and control Barefoot Buttons…",
  "descriptionHtml": null,              // set when includeDescriptionHtml is on

  "weight": null,
  "weightUnit": null,
  "dimensions": null,                   // { length, width, height, unit } when the store sets them
  "addToCartUrl": "https://barefootbuttons.com/?add-to-cart=1296",
  "rank": 3,                            // 1-based, in orderBy order, per store

  "error": null,
  "errorType": null,
  "scrapedAt": "2026-10-05T23:26:23.697Z",
  "source": "barefootbuttons.com",
  "sourceUrl": "https://barefootbuttons.com/wp-json/wc/store/v1/products?per_page=100&page=1&orderby=date&order=desc"
}
```

Every row carries the same 58 columns; the ones that do not apply to a row type are `null` (the category and store columns — `categoryId`, `productCount`, `description`, `totalProducts`, `returned`, `filteredOut`, `wooDetected`, `wordpressName` — are left out above).

After a store's products comes one free **store row** — for the live store, which has 61 products:

```jsonc
{ "ok": true, "rowType": "store", "input": "https://barefootbuttons.com", "storeUrl": "https://barefootbuttons.com",
  "currency": "USD", "totalProducts": 61, "returned": 61, "filteredOut": 0,
  "wooDetected": true, "wordpressName": "Barefoot Buttons", "source": "barefootbuttons.com",
  "sourceUrl": "https://barefootbuttons.com/wp-json/wc/store/v1/products" /* every other column null */ }
```

`totalProducts` is the store's own count of matching products (its `x-wp-total` header) — the number to compare `returned` against when `maxProductsPerStore` cut the walk short. A store row is written only for a store that returned at least one paid row.

#### Variation rows

With `"rowPerVariant": true`, every variable product's row is followed by one row per variation. A variation row is a `product` row with `variationId` set, `productId` and `parentId` naming the variable product, its own `sku`, `price`, `regularPrice`, `stockStatus` and `stockQuantity`, and `variationAttributes` saying what it is — `{ "Size": "Large", "Colour": "Red" }`. A `null` value is WooCommerce's "any": that variation sells every value of the attribute (the captured Set Screw Kit has exactly one such variation). What a variation does not publish of its own — categories, tags, brands, images, an empty description — is taken from its product, so each row stands on its own in a spreadsheet. A variation row carries its product's `rank`, and the product's own row is still returned.

#### Category rows

With `"includeCategories": true`, one `rowType: "category"` row per product category: `categoryId`, `name`, `slug`, `parentId` (for the tree), `productCount`, `url`, `description` and `imageUrl`. The store returns its whole category list in one request.

### Use cases

- **Competitor price and assortment tracking** — run it daily against a list of stores, diff on `productId` (and `variationId`), and you have every price move, new product and discontinued line.
- **Stock and sell-out monitoring** — `stockStatus` and, where the merchant shows it, `stockQuantity` per product or per variation: which sizes sold out, and how fast stock is moving.
- **Discount and promotion watching** — `onSale`, `salePrice` and `discountPercent`, or `onSaleOnly` to fetch nothing else.
- **Catalogue and feed building** — a clean product feed for a marketplace, an affiliate site or a comparison engine, without asking each merchant for API keys.
- **Supplier and dropshipping research** — SKUs, attributes, dimensions and stock across a list of supplier stores in one schema.
- **Retrieval for a shopping agent** — one flat array of products with descriptions, prices and links, ready to embed.
- **Market mapping** — feed a list of domains and the diagnostic rows tell you which of them run WooCommerce at all, which run WordPress without it, and which run neither.

### How it works, and why it keeps working

WooCommerce ships a public, read-only **Store API** with every install — `/wp-json/wc/store/v1/` — the same API the store's own block-based product grids, cart and checkout read. It needs no key because it only ever shows what any visitor to the shop can see. This Actor reads it and nothing else.

| Step | Request | What it gets |
|---|---|---|
| 1 | `/wp-json/wc/store/v1/products?per_page=100&page=1&…` | Up to 100 products — and the proof the site is a WooCommerce store |
| 2… | the same, `page=2`, `page=3`, … | The next 100, until your cap or the store's own `x-wp-totalpages` |
| V | `…/products?type=variation&parent=<id>&per_page=100` | One variable product's variations, with `rowPerVariant` |
| C | `…/products/categories` | The category list, with `includeCategories` or a category filter given by name |
| N | `/wp-json/?_fields=name,namespaces` | The site's name for the store row — one small request, after everything else |

Details that came out of real captured responses, and that a naive version gets wrong:

- **Prices are strings in minor units.** `"995"` with `currency_minor_unit: 2` is 9.95; a yen store has minor unit 0, a dinar store 3. Every price column is converted with the store's own minor unit, and price *filters* are converted the other way — which is why a run with `minPrice` or `maxPrice` first reads one product to learn it.
- **`sale_price` is not the sale price.** The Store API puts the current price in it whether or not anything is on sale. `salePrice` here is only set when `onSale` is true, so "has a sale price" means "is on sale".
- **The `Link` header lies, a little.** The captured page-2 response announced its next page as `…?per_page=5&page=3#038;page=2` — WordPress escaping its own query string into a fragment. The walk uses `x-wp-totalpages` first, `rel="next"` only as a yes/no, a short page as the last resort, and always builds its own page URLs.
- **A simple product's add-to-cart URL points at the API.** The captured one is `/wp-json/wc/store/v1/products?per_page=20&#038;add-to-cart=1296`; `addToCartUrl` is rebuilt as the store's own `/?add-to-cart=1296`.
- **"9999" is not stock.** An out-of-stock product reports it may be bought 9,999 at a time. `stockQuantity` comes from the stock line the store shows ("93 in stock") or its low-stock counter, and is null when the store does not manage stock.

**Telling a store from a site that is not one.** When the products call does not answer with products, the answers that did come back are classified, never guessed: a WordPress REST error (`rest_no_route`) means WordPress is there and the Store API is not, and the site's own `/wp-json/` index — its list of REST namespaces — confirms whether WooCommerce is installed at all (`not-woocommerce`, with the site's name). An HTML page from both the products route and `/wp-json/` means `not-wordpress`, after exactly one probe. WordPress advertises its REST root in a `Link: rel="https://api.w.org/"` header on the pages it renders, so a store on plain permalinks (`/?rest_route=`) is still found when its error page says where; an older store whose index lists only the unversioned `wc/store` namespace is read through that.

Underneath: **Apify proxy**, one pinned session per parallel worker. When something in front of a store refuses an exit IP — HTTP 403, HTTP 429, or a bot-check page wearing HTTP 200 — that session is retired and **the same request is made once more from a different address**. If the second address is refused too, the store stops, **keeps every row it already delivered**, and files one free `blocked` (or `rate-limited`) row.

**Merchant servers are small.** Requests to one store are spaced 300–600 ms apart and never more than two at a time (two only while fetching variation lists). The User-Agent names this Actor.

### Filters and sorting

Every filter is applied by the store, so a filtered run downloads only what it keeps:

| Input | Sent as | Notes |
|---|---|---|
| `search` | `search=` | The store's own product search |
| `categories` | `category=<id>`, one walk per category | Ids, slugs, names or category URLs; non-ids are looked up in the store's list. A product in two of them is returned once |
| `onSaleOnly` | `on_sale=true` | |
| `onlyAvailable` | `stock_status=instock` | Also drops sold-out variations |
| `minPrice` / `maxPrice` | `min_price=` / `max_price=` in minor units | Major units in, converted per store |
| `orderBy` | `orderby=` + `order=` | `date` (newest), `price`, `price-desc`, `popularity`, `rating`, `title` |

The Actor also re-checks `onlyAvailable`, `onSaleOnly` and the price range on what comes back, because older Store API releases ignore filter parameters they do not know rather than refusing them. A product that fails the re-check is not written and not billed; the store row counts it in `filteredOut`.

### How it compares

- **No keys, no plugin, no merchant cooperation.** It reads what every WooCommerce store already serves the public.
- **One request per 100 products.** Not one page fetch per product, and no browser.
- **Prices are numbers, in real units, with the currency on every row.** Not `"995"`, not a string with a symbol glued on.
- **Variations are real rows.** Each with its own SKU, price and stock, so a spreadsheet or a SQL table can hold them without a JSON parser.
- **Failures are legible and free.** Not WordPress, WordPress without WooCommerce, blocked, rate-limited, empty, a typo in a domain — each is a diagnostic row with an `errorType` you can branch on, and no charge. A run that returns nothing at all finishes **SUCCEEDED with zero results**, a status message that says so, and a free diagnostic row per store telling you why; it finishes **FAILED** only when no entry was usable, or the Actor itself hit an error.
- **A partial catalogue is kept, not thrown away.** Hit `maxRunSecs` or your charge ceiling on page 9 and you keep pages 1–8.

### Input reference

| Field | Type | Default | What it does |
|---|---|---|---|
| `stores` **(required)** | array of strings | prefilled with one store | Store domains or any URL on the store. Scheme optional; subdirectory installs (`/shop`) tried first, then the root. Duplicates are read, and billed, once |
| `search` | string | `""` | The store's own search |
| `categories` | array of strings | `[]` | Category ids, slugs, names or URLs — one walk per category |
| `onSaleOnly` | boolean | `false` | Only products on sale |
| `onlyAvailable` | boolean | `false` | Only in-stock products (and variations) |
| `minPrice` / `maxPrice` | number | — | Price range in the store's currency, ordinary units |
| `orderBy` | select | `date` | `date`, `price`, `price-desc`, `popularity`, `rating`, `title` |
| `rowPerVariant` | boolean | `false` | One extra row per variation of each variable product; the product row is kept. One extra request per variable product. Variation rows are billed like products |
| `includeCategories` | boolean | `false` | One row per product category, billed at $0.0005 each |
| `includeDescriptionHtml` | boolean | `false` | Add the raw description markup next to the text every row already has |
| `includeImages` | boolean | `true` | Fill in `images[]` (src, alt). Off still keeps `imageUrl` |
| `maxProductsPerStore` | integer | `100` | 1–50,000 products per store, in `orderBy` order. Also the page size up to 100. Variation rows do not count towards it |
| `maxConcurrency` | integer | `3` | Stores in parallel. Each worker keeps its own proxy session |
| `maxRunSecs` | integer | `240` | Whole-run wall-clock budget. When it runs out the Actor keeps what it has and files a free `deadline` row for each store it cut short or never reached |
| `proxyConfiguration` | object | Apify datacenter | The cheap default, which merchant servers accept. Switch to the RESIDENTIAL group for a store behind a firewall |

### Output reference

Every row carries the same keys. `ok: true` is a product (or variation), category or store row; `ok: false` is a free diagnostic row.

| Field | What it is |
|---|---|
| `rowType` | `product`, `category`, `store` or `diagnostic` |
| `input`, `storeUrl`, `currency` | The entry you supplied, the store that answered (after redirects, with its install directory), and its currency |
| `productId`, `parentId`, `variationId` | WooCommerce's ids. On a variation row `productId` and `parentId` name the variable product and `variationId` the variation |
| `name`, `slug`, `sku`, `type`, `url` | The product (or category) name and slug, the SKU, the product type, and the public page |
| `price`, `regularPrice`, `salePrice`, `priceMin`, `priceMax` | Major units. `salePrice` only on sale; `priceMin`/`priceMax` from the store's price range, or `price` |
| `onSale`, `discountPercent` | The store's sale flag, and how far below the regular price |
| `inStock`, `stockStatus`, `stockText`, `stockQuantity` | Stock flag, `instock`/`outofstock`/`onbackorder`, the store's own stock line, and the count when it shows one |
| `isPurchasable`, `hasOptions` | Whether it can be bought, and whether options must be chosen first |
| `averageRating`, `reviewCount` | Rating (null with no reviews) and review count. Reviews themselves are never read |
| `categories`, `categorySlugs`, `tags`, `brands` | Names (and category slugs) |
| `attributes`, `variationAttributes`, `variationCount` | `{ name: [values] }`; on a variation row `{ name: value }`; how many variations |
| `imageUrl`, `images` | The first image, and all of them as `{ src, alt }` |
| `shortDescriptionText`, `descriptionText`, `descriptionHtml` | Text (always, up to 20,000 characters) and markup (opt-in) |
| `weight`, `weightUnit`, `dimensions`, `addToCartUrl` | Shipping facts when the store sets them, and a working add-to-cart link |
| `rank` | 1-based position in `orderBy` order, per store |
| `categoryId`, `productCount`, `description` | On category rows |
| `totalProducts`, `returned`, `filteredOut`, `wooDetected`, `wordpressName` | On store rows (and, where known, on a store's diagnostic rows) |
| `ok`, `error`, `errorType` | Whether this row is data, and if not, why not |
| `scrapedAt`, `source`, `sourceUrl` | When, the host, and the exact Store API request it came from |

`errorType` on a diagnostic row is one of:

| Value | Meaning | Charged? |
|---|---|---|
| `not-woocommerce` | WordPress answered, but the WooCommerce Store API is not there (its `/wp-json/` namespaces say so), or the site's REST API is switched off | No |
| `not-wordpress` | The site answered the Store API route and `/wp-json/` with web pages: not WordPress, or its REST API is hidden | No |
| `blocked` | Something in front of the store refused two different proxy exits, or its REST API is closed to visitors. Rows already returned are kept | No |
| `rate-limited` | HTTP 429 from two different exits. Rows already returned are kept | No |
| `http` | Nothing answered at the domain, the store answered with an error, or a part of the walk (a page, a variation list, the category list) failed. Rows already returned are kept | No |
| `timeout` | The store did not answer in time, twice | No |
| `deadline` | The run's `maxRunSecs` ran out before or while reading this store. Rows already returned are kept | No |
| `no-products` | A WooCommerce store whose catalogue is empty, whose products all missed your filters, or that has no category by the name you gave | No |
| `invalid-input` | The entry was not a domain or a URL | No |

### Pricing

**$1 per 1,000 products.** Pay-per-event, with the proxy already inside that number — there is no separate proxy line on your bill for this Actor.

| Event | What triggers it | FREE | Starter | Scale | Business |
|---|---|---|---|---|---|
| **Product** *(primary)* | One product **or variation** row written to your dataset | $0.001 | $0.001 | $0.0008 | $0.0006 |
| Category | One category row, when you ask for them | $0.0005 | $0.0005 | $0.0005 | $0.0005 |
| Run started | Once per run, after the first paid row | $0.001 | $0.001 | $0.001 | $0.001 |

**Worked example.** A 500-product catalogue, 100 of them variable with three variations each, with `rowPerVariant` on: 500 product rows + 300 variation rows = 800 × $0.001 = **$0.80**, plus $0.001 for the run. The prefill — barefootbuttons.com's 61 products — is **$0.062**.

What you are never charged for: the store summary row, a site that is not a WooCommerce store, a store that refused us, an empty catalogue or filter, a typo, or a store the run never reached before `maxRunSecs`. A run whose stores were all usable but produced nothing finishes **SUCCEEDED with zero results**, a status message that says so, and a free diagnostic row per store telling you why; it bills **nothing at all**, start fee included. A run finishes **FAILED** only when there was nothing usable to attempt, or the Actor itself hit an error.

Set `ACTOR_MAX_TOTAL_CHARGE_USD` on a run and the Actor stops walking once the ceiling is in sight, rather than handing you rows it cannot bill or billing you for rows it cannot hand over. It finishes SUCCEEDED with the ceiling named in its status message, and everything already delivered is yours.

### Limits, and the ones that might bite

**Stores can switch the API off.** A security plugin can close the WordPress REST API to visitors, and a firewall can refuse datacenter addresses. Those stores come back as free `blocked` or `not-woocommerce` rows rather than partial data; switching `proxyConfiguration` to the RESIDENTIAL group fixes the second kind, nothing public fixes the first.

**Prices are what an anonymous visitor sees.** The Store API answers as a logged-out shopper in the store's default location: prices before customer-group, membership, wholesale or geolocated pricing, and usually in the store's base currency, whatever a currency-switcher plugin would show a visitor.

**Stock counts are only as visible as the merchant makes them.** `stockQuantity` is filled when the store shows a count ("93 in stock") or reports low stock; a store that hides counts, or does not manage stock, gives `stockStatus` and a null quantity.

**Only what is published.** Draft, private and hidden products, and password-protected ones' details, are not in the Store API. This Actor never logs in, never uses WooCommerce REST keys belonging to anyone, and never reads orders, customers, users or reviews.

**Variations cost a request each.** One extra request per variable product (100 variations per request) at most two at a time, so a catalogue that is mostly variable products takes roughly twice as long per row with `rowPerVariant` on. Raise `maxRunSecs` for a big variant run.

**Some stores serve variations differently.** Variation rows come from the Store API's `type=variation&parent=<id>` query; the Actor keeps only variations that really belong to the product asked about. A store whose API does not support that query returns its product rows with a free `http` row explaining the variations were skipped.

**The upstream format may change.** WooCommerce has kept the Store API's v1 shape stable for years, but it is a merchant-run, plugin-extensible endpoint. When a shape changes, rows stop arriving and you get free diagnostic rows rather than quietly wrong data, and a run that returns nothing bills nothing.

### Use it from an AI agent, or from code

One JSON object in, one flat array out — the shape agent runtimes want. The Actor runs with **limited permissions**, uses **pay-per-event** pricing and never enters Standby, so it works over the Apify MCP server and with x402 agentic payments. The **Integrations** tab pushes results to Slack, a webhook, Zapier, Make, Google Sheets, Snowflake or BigQuery.

```bash
curl -X POST "https://api.apify.com/v2/acts/insight.solutions~woocommerce-products-api/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"stores":["barefootbuttons.com"],"onlyAvailable":true,"orderBy":"price","maxProductsPerStore":50}'
```

```python
## pip install apify-client
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("insight.solutions/woocommerce-products-api").call(run_input={
    "stores": ["https://barefootbuttons.com"],
    "rowPerVariant": True,
    "onlyAvailable": True,
    "maxProductsPerStore": 500,
})

for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row["rowType"] == "diagnostic":
        print("skipped:", row["input"], row["errorType"], row["error"])
        continue
    if row["rowType"] != "product":
        continue
    options = row["variationAttributes"] or {}
    sale = f'  ON SALE -{row["discountPercent"]}%' if row["onSale"] else ""
    print(f'{row["name"]} {options}  {row["price"]} {row["currency"]}  stock={row["stockQuantity"]}{sale}')
```

**Building an app with Lovable?** Apify's Lovable connector runs any Actor from inside a Lovable project with your Apify token. Connect Apify in Lovable once, then ask it: *"Use Apify to run insight.solutions/woocommerce-products-api on a store URL I enter and build a price tracker around the products it returns."* At the default settings a run finishes well inside Lovable's 300-second synchronous limit, and every result is one JSON object per row, so the generated app needs no extra parsing. Usage bills to the Apify account whose token is connected.

### FAQ

**Do I need API keys or permission from the store?**
No keys. The Store API is part of every WooCommerce install and is served to anyone who asks, the same as the shop's product pages — it only ever shows what a visitor can see. Whether the *use* you make of a catalogue is allowed is a separate question, and it is yours — see the notes at the bottom.

**Why did my store come back as `not-woocommerce`?**
The site is WordPress, but its REST index lists no `wc/store` namespace: WooCommerce is not installed, or the store is so old it predates the Store API, or a plugin has switched the REST API off. The row carries the site's name when WordPress published one.

**Why `not-wordpress`?**
The products route and `/wp-json/` both answered with web pages. Either the site is not WordPress (a Shopify store? use the [Shopify Products API](https://apify.com/insight.solutions/shopify-products-api)), or its REST API is hidden behind a custom path that the site does not advertise.

**How do I get every product from a big store?**
Raise `maxProductsPerStore` (up to 50,000) and `maxRunSecs`. Ten thousand products is 100 sequential requests with a short pause between each — plan for a few minutes on a small merchant server, and bound it with `ACTOR_MAX_TOTAL_CHARGE_USD` if you are not sure how big the store is. The store row's `totalProducts` tells you how many there were.

**Are variation rows charged?**
Yes, at the same rate as products — a variation row is a row. That is why `rowPerVariant` is off by default. Without it, a variable product's row still carries `priceMin`, `priceMax`, `attributes` and `variationCount`.

**Can I get inventory quantities?**
When the store shows them, yes: `stockQuantity` is read from the store's own stock line or its low-stock counter. Many stores show no count, and then it is null.

**Are reviews included?**
No. The rating and the review count are on every product row; the reviews themselves carry reviewer names, so they are never read.

**What happens if one store fails?**
The others still run. The failed one produces a free diagnostic row and the run finishes `SUCCEEDED`. If *every* store fails, the run still finishes `SUCCEEDED`, with zero results, a status message that says so and one diagnostic row per store, and you are billed nothing at all. It finishes `FAILED` only when no entry in `stores` is a domain or store URL, or the Actor itself hit an error.

**Which proxy should I use?**
Start with the default datacenter proxy; merchant servers serve the Store API to anyone and the price is built on it. If a particular store comes back `blocked`, re-run that one with the RESIDENTIAL group.

### Legal and data-protection notes

- **Public storefront data only.** Every request is to an endpoint the store serves to any visitor. The Actor never logs in, never sends cookies, never uses API keys belonging to anyone, and never reads orders, customers, users, reviews or anything else that needs authentication.
- **No personal data.** Product data is business information. Reviews — the one place the Store API carries people's names — are never requested; only each product's average rating and review count are kept.
- **Respect the store.** This Actor paces itself (300–600 ms between requests, at most two at a time per store), identifies itself in its User-Agent and never tries to get past a refusal more than once. Please do not defeat that by pointing many parallel runs at one small merchant.
- **Reuse is a separate question.** Collecting prices and stock for comparison, research and competitor analysis is ordinary; republishing a merchant's descriptions and photography verbatim is different — the copy and the images belong to whoever made them.
- **Not affiliated with WooCommerce, Automattic, or any store, brand or merchant whose catalogue you retrieve.** All product names and trademarks belong to their respective owners and are used only to describe which public endpoints this Actor reads.

### Our other Actors

Every Insight Solutions Actor is pay-per-result with no browser, no login and no API key, and every one of them returns free diagnostic rows instead of billing for failures. Prices are per 1,000 results.

**Video, audio & social**

- [YouTube Transcript API](https://apify.com/insight.solutions/youtube-transcript-api) — captions as timed segments, text, SRT or VTT, with language fallback and translation.
- [YouTube Comments API](https://apify.com/insight.solutions/youtube-comments-api) — comments and replies with likes, pinned and hearted flags, newest or top sort.
- [YouTube Channel API](https://apify.com/insight.solutions/youtube-channel-api) — a channel's videos, Shorts and live streams, plus YouTube search.
- [Podcast Search, Episodes & Charts API](https://apify.com/insight.solutions/podcast-api) — Apple Podcasts search, charts and full episode feeds.
- [Bluesky Scraper](https://apify.com/insight.solutions/bluesky-scraper) — profiles, posts, followers and follows from the public AT Protocol API.
- [Telegram Channel Scraper](https://apify.com/insight.solutions/telegram-channel-scraper) — posts, views and channel stats from public Telegram channels.
- [Substack Scraper](https://apify.com/insight.solutions/substack-scraper) — posts with full free text, comments and publication profiles.
- [Hacker News API](https://apify.com/insight.solutions/hacker-news-api) — stories, comments, users, front page and a structured "Who is hiring?" parser from the official HN APIs.
- [GitHub Repos, Trending & Releases API](https://apify.com/insight.solutions/github-repos-api) — repository facts, trending lists, releases and search, no token needed for small runs.
- [Discourse Forum API](https://apify.com/insight.solutions/discourse-forum-api) — topics, posts and categories from any Discourse community via its own JSON endpoints, usernames only.

**News, documents & the web**

- [Google News Search, Topics & Real Article URLs](https://apify.com/insight.solutions/google-news-api) — news search and topic feeds with the publisher's real URL decoded.
- [Website to Markdown — Content Extractor for LLMs & RAG](https://apify.com/insight.solutions/website-content-extractor) — any site as clean Markdown, text and heading-aware chunks.
- [WordPress Posts & Pages API](https://apify.com/insight.solutions/wordpress-posts-api) — posts and pages from any WordPress site as clean text or Markdown, with categories, tags and images.
- [Website Metadata API](https://apify.com/insight.solutions/website-metadata-api) — any URL's meta tags, Open Graph, Twitter cards, hreflang, feeds, JSON-LD and microdata, with normalised Product, Article, Event and FAQ entities.
- [Subdomain Finder API](https://apify.com/insight.solutions/subdomain-finder-api) — every subdomain of a domain from certificate-transparency logs, resolved over DNS, with an optional live HTTP check.
- [Internet Archive API](https://apify.com/insight.solutions/internet-archive-api) — archive.org search, item metadata, files and reviews.
- [Wayback Machine Toolkit](https://apify.com/insight.solutions/wayback-toolkit) — archived URL inventories, snapshots and text diffs between dates.
- [Website Technology Detector](https://apify.com/insight.solutions/website-tech-detector) — the tech stack behind any site, with the evidence for each detection.
- [Domain Intelligence API](https://apify.com/insight.solutions/domain-intelligence-api) — DNS, RDAP registration, TLS certificate and HTTP facts in one row per domain.
- [SEO Page Audit](https://apify.com/insight.solutions/seo-page-audit) — sitemap crawl with on-page checks, structured data and broken-link reports.
- [Keyword Suggestions API](https://apify.com/insight.solutions/keyword-suggestions-api) — Google, YouTube, Bing, Amazon and eBay autocomplete with alphabet and question expansions.
- [Website Contact Extractor](https://apify.com/insight.solutions/website-contact-extractor) — emails, phone numbers and social profiles from any list of websites.
- [Web Search Results API](https://apify.com/insight.solutions/web-search-api) — Bing and DuckDuckGo organic results with snippets, no key, no browser.
- [Company Enrichment API](https://apify.com/insight.solutions/company-enrichment-api) — a domain in, a company profile out: firmographics, contacts, tech stack, DNS and hiring signal.
- [Company Dossier API](https://apify.com/insight.solutions/company-dossier-api) — one company in, twelve sections out: profile, tech, contacts, DNS, open roles, news, SEC filings, federal awards, recalls, YC batch and apps.
- [Press Releases API](https://apify.com/insight.solutions/press-releases-api) — GlobeNewswire and PR Newswire releases plus any newsroom feed, by keyword, company, ticker or subject.
- [Federal Register API](https://apify.com/insight.solutions/federal-register-api) — rules, proposed rules, notices and the Public Inspection desk with dockets, comment deadlines and CFR references.
- [Academic Papers Search API](https://apify.com/insight.solutions/academic-papers-api) — OpenAlex, Crossref, arXiv and PubMed in one row per paper: abstract, citations, open-access PDF, authors and venue.
- [RSS & Atom Feed Monitor](https://apify.com/insight.solutions/rss-feed-monitor) — any RSS, Atom or JSON feed (or an OPML file) in, only the new items out, with keyword filters and a webhook.
- [Website Change Monitor](https://apify.com/insight.solutions/website-change-monitor) — watch any pages, diff the text between runs, get change rows with added/removed lines, keyword alerts and a webhook.
- [Wikipedia & Wikidata API](https://apify.com/insight.solutions/wikipedia-api) — article text, search, daily pageviews and Wikidata entity facts, any language edition.

**Business, finance & jobs**

- [Congress & Insider Trades API](https://apify.com/insight.solutions/congress-insider-trades-api) — STOCK Act periodic transaction reports and SEC Form 4 insider trades in one schema.
- [Federal Contracts, Grants & Lobbying API](https://apify.com/insight.solutions/federal-contracts-grants-api) — SAM.gov opportunities, USAspending awards, Grants.gov notices and Senate lobbying filings in one schema.
- [SEC EDGAR API](https://apify.com/insight.solutions/sec-edgar-api) — filings, XBRL financials and full-text search by ticker or CIK.
- [Clinical Trials & FDA API](https://apify.com/insight.solutions/clinical-trials-fda-api) — ClinicalTrials.gov studies plus openFDA recalls, labels, approvals, 510(k)s and adverse-event reports.
- [Product & Vehicle Recalls API](https://apify.com/insight.solutions/product-recalls-api) — CPSC, NHTSA, FDA and USDA recalls, vehicle complaints and ratings, plus a VIN decoder.
- [Y Combinator Companies, Batches & Founders](https://apify.com/insight.solutions/yc-companies-directory) — the YC directory with founders and social links, filterable by batch, industry and hiring status.
- [Career Site Jobs API](https://apify.com/insight.solutions/ats-jobs-api) — jobs straight from Greenhouse, Lever, Ashby, Workable and 10+ other ATS career sites.
- [Workday Jobs API](https://apify.com/insight.solutions/workday-jobs-api) — every posting on any Workday career site, with facets, search, posted dates and full descriptions.
- [New Job Postings Monitor](https://apify.com/insight.solutions/job-postings-monitor) — new, closed and changed postings on the career sites you watch.
- [Hiring Signals API — Open Roles & Hiring Surge by Company](https://apify.com/insight.solutions/hiring-signals-api) — one row per company per run: open roles, what opened and closed, department and seniority breakdowns, and a hiring-surge flag.
- [Remote Jobs API](https://apify.com/insight.solutions/remote-jobs-api) — RemoteOK, Remotive, We Work Remotely, Himalayas, Jobicy and more in one schema, deduplicated.
- [Shopify Products API](https://apify.com/insight.solutions/shopify-products-api) — any Shopify store's catalogue, variants, prices and stock signals.
- [Shopify App Store API](https://apify.com/insight.solutions/shopify-app-store-api) — Shopify App Store listings by category, collection or developer: pricing plans, ratings and merchant reviews.
- [Luma Events API](https://apify.com/insight.solutions/luma-events-api) — upcoming Luma events by city, category or calendar: dates, venue, price, capacity and organiser calendar, no host names.
- [Shopify Store Monitor](https://apify.com/insight.solutions/shopify-store-monitor) — price drops, sales, restocks, sell-outs and new products on any Shopify store, one row per change.
- [Public Tenders API](https://apify.com/insight.solutions/public-tenders-api) — EU TED, UK Find a Tender and Contracts Finder notices by keyword, CPV code, country, stage and deadline.
- [Nonprofit & IRS 990 Lookup API](https://apify.com/insight.solutions/nonprofit-990-api) — search US nonprofits and get EIN, NTEE code and multi-year Form 990 financials.
- [OpenStreetMap Places API](https://apify.com/insight.solutions/osm-places-api) — businesses and points of interest by category and area from OpenStreetMap: name, address, coordinates, website, phone, opening hours.

**Apps & games**

- [App Store & Google Play Reviews API](https://apify.com/insight.solutions/app-reviews-api) — reviews from both stores with ratings, versions and developer replies.
- [App Store Top Charts & App Search API](https://apify.com/insight.solutions/app-charts-api) — Apple top charts by country and genre, plus app search and details.
- [App Store Keyword Rank Tracker](https://apify.com/insight.solutions/app-store-keyword-rank-tracker) — where any app ranks for any keyword on the App Store and Google Play, with rank changes and ASO suggestions.
- [Steam Reviews API](https://apify.com/insight.solutions/steam-reviews-api) — Steam reviews with playtime, helpfulness and game details.
- [Steam Game Data API](https://apify.com/insight.solutions/steam-store-stats-api) — prices, tags, review scores, live player counts and top charts.
- [Sports Scores & Schedules API](https://apify.com/insight.solutions/sports-scores-api) — scores, schedules, standings, teams and headlines for the NFL, NBA, MLB, NHL, soccer and more, no key.
- [App Store Monitor](https://apify.com/insight.solutions/app-store-monitor) — App Store and Google Play listings watched between runs: new versions with release notes, price, rating and listing changes, one row per change.

# Actor input Schema

## `stores` (type: `array`):

One entry per store. A bare domain (`barefootbuttons.com`), a full URL, or a link to any page of the shop — a product page, a category page, a link with tracking parameters — all work; the path is reduced to the store. A store installed in a subdirectory (`https://example.com/shop`) is tried there first, then at the root. Redirects are followed, and duplicates are read, and billed, once.

## `search` (type: `string`):

Only products the store's own search matches (`search=`). WooCommerce searches names, descriptions and SKUs. Leave empty for the whole catalogue.

## `categories` (type: `array`):

Only products in these categories — each one is its own walk of the catalogue. Give category ids (`85`), slugs (`big-bore`), names (`Big Bore`) or category page URLs; anything but an id is looked up in the store's own category list (one extra request), and a name the store does not have comes back as a free row listing the ones it does. A product in two of your categories is returned once.

## `onSaleOnly` (type: `boolean`):

Only products the store marks as on sale (`on_sale=true`).

## `onlyAvailable` (type: `boolean`):

Only products in stock (`stock_status=instock`) — and, with one row per variation, only variations in stock. Products on backorder are left out. The store applies the filter, and the Actor checks it again on what comes back, because older stores ignore filters they do not know.

## `minPrice` (type: `number`):

Only products priced at least this, in the store's own currency and in ordinary units (`9.95`, not cents). The Store API filters in the currency's minor units, so the Actor first reads one product to learn them (one small extra request per store). A variable product matches when any of its variations does.

## `maxPrice` (type: `number`):

Only products priced at most this, in the store's own currency and in ordinary units.

## `orderBy` (type: `string`):

The order the store returns products in, which is also the order `rank` counts. `maxProductsPerStore` keeps the first N in this order — newest, cheapest, best-selling, best-rated.

## `rowPerVariant` (type: `boolean`):

For every variable product, also return one row per variation — each size or colour with its own `variationId`, `variationAttributes`, SKU, price and stock — right after the product's own row, which is still returned. It costs one extra request per variable product, and variation rows are billed like product rows, so this is off by default. `maxProductsPerStore` still counts products, not rows.

## `includeCategories` (type: `boolean`):

Also return one row per product category in the store — name, slug, parent, product count, page and image — from one request. These are `rowType: "category"` rows, billed at $0.0005 each.

## `includeDescriptionHtml` (type: `boolean`):

Add `descriptionHtml`, the merchant's raw description markup, alongside the plain-text `descriptionText` (up to 20,000 characters) every row already carries. Page builders can make the markup many times the size of the words, so it is opt-in.

## `includeImages` (type: `boolean`):

Fill in `images` — every image of the product with its URL and alt text. Off, rows keep `imageUrl`, the first image, which is a link rather than a payload. Nothing extra is fetched either way.

## `maxProductsPerStore` (type: `integer`):

How many products to read from each store, in `orderBy` order. The Store API serves up to 100 per request, so a smaller number is also a smaller download: ask for 20 and the Actor asks the store for 20. Variation rows do not count towards it.

## `maxConcurrency` (type: `integer`):

How many stores are read at once. Each parallel worker keeps its own proxy session, so a store that blocks one exit IP does not burn the others'. Requests to one store are spaced 300–600 ms apart and never more than two at a time — merchant servers are small — so this is the lever on speed for a long list.

## `maxRunSecs` (type: `integer`):

Wall-clock budget for the whole run. When it is reached the Actor stops, keeps and bills for every row it already wrote, and files a free `deadline` row for each store it cut short or never reached.

## `proxyConfiguration` (type: `object`):

The Store API is served by each merchant's own WordPress, which answers anyone, so this Actor defaults to Apify's datacenter proxy — the cheap one — and its cost is inside the per-product price. A store behind a firewall that refuses datacenter ranges comes back as a free `blocked` row; switching this to the RESIDENTIAL group is the fix. Sessions rotate automatically when an exit IP is refused.

## Actor input object example

```json
{
  "stores": [
    "barefootbuttons.com",
    "https://example.com/shop"
  ],
  "search": "",
  "categories": [],
  "onSaleOnly": false,
  "onlyAvailable": false,
  "orderBy": "date",
  "rowPerVariant": false,
  "includeCategories": false,
  "includeDescriptionHtml": false,
  "includeImages": true,
  "maxProductsPerStore": 100,
  "maxConcurrency": 3,
  "maxRunSecs": 240,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

One row per product, plus one per variation and one per category when you ask for them, and a free summary row per store. Stores that could not be read get a free diagnostic row saying why. Delivered as JSON items in the default dataset.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "stores": [
        "https://barefootbuttons.com"
    ],
    "search": "",
    "categories": [],
    "onSaleOnly": false,
    "onlyAvailable": false,
    "orderBy": "date",
    "rowPerVariant": false,
    "includeCategories": false,
    "includeDescriptionHtml": false,
    "includeImages": true,
    "maxProductsPerStore": 100,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("insight.solutions/woocommerce-products-api").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "stores": ["https://barefootbuttons.com"],
    "search": "",
    "categories": [],
    "onSaleOnly": False,
    "onlyAvailable": False,
    "orderBy": "date",
    "rowPerVariant": False,
    "includeCategories": False,
    "includeDescriptionHtml": False,
    "includeImages": True,
    "maxProductsPerStore": 100,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("insight.solutions/woocommerce-products-api").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "stores": [
    "https://barefootbuttons.com"
  ],
  "search": "",
  "categories": [],
  "onSaleOnly": false,
  "onlyAvailable": false,
  "orderBy": "date",
  "rowPerVariant": false,
  "includeCategories": false,
  "includeDescriptionHtml": false,
  "includeImages": true,
  "maxProductsPerStore": 100,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call insight.solutions/woocommerce-products-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,insight.solutions/woocommerce-products-api"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/q0Jbd9fl39ON1bCC6/builds/Lad27N4gtcahSGUCT/openapi.json
