# The RealReal Scraper - Luxury Prices, Brands & Listings (`abotapi/therealreal-scraper`) Actor

Scrape luxury consignment listings from therealreal.com. Browse by designer, category or keyword and get designer, item class, condition grade, retail price versus current list price, measurements, materials and authentication notes. Built for price positioning per brand per category.

- **URL**: https://apify.com/abotapi/therealreal-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce
- **Stats:** 1 total users, 1 monthly users, 80.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 item 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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## The RealReal Luxury Consignment Scraper

Pull the live resale catalogue of The RealReal, the largest US marketplace for
authenticated luxury consignment, as clean tabular data.

Every row is one consigned item: a single physical object, listed once, with
the designer, the item class, the marketplace's own condition grade, what it
originally retailed for, and what it is listed at today.

### What makes this useful

The RealReal is the closest thing the resale market has to a public price
book. Because each listing is a unique physical item rather than a stocked
product, the catalogue is a running record of what specific pieces actually
get listed at, in what condition, by which designer.

This Actor is built around that. Every row carries both prices and both
derived percentages:

| Field | Meaning |
|---|---|
| `retailPrice` | what the item cost new |
| `listPrice` | what it is consigned for now |
| `originalPrice` | the consignment price before any markdown |
| `vsRetailPct` | how far the list price sits below retail |
| `discountPct` | how far the list price sits below the was-price |

So a pull for one designer, in one category, is a price-positioning table for
that brand with no further work. That is the intended use: brand and
merchandising teams tracking where their pieces land on the resale market,
resellers pricing inventory, and appraisers checking comparables.

### Two ways to browse

Fill in either or both; each entry is walked as its own catalogue.

- **Designers** the analytics axis. Give a list of designer names and each one
  is walked as its own catalogue. Pair it with **Category for the designers
  above** to compare like with like, for example every Chanel handbag.
- **Category paths** a whole department, for example `women/handbags`,
  `men/shoes`, `jewelry` or `watches`.

Or switch to **url** mode and paste category, designer or item links directly.

#### Why there is no keyword box

This marketplace does not narrow its catalogue pages by keyword. A keyword is
accepted and then ignored, and the page comes back with the entire catalogue,
about 3 million items, whose first results have nothing to do with what was
asked for. A keyword box would therefore look like it worked while returning
the wrong thing, so this Actor does not offer one and skips a pasted keyword
link with an explanation rather than answering it. Browse by category or
designer instead, and use the filters below to narrow.

### Condition grades

The marketplace grades every item on its own five-step scale, and the grade is
returned verbatim in the `condition` field:

| Grade | Meaning |
|---|---|
| Pristine | Original condition, may include tags, dustbag or box |
| Excellent | Tried on or lightly worn, in great condition |
| Very Good | Minor signs of wear |
| Good | Visible wear |
| Fair | Heavy wear |

Set the **Condition grade** filter to keep only one grade. When an item page is
read, the inspector's own free-text note (for example "minor residue at flap")
is returned separately as `conditionNote`.

### Output fields

Returned on every row, from the result pages, at no extra cost:

`itemId`, `sku`, `variantId`, `name`, `url`, `designer`, `designerId`,
`itemClass`, `category`, `condition`, `conditionNote`, `color`, `gender`,
`primaryMaterial`, `attributes`, `retailPrice`, `listPrice`, `originalPrice`,
`currency`, `discountPct`, `vsRetailPct`, `isOnSale`, `availability`, `isSold`,
`quantity`, `obsessionCount`, `isEditorsPick`, `badge`, `images`,
`imageCount`, `sourceUrl`, `scrapedAt`

Added when **Read each item's page** is on:

`measurements` (height, width, strap drop and so on), `materials`,
`description`, `authenticationNote`, `sustainability`, `returnPolicy`,
`disclaimers`

Added when **Incremental changes for scheduled runs** is on:

`changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt`

### Cost

Three charge events:

- **Actor start**, once per run.
- **Item result**, one per item returned. This is the main cost.
- **Item page detail**, one per item whose own page was read. Charged ONLY
  when **Read each item's page** is on, and only for items that actually
  reached the dataset. An item whose page could not be read is still returned
  with its result-page fields and is NOT charged this event.

Leaving the detail toggle off is the cheap mode, and it still returns the
designer, condition grade, both prices and both percentages. Turn it on when
you need measurements or the full item copy.

### Recurring monitoring

Turn on **Incremental changes for scheduled runs** and the Actor remembers the
previous run of the same search by itself. Later runs return only what
changed, marked `NEW`, `UPDATED` or `REAPPEARED`.

On a consignment marketplace this is unusually informative, because each item
is unique:

- an `UPDATED` row with `listPrice` in `changedFields` is a markdown
- an `EXPIRED` row usually means the item sold

