# Wayfair Scraper - Prices, Discounts and Ratings (`s-r/wayfair-scraper`) Actor

Scrape Wayfair category listings: product name, current price, strikethrough was-price with discount percent, rating and review count. Reads the GraphQL payload embedded in the page, through US-pinned rotating exits.

- **URL**: https://apify.com/s-r/wayfair-scraper.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** E-commerce, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 run start fees

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

Scrape **Wayfair** category listings: product name, current price, the
strikethrough was-price with the discount percentage worked out, rating and
review count. Point it at any category URL and it pages through.

### Retries are normal, and raising them is the fix

A small share of requests come back empty on the first try. That is expected
and it is not the site closing: asking again clears it, which is why the
`retries` default is 5 rather than 1.

If you see `fetch_failed` in the run's errors, raise `retries` before assuming
the page is unavailable. Everything else is handled for you.

### Where the data actually is

Not in JSON-LD. Wayfair publishes only `WebSite` and `BreadcrumbList` there —
nothing about products. The catalogue lives in an **escaped GraphQL payload**
embedded in the HTML, which this Actor unescapes and reads.

Two quirks of that payload are worth knowing if you ever compare output against
the raw page:

**Prices carry a doubled currency symbol**: `"$$319.99"`. Stripping the first
character, which is the obvious thing to do, turns $319.99 into $19.99. The
numbers are extracted rather than trimmed.

**Product blocks are anchored on `leadPrice`.** That field occurs exactly once
per priced product. `displayListingId` occurs **205 times** on a 50-product page
and `listingUrl` 225 times, because both also appear in tracking metadata.
Anchoring on either over-counts fourfold, collapses the extraction windows, and
shifts every field by one row — so you get product 3's price on product 2's
name, with nothing to warn you. That is the single most dangerous failure this
parser could have, and the tests pin it.

The `name` comes from the product URL slug, because that is the only place the
page states a plain product name. Wayfair's `marketingCopy` is a description and
is returned as `description`, under its real meaning.

### Discounts are only reported when they are real

`on_sale` is set **only** when both conditions hold: Wayfair marks the price
line `SALE`, *and* the strikethrough price is genuinely above the current one.

That caution is deliberate. A strikethrough that is not higher than the lead
price is a formatting artefact, and on other retail sites a pair of prices that
looks like was-and-now often turns out to be a low-to-high range across
merchants. Reporting those as markdowns would invent discounts that do not
exist. When the pair does not qualify, `was_price` comes back `null` rather than
being filled in anyway.

`discount_percent` is computed from the two, so it only ever exists alongside a
real markdown.

### Fields

| Field | What it is |
|---|---|
| `listing_id` | Wayfair's own id, e.g. `W117455547`. Stable, use it to join runs |
| `variant_id` | The priced variant |
| `name` | Product name from the URL slug |
| `description` | Wayfair's marketing copy |
| `manufacturer` | Brand, where published |
| `price` | Current price |
| `was_price` | Strikethrough price, only on a real markdown |
| `on_sale` | Whether a genuine discount is live |
| `discount_percent` | Percent off |
| `rating`, `review_count` | Average rating and how many reviews |
| `url` | Product page |

### Input reference

| Field | Type | Default |
|---|---|---|
| `category_url` | Wayfair category page | sofas |
| `limit` | 1-2000 | 96 |
| `retries` | 1-10 | 5 |

A non-Wayfair URL is rejected with a message rather than fetched.

### Typical uses

- **Competitive price tracking.** Run a category on a schedule, join snapshots
  on `listing_id`, and you have a price history with the discount state
  attached.
- **Discount depth analysis.** `discount_percent` across a category shows how
  hard a range is being marked down, and `on_sale` tells you what share of the
  assortment is discounted at all.
- **Assortment and review mining.** Rating and review count per listing at
  category scale shows which products actually sell.

### Notes on behaviour

Pages are paced with a short randomised gap. Requests already cost a retry now
and then, and hammering the site would raise the block rate for everyone using
the same pool.

A page that loads but yields no listing objects is reported as `no_products`
rather than returned as an empty result, and a refused page is never parsed
as an empty category. A successful-looking run with zero rows would be the worst
outcome here, so both are made loud.

Prices are US dollars from the US site.

# Actor input Schema

## `category_url` (type: `string`):

A Wayfair category page, for example https://www.wayfair.com/furniture/sb0/sofas-c413892.html. Paginated automatically.

## `limit` (type: `integer`):

How many products to return. Roughly 48 per page.

## `retries` (type: `integer`):

How many exits to try before giving up. Wayfair is behind PerimeterX and only about 1 exit in 20 is clean (measured 2026-09-07), so this is a hunt budget rather than a retry count. A refusal is a 5.9 KB 429, so hunting is cheap; once a clean exit is found it is pinned and reused for every later page.

## Actor input object example

```json
{
  "category_url": "https://www.wayfair.com/furniture/sb0/sofas-c413892.html",
  "limit": 96,
  "retries": 40
}
```

# Actor output Schema

## `products` (type: `string`):

One row per product listing.

## `summary` (type: `string`):

Counts, price and rating coverage, and how many products are on sale.

## `errors` (type: `string`):

Failures with a code and a redacted message.

# 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 = {
    "category_url": "https://www.wayfair.com/furniture/sb0/sofas-c413892.html",
    "limit": 96,
    "retries": 40
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/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 = {
    "category_url": "https://www.wayfair.com/furniture/sb0/sofas-c413892.html",
    "limit": 96,
    "retries": 40,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/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 '{
  "category_url": "https://www.wayfair.com/furniture/sb0/sofas-c413892.html",
  "limit": 96,
  "retries": 40
}' |
apify call s-r/wayfair-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,s-r/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/NNR0sPLn1GhY3hzMy/builds/JiDvsgIo6IbJKdmod/openapi.json
