# Chairish Scraper: New Listing & Price Drop Alerts (`accountable_eel/chairish-listing-lookup`) Actor

Chairish scraper by category: US vintage furniture and decor with price, markdown flag and dealer info, plus new-listing and price-drop alerts. No login required. Pay per listing; misses and quiet runs are free.

- **URL**: https://apify.com/accountable\_eel/chairish-listing-lookup.md
- **Developed by:** [Adrian Voss](https://apify.com/accountable_eel) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 listing returneds

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

## Chairish Scraper: New Listing & Price Drop Alerts

**Watch a search, get only what's new.** New listings and price drops since your last run, charged
per new row, free on quiet days. Schedule it hourly and send it to Discord, Slack, Google Sheets or
n8n.

This actor reads Chairish, the US vintage furniture and decor marketplace, by category: give it a
keyword like "chairs" or "wall mirrors", or paste a `chairish.com/collection/...` link, and get
one row per listing with the title, price, Chairish's own category path, its newly-made/vintage
flag, and the link. Turn on monitoring and each run returns only the listings that appeared, or got
cheaper, since the previous run — including a listing Chairish itself just marked down.

### Who it's for

- **Dealers and sourcing agents** hunting vintage pieces on Chairish who lose good finds to whoever
  refreshes the page fastest. A watchlist on "chairs" or "chaise lounge", run every hour,
  puts new listings in your Discord or Telegram without keeping a tab open.
- **Interior designers** researching what a category or period piece genuinely costs on Chairish
  right now.
- **Resellers** watching for Chairish's own markdowns — this actor treats a listing going on sale
  as a price-drop alert, not just a price falling below what this monitor last saw.
- **Bot and workflow builders** who want a stable JSON row per listing instead of maintaining their
  own scraper.

### Why this one

- **Category-first search, honestly.** Chairish's own `robots.txt` disallows `/search`, so this
  actor never uses it — and it only maps keywords to the Chairish collections this build actually
  confirmed render a real product grid server-side (`chairs`, `wall mirrors`, `chaise lounge`,
  `runner rugs`, `bar stools`, and a handful more). Most of Chairish's own category names for common
  furniture nouns (mirrors, sofas, tables, dining chairs...) turned out to be "hub" pages with an
  empty, JavaScript-only product grid; rather than silently return zero rows for those, this actor
  rejects them up front with a clear message. A pasted `chairish.com/collection/...` link works for
  any leaf collection, including ones this build didn't test.
- **Monitoring built in, not bolted on.** `deltaMode` remembers what each watchlist has seen. New
  listings and price drops (with your own minimum drop %) come back; everything else is removed
  before billing. A quiet run returns one summary row and costs only the start fee.
- **Chairish's own markdown flag counts too.** A listing that goes on sale is a price drop even the
  first time this monitor sees it — see "Monitoring" below.
- **No login required.** Chairish's collection pages are plain server-rendered HTML — no CAPTCHA,
  and a residential IP reads them with no challenge at all.

### What you get

By default each listing is its own row. Every row also repeats the search it came from.

| Field | What it is |
|---|---|
| `listingId`, `url`, `title` | Chairish's listing ID, the listing link and its title |
| `price`, `currency` | The price in US dollars |
| `category` | Chairish's own full category path (e.g. "Furniture/Seating/Dining Chairs") |
| `isNewlyMade`, `condition` | Chairish's own newly-made flag, and the same thing as `New` / `Vintage/Antique` — not a graded condition (see FAQ) |
| `isDiscounted`, `discountPct` | Chairish's own markdown flag and percentage |
| `isPurchasable` | Whether Chairish currently shows the listing as purchasable |
| `imageUrl` | Link to the main photo (links only, nothing is downloaded) |
| `sellerType`, `sellerHash` | Always `dealer` (see FAQ), and an anonymous 16-character dealer hash |
| `isNew`, `changeType`, `firstSeenAt` | Monitoring: `new`, `price-drop` or `seen`, and when this watchlist first saw it |
| `previousPrice`, `priceDropPct` | Monitoring: the price last seen and the drop in %, on price-drop rows |
| `searchQuery`, `listingCount`, `truncated` | The search summary |
| `newCount`, `priceDropCount`, `monitorStatus` | Monitoring summary |

Not in this actor, because Chairish's own listing cards don't carry them (confirmed on the sampled
cards): a posting date or a total-matches count. `totalAvailable` is always empty.

### Monitoring: new listings and price drops

The recipe most buyers use:

1. Put your search in `searches` (a Chairish category keyword, or paste a collection link), and
   turn on **Only return listings that are new, or cheaper, since the last run** (`deltaMode`).
2. Optionally turn on **Seed silently** so the first run remembers today's listings without sending
   them all to your webhook.
3. Save it as a Task and add an hourly **Schedule** in Apify.
4. Add a webhook or integration on the Task for **Run succeeded**: Discord or Slack webhook,
   Telegram bot via n8n or Make, a Google Sheets append, or an HTTP call to your own bot.

```json
{
  "searches": ["chairs"],
  "maxPrice": 2000,
  "deltaMode": true,
  "skipFirstRun": true,
  "alertOnNew": true,
  "alertOnPriceDrop": true,
  "minPriceDropPct": 10
}
```

How it decides:

- **New** means this watchlist has never returned that listing ID before.
- **Price drop** means the price is at least `minPriceDropPct` below the price last seen. Because
  this actor tracks the CURRENT price (already net of any markdown), a listing that goes on sale is
  naturally a price drop from the second run on.
- **Chairish's own markdown, even on a first sighting.** A listing your watchlist has never seen
  before, but which Chairish already shows as marked down, is delivered as a price-drop alert too —
  even if "Alert on new listings" is off — so a buyer watching only for markdowns doesn't miss one
  just because it's also new to the watchlist.
- Unchanged listings are removed before you are billed. A run with nothing new returns one summary
  row with `monitorStatus: NO_NEW_ROWS`, `listingCount: 0`, and costs only the start fee.
- Each watchlist remembers up to 5,000 listings, oldest forgotten first.

**Limitations.** Chairish's collection pages expose no total-matches count anywhere in the static
HTML, so `totalAvailable` is always empty and `truncated` is a best-effort signal based on whether
the last page fetched was still full. A run only sees the newest `maxListingsPerSearch` listings; a
listing that drops in price after it slides out of that window is not seen again. This actor
defaults its proxy to **Residential** rather than Datacenter — Chairish answers Apify's shared
datacenter IP range with an HTTP 418 block, even though it serves a plain, un-challenged page to an
ordinary residential IP.

### Price

- **Listings:** $3 per 1,000 listings returned, plus a $0.00005 start fee per run.
- A monitoring run with nothing new costs the start fee only. A search that returns nothing is
  never billed.

### How to use

1. **In the Apify Console.** Open the actor page and click **Start** — the `searches` field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
2. **Via the API.** Call it directly with a POST request — no Console needed once you have an API token:
   ```bash
   curl "https://api.apify.com/v2/acts/accountable_eel~chairish-listing-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"searches":["chairs"]}'
   ```
3. **On a schedule.** Save this actor as an Apify **Task** with the input you want, then add a **Schedule** (hourly, daily, weekly) so it runs on its own — no server of your own required.

Tips:

- `includeKeywords`/`excludeKeywords` match the title and Chairish's own category path before
  billing, so a word you don't want never costs you a row.
- `minPrice`/`maxPrice` are applied after fetching — Chairish has no price-filter URL parameter
  this build could find, so these are honoured client-side rather than dropped silently.
- If your keyword doesn't match, paste the real `chairish.com/collection/...` link instead — the
  actor's error message tells you when this happens.

### Input

```json
{
  "searches": [
    "chairs"
  ]
}
```

One search per line — a keyword matching a Chairish category (e.g. "chairs", "wall mirrors", "chaise lounge") or a pasted chairish.com/collection/... link. Chairish's search page is off-limits by its own robots.txt, so a keyword must resolve to one of Chairish's own collections. No login required. Accepted formats: chairs, wall mirrors, https://www.chairish.com/collection/decor, https://www.chairish.com/collection/us-vintage.

### Sample output

| query | found | status | searchQuery | listingCount | truncated | newCount | priceDropCount | monitorStatus | listings | listingId | title | price | currency | category | isNewlyMade | condition | isDiscounted | discountPct | isPurchasable | imageUrl | sellerType | sellerHash | changeType | isNew | previousPrice | priceDropPct | firstSeenAt | url | scrapedAt |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| chairs | true | OK | <search> | <listings returned> | <more results were available> | <new listings this run> | <price drops this run> | \<monitoring status (quiet / seeded runs)> | \<all listings found (full list)> | <chairish listing id> | <title> | \<price (usd)> | <currency> | \<chairish's own category path> | \<newly made (vs. vintage/antique)> | \<new or vintage/antique, from chairish's own flag> | \<chairish's own markdown flag> | \<discount % (chairish's own)> | <currently purchasable> | <image> | \<always "dealer" — see readme> | <anonymous dealer hash> | \<new / price-drop / seen> | \<is this listing new?> | <previous price seen by this monitor> | \<price drop % (this monitor)> | <first seen on a run> | <listing link> | 1970-01-01T00:00:00.000Z |