Turn on **Emit expired items** to get those. Expiry rows are produced only
after a run has completely scanned the tracked search, so a run that was
capped, resumed, or stopped at the marketplace's own per-search depth limit
never reports an item as gone when it simply was not reached.

### Depth limit, and how to work around it

The marketplace serves at most **2000 items per search**, however many match.
A category like women's handbags reports hundreds of thousands of matches and
still stops at 2000.

The Actor tells you in the log when a search hits that limit. To collect more,
split the search:

- browse **one designer at a time** rather than a whole category
- narrow to a **single category** with the designer category field
- walk the catalogue in **price bands** using the minimum and maximum price
  fields, for example 0 to 500, then 500 to 1000, and so on

Each of those is a separate search with its own 2000-item window.

### Ordering

Results come back newest first. The marketplace applies its other orderings in
the browser rather than on the page this Actor reads, so no sort option is
offered here rather than offering one that would quietly do nothing.

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

Optionally pipe results into the apps you already use, over Model Context
Protocol. Authorize a connector under Apify, Settings, API & Integrations, then
pick it in **mcpConnectors**. Notion gets a page per item, so it also needs
**notionParentPageUrl**; other connectors get a best-effort write or a digest.
**maxNotifyListings** caps how many items each connector receives per run.

Each connector receives a condensed summary per item, never the full record.
The complete record always stays in the dataset, and leaving this empty changes
nothing about the output.

### Sample output

Values are placeholders, not a real listing.

```json
{
  "itemId": "00000000",
  "sku": "EXM000000",
  "name": "Example Leather Shoulder Bag",
  "url": "https://www.therealreal.com/products/women/handbags/shoulder-bags/example-item-aaaaa",
  "designer": "Example Maison",
  "designerId": "000",
  "itemClass": ["Women", "Handbags", "Shoulder Bags"],
  "category": "Shoulder Bags",
  "condition": "Excellent",
  "conditionNote": "No visible signs of wear.",
  "color": "Black",
  "gender": "Women",
  "primaryMaterial": "Leather",
  "retailPrice": 3200.0,
  "listPrice": 1450.0,
  "originalPrice": 1700.0,
  "currency": "USD",
  "discountPct": 14.7,
  "vsRetailPct": 54.7,
  "isOnSale": true,
  "availability": "AVAILABLE",
  "isSold": false,
  "obsessionCount": 0,
  "measurements": { "Height": "8\"", "Width": "11\"", "Shoulder Strap Drop": "20\"" },
  "materials": ["Example Maison Shoulder Bag", "Black Leather", "Gold-Tone Hardware"],
  "images": ["https://product-images.therealreal.com/EXM000000_1_enlarged.jpg"],
  "detailFetched": true,
  "scrapedAt": "2026-09-11T00:00:00Z"
}
```

### Notes

- Prices are in US dollars.
- Sold items are returned by default, because a sold item and its final price
  are the most useful comparable there is. Turn on **Only items still
  available** to exclude them.
- This Actor collects no personal data. Consignors are not identified anywhere
  in the marketplace's listing pages, and no buyer reviews exist on
  consignment listings, so there is no reviewer identity to collect either.
- Item pages are slower to read than result pages, so keep **Max items** tight
  when the detail toggle is on.

# Actor input Schema

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

Choose 'search' to browse by category or designer, or 'url' to scrape pasted The RealReal links. Each mode reads only its own fields below.

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

Only used when mode = search, ignored in URL mode. Category paths as they appear in a The RealReal URL after /shop/, for example 'women/handbags', 'men/shoes', 'jewelry' or 'watches'. Each is walked as its own catalogue.

## `designers` (type: `array`):

Only used when mode = search. Designer names, for example 'Chanel', 'Louis Vuitton' or 'Bottega Veneta'. Each designer is walked as its own catalogue, which is what makes a per-brand price-positioning pull a single run. Names are turned into the designer's own catalogue page, so ordinary spelling works.

## `designerCategory` (type: `string`):

Optional, and only used alongside Designers. Narrows every designer catalogue to one category path, for example 'women/handbags' or 'men/shoes'. The marketplace does not serve a designer's whole catalogue on one page, so when this is empty each designer is walked under 'women'.

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

Only used when mode = url, ignored in search mode. Item links under /products/, category links under /shop/ and designer links under /designers/ are all accepted. A designer link with no category is routed to that designer's catalogue, because the marketplace does not serve the bare designer landing page to this Actor. A keyword link is skipped and says why.

## `designer` (type: `string`):

Optional. Keep only items by this designer, for example 'Chanel', 'Hermes' or 'Louis Vuitton'. To pull several designers as separate catalogues, use the Designers list above instead.

## `condition` (type: `string`):

