# Wayfair Scraper - Furniture, Prices, Stock & Reviews (`abotapi/wayfair-scraper`) Actor

Scrape Wayfair.com furniture and home products by keyword or URL, including product, category, brand and sitemap links. Extract prices, discounts, ratings, variants, stock, images, specifications, dimensions and reviews. Supports scheduled price and stock change tracking.

- **URL**: https://apify.com/abotapi/wayfair-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Security:** Limited permissions
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 product results

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

## Wayfair Scraper: Furniture & Home Goods Prices, Stock & Reviews

Wayfair Scraper turns wayfair.com, the online store for furniture, decor and home goods, into a clean product data API. Search by keyword or paste product, category, brand, search and sitemap links and scrape them directly. Every product comes back as one row with price, was-price, discount, rating, review count, option list, images and key specifications, plus optional full copy, specifications, dimensions, in-stock status for every option and customer reviews, then export to JSON, CSV or Excel, or pull the results straight into your app through the API.

### Why This Scraper?

- **Two ways to find products.** Search by keyword, or paste product, category, brand, search or sitemap links and scrape them directly.
- **48 products per request, at no extra cost.** Result pages already carry price, was-price, discount, rating, review count, colour and finish options, key specifications and images, so a plain listing run is the cheapest way to watch a whole category.
- **Full detail on demand.** Switch on description, specifications, dimensions, materials, the rating breakdown, the full gallery and in-stock status for every option, and pay only for what you use.
- **Customer voice included.** Review text, stars, date, first name, location, verified-purchase flag and the options the reviewer bought, in the order you choose, with the store's own summary of what reviewers say.
- **Built for monitoring.** Incremental mode returns only new, changed, restocked and, optionally, delisted products on a schedule, a price-and-stock-only mode keeps description or image edits out of your bill, and a refused run fails loudly instead of returning an empty dataset.
- **Missing products are never charged.** A product page that no longer exists is skipped, not returned and not billed.
- **Resume a large pull.** An interrupted run can continue from where it left off without paying twice for products already collected.

### Use Cases