A real row from a `chairs` run (2026-09-22), trimmed:

```json
{
  "searchQuery": "chairs",
  "listingCount": 5,
  "listingId": "35487167",
  "url": "https://www.chairish.com/product/35487167/antique-viennese-fan-chairs-in-walnut-veneer-with-lemon-band-inlays-austria-1830-set-of-6",
  "title": "Antique Viennese Fan Chairs in Walnut Veneer with Lemon Band Inlays, Austria, 1830, Set of 6",
  "price": 17971,
  "currency": "USD",
  "category": "Furniture/Seating/Dining Chairs",
  "isNewlyMade": false,
  "condition": "Vintage/Antique",
  "isDiscounted": false,
  "sellerType": "dealer",
  "sellerHash": "3f7c1a9e0d2b5648"
}
```

### Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

```bash
curl "https://api.apify.com/v2/acts/accountable_eel~chairish-listing-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"searches":["chairs"]}'
```

**n8n.** Add an HTTP Request node: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~chairish-listing-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body Content Type `JSON`, JSON Body `{"searches":["chairs"]}` (swap in an expression from an earlier node for a real value).

**Clay.** Add an "HTTP API" column: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~chairish-listing-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body `{"searches":["{{search}}"]}`, mapping the row's search into the `searches` array.

**MCP.** In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "Chairish Scraper: Vintage Furniture Price Alerts" — the agent will find and run this actor.