Optional. Keep only items at this condition grade. The marketplace grades every consigned item on its own five-step scale, from Pristine (original condition, may include tags and box) down to Fair (heavily worn).

## `color` (type: `string`):

Optional. Keep only items in this colour, as the marketplace names it, for example 'Black', 'Brown', 'Blue' or 'Gold'.

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

Optional. Only keep items whose current list price is at or above this amount, in US dollars. Also sent to the marketplace as a filter, so it narrows the walk itself rather than only the output. Useful for walking a large catalogue in price bands.

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

Optional. Only keep items whose current list price is at or below this amount, in US dollars. Also sent to the marketplace as a filter.

## `availableOnly` (type: `boolean`):

Optional. Keep only items that can be bought right now. Sold items are returned by default, because on a consignment marketplace a sold item and its final price are the most useful data points for pricing analysis.

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

Optional. Keep only items whose current list price is below their earlier consignment price.

## `vintageOnly` (type: `boolean`):

Optional. Keep only items the marketplace flags as vintage.

## `editorsPickOnly` (type: `boolean`):

Optional. Keep only items the marketplace's own editors have highlighted.

## `withTagsOnly` (type: `boolean`):

Optional. Keep only items still carrying their original retail tags, which usually sit at the top of the condition scale.

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

Read every item's own page for its measurements (height, width, strap drop and so on), full description and materials, item-class hierarchy, authentication note, return policy and carbon and water figures. Off by default because it costs one extra page per item. A pasted item link always reads the item page, because there is no result card behind it, so an item link is always billed one 'Item page detail' event whatever this toggle says.

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

Maximum number of items to return across the whole run. This is the run's cap. Use 0 for unlimited.

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

Maximum number of result pages to take per keyword, category, designer or link. Each page carries about 120 items. 0 = unlimited; the run then stops only at Max items, at the marketplace's own per-search depth limit, or when there are no more results.

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

Paste a previous run ID or dataset ID to continue a large catalogue pull without returning or charging for items 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 item as NEW. Later runs normally return only NEW, UPDATED and REAPPEARED items, which makes it a cheap way to watch markdowns and new consignments. 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 items 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 items that were present in a previous run but are no longer listed, marked EXPIRED. On a consignment marketplace that usually means the item sold, which makes it the most useful signal here. Only produced once a run has fully scanned the tracked search, never when Max items capped it, when Resume was used, or when the run hit the marketplace's per-search depth limit. This returns, and bills, extra rows.

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

Connection used for every request. Leave the default; the run switches connections on its own when the marketplace refuses a page.

## `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 item; other connectors get a best-effort write or digest. Each connector receives a condensed summary per item, 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 item pages are created. Required to enable the Notion export; ignored by other connectors.

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

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

## Actor input object example

```json
{
  "mode": "search",
  "categories": [
    "women/handbags"
  ],
  "designers": [],
  "urls": [
    "https://www.therealreal.com/shop/women/handbags",
    "https://www.therealreal.com/designers/chanel/women/handbags"
  ],
  "condition": "",
  "availableOnly": false,
  "onSaleOnly": false,
  "vintageOnly": false,
  "editorsPickOnly": false,
  "withTagsOnly": false,
  "fetchDetails": false,
  "maxItems": 20,
  "maxPages": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (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",
    "categories": [
        "women/handbags"
    ],
    "designers": [],
    "urls": [
        "https://www.therealreal.com/shop/women/handbags",
        "https://www.therealreal.com/designers/chanel/women/handbags"
    ],
    "availableOnly": false,
    "onSaleOnly": false,
    "vintageOnly": false,
    "editorsPickOnly": false,
    "withTagsOnly": false,
    "fetchDetails": false,
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/therealreal-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",
    "categories": ["women/handbags"],
    "designers": [],
    "urls": [
        "https://www.therealreal.com/shop/women/handbags",
        "https://www.therealreal.com/designers/chanel/women/handbags",
    ],
    "availableOnly": False,
    "onSaleOnly": False,
    "vintageOnly": False,
    "editorsPickOnly": False,
    "withTagsOnly": False,
    "fetchDetails": False,
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/therealreal-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",
  "categories": [
    "women/handbags"
  ],
  "designers": [],
  "urls": [
    "https://www.therealreal.com/shop/women/handbags",
    "https://www.therealreal.com/designers/chanel/women/handbags"
  ],
  "availableOnly": false,
  "onSaleOnly": false,
  "vintageOnly": false,
  "editorsPickOnly": false,
  "withTagsOnly": false,
  "fetchDetails": false,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  }
}' |
apify call abotapi/therealreal-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/therealreal-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/0oGvEM1cXCSNnqSYM/builds/t91FFMOOEhEKhI9bU/openapi.json