- **Price and discount monitoring:** track price drops, was-prices and promotion flags across a brand, a category or a keyword on a schedule.
- **Competitive intelligence for home retailers:** compare assortment, pricing, ratings and option depth against the largest online furniture store.
- **Stock and restock alerts:** watch specific products for sold-out and back-in-stock changes, option by option, with Incremental mode.
- **Review mining:** pull thousands of verified-purchase reviews with the options each reviewer bought to find what breaks, what runs small and what people love.
- **Catalogue and content teams:** build a structured feed of descriptions, specifications, dimensions and images for a comparison site or app.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `sku` | `"W000000001"` (the product code; this is the row's identity) |
| `name` / `brand` / `collection` | `"Sample Task Chair"` / `"Sample Brand"` / `"Sample Collection"` |
| `url` | link to this product, without tracking parameters |
| `price` / `wasPrice` / `currency` | `212.99` / `244.99` / `"USD"` |
| `onSale` / `discountPercent` / `discountLabel` / `priceLabel` | `true` / `13` / `"13% Off"` / `"Sale"` |
| `rating` / `reviewCount` | `4.6` / `477` |
| `options` | every colour, size or finish choice with its link; with a product page read, also `inStock` for each and `selected` for the one shown |
| `optionCount` / `selectedOptions` | `14` / `{"Upholstery Color": "Pink"}` |
| `imageUrl` / `images` | main photo and the gallery |
| `promoFlag` / `isBestValue` / `isSponsored` | `"Fall Clearance"` / `true` / `false` (result pages only) |
| `keySpecs` | key specifications shown on the result card (result pages only) |
| `stockMessage` / `deliveryPromise` / `arrivalMessage` | the result card's units-left, delivery and arrival texts (result pages only) |
| `stockStatus` | `"IN_STOCK"` or `"OUT_OF_STOCK"` for the shown option (needs a product page read) |
| `description` / `highlights` | the product copy and its highlights (needs a product page read) |
| `specifications` / `dimensions` / `materials` | key-value specifications, measured dimensions and material list (needs a product page read) |
| `category` / `categoryId` / `breadcrumbs` | the product's category path (needs a product page read) |
| `ratingHistogram` / `reviewSummary` | review count per star and the store's own summary of the reviews (needs a product page read) |
| `reviews` | written reviews (with **Fetch customer reviews**) |
| `position` / `page` | where the product ranked in the results |
| `changeType` / `changedFields` / `firstSeenAt` / `lastSeenAt` | incremental mode: `"NEW"` / `[]` / timestamps |

Every record also carries `variantId`, `optionIds`, `brandId`, `brandUrl`, `videos`, `detailsFetched`, `reviewsRequested`, `reviewsDelivered`, `searchTerm`, `sourceUrl` and `scrapedAt`.

### How to Use

1. Pick a **mode**: `search` (by keyword) or `url` (paste Wayfair links).
2. Fill in the fields for that mode, then apply any sort, price, rating, brand or sale filters you want; they apply to both modes.
3. Set **Max products** to control run size and cost, then click **Start**.
4. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Search by keyword:**

```json
{
  "mode": "search",
  "queries": ["standing desk"],
  "maxItems": 50
}
```

**Search with filters, discounted only, cheapest first:**

```json
{
  "mode": "search",
  "queries": ["area rug"],
  "sortBy": "priceAsc",
  "maxPrice": 150,
  "minRating": 4,
  "onSaleOnly": true,
  "maxItems": 100
}
```

If you leave `urls` untouched in url mode, the actor scrapes six sample links, one per link shape. Replace them with your own links.

**Paste links, with full product-page detail:**

```json
{
  "mode": "url",
  "urls": [
    "https://www.wayfair.com/furniture/sb0/office-chairs-c478390.html",
    "https://www.wayfair.com/brand/bnd/lark-manor-b37152.html"
  ],
  "fetchDetails": true,
  "maxItems": 40
}
```

**Monitor a category on a schedule, price and stock only:**

```json
{
  "mode": "url",
  "urls": ["https://www.wayfair.com/furniture/sb0/office-chairs-c478390.html"],
  "incrementalMode": true,
  "changeScope": "priceAndStock",
  "emitExpired": true
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/wayfair-scraper").call(run_input={"mode": "search", "queries": ["standing desk"], "maxItems": 50})
for product in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(product["name"], product["price"], product["rating"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/wayfair-scraper').call({ mode: 'search', queries: ['standing desk'], maxItems: 50 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Link shapes you can paste

In url mode the actor reads these shapes of link: a keyword search (`/keyword.php?keyword=...`), a category or search-result page (for example `/furniture/sb0/office-chairs-c478390.html`), a brand page (`/brand/bnd/<brand>-b<id>.html`), a product page (`/<department>/pdp/<name>-<code>.html`, optionally with `?piid=` to pick an option) and the store's product sitemaps (`/seo-pdp-sitemap~<n>.xml`, each listing hundreds of products). Filtered category pages, curated collections and daily-sale pages are read like any other result page, though editorial pages carry few products. Other pages of the store (help, account, cart, financing) are skipped with a warning. If you leave `urls` untouched, one sample of each shape is used. A result-page link that starts past page 1 is read from that page, and a `sortby` value written into a link is kept.

#### Output tab views

The Output tab offers four views: **Products** (the result-card fields), **Product detail** (specifications, dimensions, options and gallery), **Customer reviews** (one row per written review, unwound from the reviews array) and **Changes since the last run** (incremental status and changed fields; with Compact updates on, also the old and new values of each price or stock change and the per-option changes). Customer reviews are capped per product by **Max reviews per product**, from 1 to 1000 (default 20).

#### Keeping a run cheap

A result page already carries price, discount, options, images and rating for about 48 products, at no extra cost. **Read each product's page** and **Fetch customer reviews** each need one extra page per product and are billed with one Product page detail event per product that reaches the dataset. They share that one page, so turning both on costs the same as turning on either. Each review returned inside a row is billed with one Customer review event. A pasted product link always reads the product page, because there is no result card behind it to fall back on, so it is billed one Product page detail event whenever it reaches the dataset, whatever the toggle says. Product pages that no longer exist are never returned and never billed.

#### How Max products and the filters shape a run

When **Max products** is set, every keyword and link in a run gets its own share of it, split evenly, so one popular search cannot use up the whole run and starve the rest. The price, rating, brand, sale and sponsored filters narrow the rows this actor returns rather than being sent to the store, so a narrowed run reads more result pages per row returned. The store serves at most a few hundred result pages for any one search; when it stops serving new products the run says so and the search counts as a partial scan.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset ID and the actor skips everything already collected there, so you don't pay twice.
- **Incremental mode** (`incrementalMode`) is for scheduled runs over the same search or links. Each product is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED` or `EXPIRED` (only after a run that scanned the whole tracked scope, and only with `emitExpired`). `stateKey` names or shares the stored state. With Incremental mode off, output is exactly as before.

**What counts as a change.** Compared: price, was-price, discount, on-sale flag, stock status, the option list with per-option stock, name, brand, description, specifications, dimensions and categories. Excluded by default but still returned in the row: scrape timestamps, the product's rank and page, the sponsored flag, delivery promise and arrival date (they depend on the day and the shipping address), the live units-left message, how the product was found, image and video links (the gallery shown on a result card varies between two page layouts), and ratings and reviews (average rating, review count, rating breakdown, review summary and the review sample, which move with every new review). Option and material lists are compared regardless of the order the store returns them. Switching the exclusions never causes a one-time wave of `UPDATED` rows: `UNCHANGED` is decided by re-reading the stored record under the current settings.

- `changeScope: priceAndStock` compares only price, was-price, discount, on-sale flag, stock status and per-option stock, so a name, description or specification edit is never a billable `UPDATED` row. Switching `changeScope`, or editing `ignoreFieldsForChanges`, keeps your existing saved state: nothing comes back as `NEW`, each stored product is simply re-compared under the new setting.
- `compactUpdates` turns `UPDATED` and `EXPIRED` rows into slim rows (ids, link, name, current price and stock, `priceChanges` old and new, `variantChanges` per option as `ADDED`, `REMOVED` or `CHANGED`). `NEW` rows stay full. A compact row does not carry product-page detail fields or reviews, so no detail or review charge applies to it.
- `expireAfterMissedScans` applies with `emitExpired`. `EXPIRED` means the product dropped out of the scanned results; it does not prove it is unavailable, because no product page is opened. Use `stockStatus` for availability.
- A pasted link that starts past page 1, a run stopped by **Max products**, **Max result pages**, the time limit or your maximum cost per run, a resumed run, or a run whose search or filters differ from those the state key was last saved with never produces `EXPIRED` rows.

`EXPIRED` rows count against **Max products** like every other row. They are only produced after a run that scanned the whole tracked scope, so a run that stopped because it reached **Max products** returns none. Products held back that way stay tracked and are returned, and billed, as `EXPIRED` on a later complete run.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each product, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify, Settings, Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` for keywords plus filters, or `url` to scrape pasted Wayfair links. |
| `queries` | array | `["standing desk"]` | Search keywords (search mode). |
| `urls` | array | six sample links (one per link shape) | Wayfair product, search, category, brand or sitemap links (url mode). |
| `sortBy` | string | `relevance` | `relevance`, `priceAsc`, `priceDesc` or `rating`. A sort written into a pasted link wins. |
| `minPrice` | number | (none) | Only keep products at or above this price, in the store's currency. |
| `maxPrice` | number | (none) | Only keep products at or below this price, in the store's currency. |
| `onSaleOnly` | boolean | `false` | Keep only products whose current price is below their was-price. |
| `minRating` | number | (none) | Keep only products with an average rating of at least this value (0 to 5). |
| `brands` | array | (none) | Keep only products from these brands (case and trademark signs ignored). |
| `skipSponsored` | boolean | `false` | Leave out the paid placements mixed into result pages. |
| `fetchDetails` | boolean | `false` | Read every product's own page for the full description, specifications, dimensions and per-option stock. |
| `fetchReviews` | boolean | `false` | Also collect customer reviews (reads the product page too). |
| `maxReviewsPerProduct` | integer | `20` | Cap on reviews collected per product, from 1 to 1000. |
| `reviewSort` | string | `relevance` | `relevance`, `newest`, `highest` or `lowest`. |
| `includeReviewImages` | boolean | `false` | Add the photo links reviewers attached. |
| `maxItems` | integer | `20` | Maximum number of products to return across the whole run, up to 100000. `0` = the 100000 maximum. |
| `maxPages` | integer | `0` | Maximum result pages per keyword or link. `0` = no limit set; a safety limit always applies, and the log says when a limit stops a search that still had results. |
| `resumeFromRunId` | string | (none) | Continue one interrupted run without re-collecting or re-billing its products. |
| `incrementalMode` | boolean | `false` | Return only new and changed products on scheduled runs. |
| `stateKey` | string | (none) | Name or share an incremental-mode state. |
| `changeScope` | string | `all` | `priceAndStock` limits change detection to price, stock and per-option stock. |
| `compactUpdates` | boolean | `false` | Slim `UPDATED` and `EXPIRED` rows with `priceChanges` and `variantChanges`. |
| `expireAfterMissedScans` | integer | `1` | Mark `EXPIRED` only after this many consecutive complete scans missed the product. |
| `ignoreFieldsForChanges` | array | (none) | Extra fields (or dotted paths such as `options.url`) that never count as a change. |
| `emitUnchanged` | boolean | `false` | Also return (and bill) unchanged products. |
| `emitExpired` | boolean | `false` | Also return (and bill) products no longer found after a full scan, within the room left under **Max products**. |
| `proxy` | object | `{"useApifyProxy": true}` | Connection settings. |
| `mcpConnectors` | array | (none) | Optional: send a summary of each product to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which products are created. |
| `maxNotifyListings` | integer | `50` | Cap on products written to each connector per run. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

```json
{
  "recordType": "product",
  "sku": "W000000001",
  "variantId": "AB00000001",
  "optionIds": "100000001",
  "name": "Sample Task Chair",
  "brand": "Sample Brand",
  "collection": "Sample Collection",
  "url": "https://www.wayfair.com/furniture/pdp/sample-brand-sample-task-chair-w000000001.html?piid=100000001",
  "price": 212.99,
  "wasPrice": 244.99,
  "currency": "USD",
  "onSale": true,
  "discountPercent": 13,
  "discountLabel": "13% Off",
  "priceLabel": "Sale",
  "rating": 4.6,
  "reviewCount": 477,
  "ratingHistogram": {"1": 10, "2": 8, "3": 28, "4": 75, "5": 356},
  "stockStatus": "IN_STOCK",
  "category": "Office Chairs",
  "breadcrumbs": [{"name": "Furniture", "url": "https://www.wayfair.com/furniture/cat/furniture-c45974.html"}],
  "description": "A sample description of the product.",
  "specifications": {"Product Type": "Task Chair", "Weight Capacity": "300 lb."},
  "dimensions": {"Overall Dimensions": "39'' H X 26'' W X 26'' D"},
  "materials": ["Linen", "Foam"],
  "selectedOptions": {"Upholstery Color": "Pink"},
  "options": [
    {"optionKey": "Upholstery Color: Pink", "group": "Upholstery Color", "name": "Pink", "optionId": 100000001, "inStock": true, "selected": true, "url": "https://www.wayfair.com/furniture/pdp/sample-brand-sample-task-chair-w000000001.html?piid=100000001"}
  ],
  "optionCount": 14,
  "imageUrl": "https://assets.wfcdn.com/im/0/compr-r85/0000/000000001/i.jpg",
  "promoFlag": "Fall Clearance",
  "isBestValue": true,
  "isSponsored": false,
  "position": 3,
  "page": 1,
  "detailsFetched": true,
  "reviewsRequested": true,
  "reviews": [
    {"reviewId": 1, "rating": 5, "text": "Sample review text.", "date": "09/25/2026", "author": "Sam", "location": "Springfield, ST", "verifiedPurchase": true, "purchasedOptions": {"Upholstery Color": "Green"}, "language": "en", "images": []}
  ],
  "reviewsDelivered": 1,
  "changeType": "NEW",
  "changedFields": [],
  "firstSeenAt": "2026-10-31T12:00:00Z",
  "lastSeenAt": "2026-10-31T12:00:00Z",
  "scrapedAt": "2026-10-31T12:00:00Z"
}
```

### Plan Requirement

The default connection works: the standard Apify datacenter pool served product and result pages in testing, so no residential proxy is needed to get started. This store does sometimes refuse a page; when it does, the run switches to a fresh connection and retries. If refusals keep coming it escalates, in order, to residential exits, then to backup connections, and finally to Apify Proxy's enhanced group, and it stays on the connection that works for the rest of the run. Residential connections give the most reliable results on a bad day, and you can select them in the Connection section. Proxy traffic is billed on your own Apify plan, and the enhanced group costs more per request. Customer reviews beyond the first few cannot be read on the enhanced group, and the run says so.

If your plan does not include proxy access, the run says so in plain words rather than returning an empty dataset. These are the messages you may see in the run log:

- Warning: "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable." followed by an upgrade pointer.
- Info: "Served over the backup connection (the default connection was unavailable)."
- Failure: "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required".
- Failure: "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested Wayfair pages."
- Failure: "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later."

You can supply your own proxy URLs instead; custom proxies get no automatic backup connection.

### FAQ

#### How much does it cost?

Four events can appear on a run, and the **Pricing** tab shows the current rates:

- **Dataset item:** one event per product row returned in the dataset (incremental `UNCHANGED` rows are suppressed and not billed unless **Emit unchanged products** is on).
- **Product page detail:** one event per product row whose own page is read, whether through **Read each product's page**, **Fetch customer reviews** or a pasted product link. Turning both toggles on still costs one event per product.
- **Customer review:** one event per review returned inside a product row.
- **Actor start:** one event each time a run starts.

Use **Max products** to cap the cost of any run. Failed, empty or missing pages are never billed, and a run that reaches your maximum cost per run stops and says so.

#### Is it legal to scrape Wayfair?

This actor collects only publicly available product data. You are responsible for how you use it: follow Wayfair's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Prices and specifications are generally facts, but product photos and brand names may be subject to third-party rights. Reviews are written by private individuals, so treat them as personal data where privacy law applies.

#### Can I get only new or changed products on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new, updated and restocked products, and unchanged ones are not billed unless you turn on **Emit unchanged**.

#### Why does a product show a different price than the website for me?

Prices and delivery promises can depend on the option shown and on the shipping address the connection resolves to. The price on a row is the price of the option named in `selectedOptions` (or in `optionIds`). Delivery texts are excluded from change detection for that reason.

#### Why did my run fail instead of returning an empty dataset?

If the store refuses every request, the run stops with a clear message so "no matches today" is never confused with "nothing could be read". Run it again in a few minutes. If only some pages could be read, you still get what was found, and the run tells you the result is incomplete.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

### 🔗 Want more e-commerce data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🛒 <a href="https://apify.com/abotapi/nitori-net-jp-scraper"><b>Nitori Japan Furniture &amp; Home Goods Scraper</b></a><br>Scrape Nitori Japan (nitori-net.jp) furniture and home goods by keyword, category or...</td><td>🛒 <a href="https://apify.com/abotapi/harvey-norman-scraper"><b>Harvey Norman Scraper</b></a><br>Scrape Harvey Norman Australia products: name, brand, price, was-price / discount...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/netshoes-scraper"><b>Netshoes Brazil</b></a><br>Scrape sportswear and footwear from netshoes.com.br. Search by keyword with the store's...</td><td>👗 <a href="https://apify.com/abotapi/otto-de-scraper"><b>OTTO.de Scraper</b></a><br>Scrape OTTO.de products by search or URL. Extract prices, discounts, variants...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/bunnings-com-au-scraper"><b>Bunnings Scraper</b></a><br>Scrape bunnings.com.au products with full specifications, price, brand, stock, image...</td><td>🛒 <a href="https://apify.com/abotapi/muji-scraper"><b>MUJI Scraper</b></a><br>Scrape MUJI products from the United States, Canada and Australia storefronts. Search by...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/wayfair-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Choose 'search' for keywords plus filters, or 'url' to scrape pasted Wayfair links. Each mode reads only its own field below.

## `queries` (type: `array`):

Only used when mode = search, ignored in URL mode. One or more keywords, for example 'standing desk', 'area rug 8x10' or 'bar stool'.

## `urls` (type: `array`):

Only used when mode = url, ignored in search mode. Product links (/pdp/), search links (keyword.php?keyword=...), category, brand and curated-collection pages and the store's product sitemaps (seo-pdp-sitemap~N.xml) are all accepted; other store pages are skipped with a warning. A result-page link that already carries curpage=N starts at that page, and a sortby value written into a link is kept. Tracking parameters are ignored.

## `sortBy` (type: `string`):

Order of the result pages. A sort order written into a pasted link wins over this setting. The store mixes sponsored products into every ordering, so the order is not strict.

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

Only return products priced at or above this value, in the store's currency (US dollars). Applied to the returned rows.

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

Only return products priced at or below this value, in the store's currency (US dollars). Applied to the returned rows.

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

Only return products that currently show a was-price above the current price.

## `minRating` (type: `number`):

Only return products whose average customer rating is at least this value (0 to 5).

## `brands` (type: `array`):

Only return products from these brands. Matching ignores case and trademark signs, for example 'Lark Manor'.

## `skipSponsored` (type: `boolean`):

Result pages mix paid placements in with the organic products. Turn on to leave the sponsored ones out; each returned row carries an isSponsored flag either way.

## `fetchDetails` (type: `boolean`):

Read every product's own page for the full description, specifications, dimensions, materials, breadcrumbs, the rating breakdown, the full image and video gallery and in-stock status for every option. Off by default because it costs one extra page per product. A pasted product link always reads the product page, because there is no result card behind it, so a product link is always billed one 'Product page detail' event whatever this toggle says.

## `fetchReviews` (type: `boolean`):

Also collect customer reviews for each product: rating, text, date, first name, location, verified-purchase flag and the options the reviewer bought. Also reads the product page, so it carries the same per-product surcharge as the toggle above, plus one 'Customer review' event per review returned. The average rating, review count and rating breakdown do not need this.

## `maxReviewsPerProduct` (type: `integer`):

Cap on how many individual reviews are collected for each product when 'Fetch customer reviews' is on. The first few come with the product page; further ones are read twenty at a time. Each review returned is billed, so keep this tight.

## `reviewSort` (type: `string`):

Which reviews come first when a product has more than the cap.

## `includeReviewImages` (type: `boolean`):

Add the links of the photos reviewers attached to each review.

## `maxItems` (type: `integer`):

Maximum number of products to return across the whole run, EXPIRED rows from Incremental mode included. This is the run's cap. Use 0 for the maximum of 100000 products per run.

## `maxPages` (type: `integer`):

Maximum number of result pages to take per keyword or link. Each page carries about 48 products. 0 = no page limit set; the run then stops at Max products, when there are no more results, when the store stops serving new products for the search, or at a safety limit of 4167 pages per keyword or link (far above what Max products can reach). When a limit you set here stops a search that still had results, the log says so.

## `proxy` (type: `object`):

Connection used for every request. Leave the default; the run escalates on its own when the store refuses a page. Choose residential for the most reliable results.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a large catalogue pull without returning or charging for products already collected there. Use this after an interrupted run. For recurring monitoring of the same search, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns every matching product as NEW. Later runs normally return only NEW, UPDATED and REAPPEARED products, which makes it a cheap way to watch price drops and restocks. What is compared: price, was-price, discount, on-sale flag, stock status, the option list with per-option stock, name, brand, description, specifications, dimensions and categories. What is excluded by default (still returned in the row, never a change): scrape timestamps, the product's rank and page in the results, the sponsored flag, delivery promise and arrival date, the live units-left message, how the product was found, image and video links, and ratings and reviews (average rating, review count, rating breakdown, review summary and the review sample). Option and material lists are compared regardless of the order the store returns them. 'Changes to watch' narrows this to price and stock only. Turn on 'Emit unchanged' or 'Emit expired' only when you also want those rows returned (and billed). State is kept separately for each search and filter setup; use State key to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently configured runs. Leave empty to derive a key automatically from the search and filter settings, so two different searches never mix state.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return products that have not changed since the last run, marked UNCHANGED. This returns, and bills, extra rows you already have, so leave it off unless you want a full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return products that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search, never when Max products capped it or Resume was used. EXPIRED rows count against Max products: a run returns at most the room left under it, and the rest are returned on a later complete run. This returns, and bills, extra rows, so leave it off unless you need expiry tracking.

## `changeScope` (type: `string`):

All fields (default): any change to the compared product data marks a product UPDATED. Price and stock only: just price, was-price, discount, on-sale flag, stock status and per-option stock count, so name, description or specification edits never produce a billable UPDATED row. Changing this never starts a new baseline: the existing saved state is kept (under an automatic or a named State key) and each stored product is simply compared under the new setting.

## `compactUpdates` (type: `boolean`):

Off by default. When on, UPDATED and EXPIRED rows are slim: ids, link, title, current price and stock, plus priceChanges (old and new value of each field that moved) and variantChanges (per-option ADDED, REMOVED or CHANGED). NEW rows stay full. A compact row does not carry product-page detail fields or reviews, so no detail or review charge applies to it.

## `expireAfterMissedScans` (type: `integer`):

Used with 'Emit expired'. A product is marked EXPIRED only after it was missing from this many CONSECUTIVE complete scans (default 1). EXPIRED means the product dropped out of the scanned results; it does not prove the product is unavailable, because no product page is opened. Check stockStatus for availability.

## `ignoreFieldsForChanges` (type: `array`):

Field names that must not count as a change, for example name or promoFlag. Use a dotted path to ignore a key inside every element of a list, for example options.url. Ignored fields are still returned in the row. Rank, sponsored flag, delivery text, media links and ratings and reviews are already ignored by default. Changing this list never starts a new baseline: the saved state is kept and each stored product is re-compared under the new list, so no product comes back as NEW.

## `mcpConnectors` (type: `array`):

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify, Settings, API & Integrations, then select it here. Notion gets a page per product; other connectors get a best-effort write or digest. Each connector receives a condensed summary per product, not the full record; the complete record always stays in the dataset. Leave empty to skip; this never changes the dataset output. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL or id of the Notion page under which product pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on products written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "queries": [
    "standing desk"
  ],
  "urls": [
    "https://www.wayfair.com/keyword.php?keyword=standing+desk",
    "https://www.wayfair.com/furniture/sb0/office-chairs-c478390.html",
    "https://www.wayfair.com/furniture/sb0/office-chairs-c478390.html?curpage=3",
    "https://www.wayfair.com/brand/bnd/lark-manor-b37152.html",
    "https://www.wayfair.com/furniture/pdp/lark-manor-asyra-wide-back-tufted-swivel-task-chair-w100057029.html?piid=729629132",
    "https://www.wayfair.com/seo-pdp-sitemap~3.xml"
  ],
  "sortBy": "relevance",
  "onSaleOnly": false,
  "skipSponsored": false,
  "fetchDetails": false,
  "fetchReviews": false,
  "maxReviewsPerProduct": 20,
  "reviewSort": "relevance",
  "includeReviewImages": false,
  "maxItems": 20,
  "maxPages": 0,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "changeScope": "all",
  "compactUpdates": false,
  "expireAfterMissedScans": 1,
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `reviews` (type: `string`):

No description

## `changes` (type: `string`):

No description

# 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 = {
    "mode": "search",
    "queries": [
        "standing desk"
    ],
    "urls": [
        "https://www.wayfair.com/keyword.php?keyword=standing+desk",
        "https://www.wayfair.com/furniture/sb0/office-chairs-c478390.html",
        "https://www.wayfair.com/furniture/sb0/office-chairs-c478390.html?curpage=3",
        "https://www.wayfair.com/brand/bnd/lark-manor-b37152.html",
        "https://www.wayfair.com/furniture/pdp/lark-manor-asyra-wide-back-tufted-swivel-task-chair-w100057029.html?piid=729629132",
        "https://www.wayfair.com/seo-pdp-sitemap~3.xml"
    ],
    "onSaleOnly": false,
    "skipSponsored": false,
    "fetchDetails": false,
    "fetchReviews": false,
    "includeReviewImages": false,
    "proxy": {
        "useApifyProxy": true
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "compactUpdates": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/wayfair-scraper").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 = {
    "mode": "search",
    "queries": ["standing desk"],
    "urls": [
        "https://www.wayfair.com/keyword.php?keyword=standing+desk",
        "https://www.wayfair.com/furniture/sb0/office-chairs-c478390.html",
        "https://www.wayfair.com/furniture/sb0/office-chairs-c478390.html?curpage=3",
        "https://www.wayfair.com/brand/bnd/lark-manor-b37152.html",
        "https://www.wayfair.com/furniture/pdp/lark-manor-asyra-wide-back-tufted-swivel-task-chair-w100057029.html?piid=729629132",
        "https://www.wayfair.com/seo-pdp-sitemap~3.xml",
    ],
    "onSaleOnly": False,
    "skipSponsored": False,
    "fetchDetails": False,
    "fetchReviews": False,
    "includeReviewImages": False,
    "proxy": { "useApifyProxy": True },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "compactUpdates": False,
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/wayfair-scraper").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 '{
  "mode": "search",
  "queries": [
    "standing desk"
  ],
  "urls": [
    "https://www.wayfair.com/keyword.php?keyword=standing+desk",
    "https://www.wayfair.com/furniture/sb0/office-chairs-c478390.html",
    "https://www.wayfair.com/furniture/sb0/office-chairs-c478390.html?curpage=3",
    "https://www.wayfair.com/brand/bnd/lark-manor-b37152.html",
    "https://www.wayfair.com/furniture/pdp/lark-manor-asyra-wide-back-tufted-swivel-task-chair-w100057029.html?piid=729629132",
    "https://www.wayfair.com/seo-pdp-sitemap~3.xml"
  ],
  "onSaleOnly": false,
  "skipSponsored": false,
  "fetchDetails": false,
  "fetchReviews": false,
  "includeReviewImages": false,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "compactUpdates": false
}' |
apify call abotapi/wayfair-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/wayfair-scraper"
        }
    }
}
```

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/h2Nf1wgbku9D3FyXH/builds/XAIDCk3c84sHbHgto/openapi.json