**Discord or Slack alerts without code.** Schedule the monitoring Task hourly, then add an
integration on the Task: in n8n or Make, trigger on "Apify: run succeeded", read the run's dataset,
skip rows where `listingCount` is 0, and post `title`, `price`, `category` and `url` to a Discord
or Slack webhook. Google Sheets users can append the same rows for a running price log.

### vs. alternatives

| | What it costs | What you get | Trade-off |
|---|---|---|---|
| **This actor** | $3 per 1,000 listings, quiet monitoring runs free | Category search mapped to Chairish's real collections, Chairish's own markdown flag treated as a price-drop signal, built-in new-listing and price-drop monitoring | No free-text search (robots.txt blocks it), no posting date or total-matches count |
| Checking Chairish by hand | Free, plus your time | Full control | Doesn't scale past a handful of searches and misses whatever appears between visits |

### Data & privacy

**Data & privacy.** This actor reads public listing pages that anyone can see without logging in.
It doesn't log in, solve CAPTCHAs or reveal hidden contact details. Chairish's own listing cards
expose no seller name, only an opaque dealer code; this actor hashes even that before it reaches
the output. Not affiliated with Chairish.

### FAQ

**Is this allowed?**
It collects the same public listing data your browser shows, for the categories you choose. It's
built for monitoring a category, not for copying the marketplace. Check that your use fits
Chairish's terms and your local law — note that this actor deliberately never uses Chairish's
`/search` page, which its own `robots.txt` disallows.

**Why does my keyword not work?**
Chairish's search page is off-limits by its own `robots.txt`, so this actor only reads its category
("collection") pages — and only the ones that are genuine, server-rendered listing grids. A keyword
must resolve to a real, working Chairish collection ("chairs", "wall mirrors", "chaise lounge",
"runner rugs", "bar stools"...). Many other real Chairish category names for common furniture nouns
(`sofas`, `tables`, `dining chairs`, most plain furniture nouns) are "hub" pages: real, valid links
whose product grid Chairish itself only fills in with client-side JavaScript, which this actor
doesn't run. Rather than silently return zero rows for those, this actor rejects them up front with
a clear message. For anything else, paste the real `chairish.com/collection/...` link — if it turns
out to be a hub page too, the run comes back with a clear "no listings" result rather than
fabricated data.

**Why is `condition` only "New" or "Vintage/Antique"?**
That's the only distinction Chairish's own listing cards expose — a binary newly-made flag, not a
graded condition (like "excellent"/"good"/"fair"). It's reported as-is, not padded out with a grade
Chairish doesn't provide.

**Why is `sellerType` always "dealer"?**
Every Chairish listing card sampled during this build carried a dealer code and none exposed a
private-seller flag — Chairish is a consignment-dealer marketplace, not a peer-to-peer one. The
field is kept (rather than removed) so the anonymous `sellerHash` still has a labeled counterpart.

**Why did my monitoring run return nothing?**
Nothing new or cheaper appeared since the last run. You get one row with `monitorStatus:
NO_NEW_ROWS` and are charged only the start fee. On the very first run with "Seed silently" on, the
status is `SEEDED`.

**What do I need to set up?**
Nothing. No Chairish account, no cookies, no proxy settings.

**Can an AI agent call this?**
Yes, through the Apify MCP server or the API call shown above. Ask for "Chairish Scraper: New
Listing & Price Drop Alerts".

### Related actors

- [Pamono Listing Lookup](https://apify.com/accountable_eel/pamono-listing-lookup): the same one
  row per listing shape for Pamono's EU/US design and vintage-furniture marketplace.
- [1stDibs Listing Lookup](https://apify.com/accountable_eel/1stdibs-listing-lookup): global luxury
  furniture and design, with 10-currency pricing and era tags.
- [Selency Listing Lookup](https://apify.com/accountable_eel/selency-listing-lookup): France's
  vintage furniture and decor marketplace, with material and style tags.
- [Vinted Listing Lookup](https://apify.com/accountable_eel/vinted-listing-lookup): new-listing and
  price-drop alerts across 22 Vinted second-hand fashion sites.

# Actor input Schema

## `searches` (type: `array`):

One search per line — a keyword matching a Chairish category (e.g. "chairs", "wall mirrors", "chaise lounge") or a pasted chairish.com/collection/... link. Chairish's search page is off-limits by its own robots.txt, so a keyword must resolve to one of Chairish's own collections. No login required. Accepted formats: chairs, wall mirrors, https://www.chairish.com/collection/decor, https://www.chairish.com/collection/us-vintage. You're only charged for the ones we actually find — a miss costs nothing.

## `testRun` (type: `boolean`):

Turn this on to test your input on a small sample before running the full list. Turn it off to process everything.

## `onlyFound` (type: `boolean`):

Only keep rows where something was actually found. Misses are always free, whether or not you show them here.

## `includeKeywords` (type: `array`):

Optional. Matched against the title and Chairish's own category path.

## `excludeKeywords` (type: `array`):

Optional. Matched against the title and Chairish's own category path.

## `maxResults` (type: `integer`):

Optional. Stop the run once this many results have been found — useful for a quick, cheap sample. Leave blank for no limit.

## `maxListingsPerSearch` (type: `integer`):

Chairish shows 48 listings per page and does not expose a total-matches count; this actor pages until it reaches your limit or runs out of pages. You pay per listing returned, so this is also your budget control.

## `minPrice` (type: `integer`):

Optional. Applied after fetching (Chairish has no price-filter URL parameter this build could find). Leave empty for no minimum.

## `maxPrice` (type: `integer`):

Optional. Applied after fetching. Leave empty for no maximum.

## `deltaMode` (type: `boolean`):

Turns this actor into a monitor. Chairish shows no posting date, so this is the only way to see what's new. A listing counts as new when its Chairish ID has not been returned by a previous run of the same watchlist, and as a price drop when its price falls since it was last seen — including a listing that just went on Chairish's own markdown sale. Already-seen, unchanged listings are dropped before you are billed, so a quiet run costs only the run fee. The first run has nothing to compare against, so (unless "Seed silently" is on) it returns everything and remembers it.

## `deltaName` (type: `string`):

Leave empty and we derive one from this run's search settings, so two schedules with different settings keep separate memories. Type your own name to keep one memory across a settings change, or to have two schedules share one.

## `alertOnNew` (type: `boolean`):

Include newly-seen listings when monitoring is on. Turn off to get price-drop alerts only.

## `alertOnPriceDrop` (type: `boolean`):

Include listings whose price dropped since this watchlist last saw them — including a listing seen for the first time already on Chairish's own markdown sale. Turn off to get new-listing alerts only.

## `minPriceDropPct` (type: `integer`):

A listing must drop by at least this percentage since it was last seen by THIS monitor to be reported as a price-drop.

## `skipFirstRun` (type: `boolean`):

Instead of returning every current listing as "new" the first time a watchlist runs, this banks them silently and starts alerting from the second run on.

## `columns` (type: `array`):

Choose which pieces of information to include in each result row. All are included by default.

## `expandRows` (type: `boolean`):

When on, each listing found gets its own row instead of being grouped under its search. You're still only charged once per search, no matter how many rows it produces.

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

Parallel requests. Keep conservative — this target has no browser fallback, so getting blocked costs more than slow-and-steady.

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

Apify Proxy config. Residential recommended for anti-bot-sensitive targets.

## Actor input object example

```json
{
  "searches": [
    "chairs"
  ],
  "testRun": false,
  "onlyFound": false,
  "includeKeywords": [],
  "excludeKeywords": [],
  "maxListingsPerSearch": 100,
  "deltaMode": false,
  "deltaName": "",
  "alertOnNew": true,
  "alertOnPriceDrop": true,
  "minPriceDropPct": 5,
  "skipFirstRun": false,
  "columns": [
    "searchQuery",
    "listingCount",
    "truncated",
    "newCount",
    "priceDropCount",
    "monitorStatus",
    "listings",
    "listingId",
    "title",
    "price",
    "currency",
    "category",
    "isNewlyMade",
    "condition",
    "isDiscounted",
    "discountPct",
    "isPurchasable",
    "imageUrl",
    "sellerType",
    "sellerHash",
    "changeType",
    "isNew",
    "previousPrice",
    "priceDropPct",
    "firstSeenAt",
    "url"
  ],
  "expandRows": true,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (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 = {
    "searches": [
        "chairs"
    ],
    "includeKeywords": [],
    "excludeKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("accountable_eel/chairish-listing-lookup").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 = {
    "searches": ["chairs"],
    "includeKeywords": [],
    "excludeKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("accountable_eel/chairish-listing-lookup").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 '{
  "searches": [
    "chairs"
  ],
  "includeKeywords": [],
  "excludeKeywords": []
}' |
apify call accountable_eel/chairish-listing-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,accountable_eel/chairish-listing-lookup"
        }
    }
}
```

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/JGYEtqMIfELQaIiEh/builds/Ddr0pdEWQs1DRp9CJ/openapi.json
