# Grailed Scraper - Listings, Prices, Sellers & Reviews (`abotapi/grailed-marketplace-scraper`) Actor

Scrape Grailed by keyword, category, designer, condition, size or URL. Extract prices and price history, descriptions, measurements, shipping, seller profiles and reviews. Includes change tracking to monitor new, updated and removed listings.

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

## Pricing

from $1.90 / 1,000 listing, seller or review records

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

## Grailed Marketplace Scraper

Scrape Grailed, the menswear and womenswear resale marketplace: search by
keyword, walk the store's own category, designer, condition and size facets,
watch sellers, or paste any store URL to walk it directly.

Every row carries the current **ask price** and the **ask history** the store
keeps (each prior asking price on the listing), plus the seller's score card
and the shipping bands. The listing description, the measurement table and
the photo set are one toggle away.

### What you get

| Field | Meaning |
|---|---|
| `recordId` / `rowType` | Stable id and the kind of row: `product`, `seller` or `review` |
| `title` / `designer` / `designers` | Listing name and designer(s) |
| `price` | The current ask price |
| `originalPrice` / `askHistory` | The oldest ask on the listing and every prior ask the store keeps |
| `size` / `conditionLabel` / `categoryPath` | Size, the store's own condition grade, and the category path |
| `sellerRating` / `sellerRatingCount` / `sellerSales` | The seller's score card, as the listing card carries it |
| `coverPhoto` / `shippingUs` | Cover image and the US shipping band |
| `description` / `measurements` / `photos` | Listing page fields (with Fetch listing details on) |
| `changeType` / `changedFields` / `firstSeenAt` / `lastSeenAt` | Incremental-mode change tracking fields |

Seller rows add the full profile (rating breakdown, badges, location, member
since). Review rows carry the rating, the note, the tags and the listing the
review refers to.

### How to use it

**Search mode** (default): give it search terms and it walks the store's own
search feed. Combine with the department, category, designer, condition,
size and price facets; every one of them narrows the feed the store itself
serves, they are not applied after the fact.

**Sellers**: name usernames (or paste profile URLs) to read seller profiles.
Two toggles add the seller's reviews as their own rows, and a walk of the
seller's on-sale listings as normal product rows.

**URL mode**: paste any store URL. A listing URL returns that single listing
in full. A category or designer URL is walked page by page, and the pasted
URL decides its own scope: the price and ordering filters still narrow what
the run keeps, while the catalogue facets are search mode only.

#### Notes on scope

- Sold listings are excluded from the store's searchable catalogue, so there
  is no sold-comparison surface to read; rows still carry the sold fields for
  when the store populates them. The ask history on each row is the price
  context the store does expose.
- The store's own feed serves at most 25 pages of 40 rows per scope, the
  same ceiling its own website lives with. Reaching deeper into a large
  catalogue means narrowing the scope with facets.
- A category or designer name the store does not serve never widens the
  scope: an unknown category stops the run with a clear message, and an
  unknown designer simply matches nothing (reported as an empty search).
- A price cut IS a change: with Incremental mode on, a moved ask price
  reports the row as UPDATED with `price` in `changedFields`.

### Output

Dataset rows are flat JSON. Example (illustrative placeholder, not a real
listing):

```json
{
  "recordId": "00000001",
  "rowType": "product",
  "listingId": "00000001",
  "title": "Sample Designer Sample Wool Sweater",
  "designer": "Sample Designer",
  "price": 210.0,
  "originalPrice": 260.0,
  "askHistory": [260.0, 235.0],
  "size": "m",
  "condition": "is_gently_used",
  "conditionLabel": "Gently Used",
  "categoryPath": "tops.sweaters_knitwear",
  "department": "menswear",
  "sold": false,
  "sellerUsername": "sample_seller_00",
  "sellerRating": 4.9,
  "sellerRatingCount": 128,
  "sellerSales": 340,
  "coverPhoto": "https://example.com/images/sample-sweater.jpg",
  "shippingUs": 12.0,
  "url": "https://www.example.com/listings/00000001",
  "scrapedAt": "2026-01-01T00:00:00Z"
}
```

With **Incremental mode** on, rows also carry `changeType` (NEW, UPDATED,
UNCHANGED, REAPPEARED, EXPIRED), `changedFields`, `firstSeenAt` and
`lastSeenAt`. **Resume from a previous run** continues one interrupted crawl
without returning or billing the records it already returned.

### Limits and costs

- `maxItems` is the single cap on a run, defaulting to 20; set 0 for
  unlimited. `maxPages` is an optional safety bound per source.
- **Fetch listing details** adds the description, the measurement table, the
  photo set and the deeper seller fields per listing. It is a paid extra
  (the Listing details event on the pricing tab); the ask price, the ask
  history and the score card are on every row either way.
- **Fetch seller reviews** emits each review as its own dataset row, billed
  as one.
- **Emit unchanged** and **Emit expired** return, and bill, extra rows;
  leave them off unless you want full snapshots or expiry tracking.

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

Results can be piped into the apps you already use through Model Context
Protocol (MCP) connectors, as an optional side channel. This never changes
the dataset output.

- `mcpConnectors`: authorize a connector under Apify, Settings, API &
  Integrations, then select it here. Notion receives a page per record;
  other connectors receive a best-effort write or digest. Each connector
  gets a condensed summary per record; the complete record always stays in
  the dataset.
- `notionParentPageUrl`: the Notion page under which record pages are
  created. Required for the Notion connector, ignored by the others.
- `maxNotifyListings`: cap on records written to each connector per run.

### Compute

This actor reads pages over plain HTTP; 512 MB of memory is the default and
is plenty. A minimal run finishes in well under a minute.

# Actor input Schema

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

How to start the scrape. 'search' runs store searches. 'url' processes the store URLs you paste: a listing URL is fetched as a single listing in full, a category or designer URL is paged like a search, and a seller profile URL reads the seller. The price and ordering filters below apply in BOTH modes; search terms, sellers and the catalogue facets (categories, designers, conditions, sizes) apply in search mode only.

## `searchTerms` (type: `array`):

One or more things to search the marketplace for, for example 'varsity jacket', 'cargo pants', 'band tee'. Each term is scraped as its own scope with the catalogue facets below applied. Search mode ONLY: in URL mode the pasted URLs decide their own scope instead.

## `sellerUsernames` (type: `array`):

One or more Grailed usernames to read as sellers. Each produces a seller profile row; turn on the two toggles below to also read the seller's reviews or their on-sale listings. Search mode ONLY: in URL mode, paste seller profile URLs instead.

## `fetchSellerReviews` (type: `boolean`):

Read each seller's reviews and emit them as their own rows (rowType 'review'), with the rating, the note, the tags and the listing the review refers to. Each review is a dataset row of its own and is billed as one. Search mode ONLY: it applies to the sellers named above, not to listings found by search or browse.

## `includeSellerListings` (type: `boolean`):

After the profile row, walk the seller's on-sale listings through the store's own feed, as normal product rows (the Max items cap applies to them like any other row). Search mode ONLY: it applies to the sellers named above.

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

Only browse these categories. Accept a whole section ('tops', 'footwear'), a subcategory name from a category page URL ('long-sleeve-t-shirts'), or the store's own feed path ('tops.short\_sleeve\_shirts'). A name the store does not serve stops the run with a clear message rather than walking a wider scope. Search mode ONLY: in URL mode the pasted URL decides its own scope.

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

Only browse these designers, by the name the store itself uses on designer pages and on the rows, for example 'Rick Owens' or 'Our Legacy'. Each name narrows the feed; a name the store does not serve simply matches nothing. Search mode ONLY: in URL mode the pasted URL decides its own scope.

## `conditions` (type: `array`):

Only browse listings in these conditions, as the store grades them: 'new', 'gently used', 'used' or 'worn'. Search mode ONLY: in URL mode the pasted URL decides its own scope.

## `sizes` (type: `array`):

Only browse listings in these sizes, exactly as the store labels them: letter sizes ('s', 'm', 'l', 'xl'), waist sizes ('30', '32'), or 'one size'. Search mode ONLY: in URL mode the pasted URL decides its own scope.

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

Paste store listing URLs (https://www.grailed.com/listings/12345678), search page URLs (https://www.grailed.com/shop/search?query=jacket), category URLs (https://www.grailed.com/categories/footwear or https://www.grailed.com/categories/womenswear/tops), designer URLs (https://www.grailed.com/designers/designer-name) or seller profile URLs (https://www.grailed.com/username). Add ?page=3 to any browse URL to start walking from that page. A pasted URL decides its own scope: the price and ordering filters below still narrow browse scopes, while the catalogue facets (categories, designers, conditions, sizes) are search mode only. A pasted listing URL returns that single listing in full.

## `department` (type: `string`):

Which storefront department to read. Applies in BOTH search mode and URL mode: search mode sends it with every request, and in URL mode it fills in only when a pasted URL does not name a department of its own.

## `minPriceUsd` (type: `integer`):

Only keep listings whose current ask price is at least this many US dollars. The store applies this at the feed itself, so a filtered run reads a narrowed feed. Applies in BOTH search mode and URL mode (a pasted single-listing URL always returns its listing in full).

## `maxPriceUsd` (type: `integer`):

Only keep listings whose current ask price is at most this many US dollars. The store applies this at the feed itself, so a filtered run reads a narrowed feed. Applies in BOTH search mode and URL mode (a pasted single-listing URL always returns its listing in full).

## `sortResultsBy` (type: `string`):

Arranges the rows this run collected, after they are read. The store serves one ranking of its own; the alternatives are a local tidy up of one run's output, not a catalogue wide ordering. Applies in BOTH search mode and URL mode.

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

Fetch each listing's own page for the description, the measurement table, the photo set and the deeper seller fields. Slower, and charged once per record. Leave off for a fast crawl: the ask price, the ask history, the condition, the size and the seller's score card are included either way.

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

The single cap on this run: stop after collecting this many records (listings, sellers and reviews together) across all search terms, sellers and URLs. Set 0 for unlimited.

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

Safety bound on how many feed pages to walk per search term, seller or URL. Leave empty (0) for no page limit: the run then stops at Max items, or at the store's own per-scope page bound, not here. Does not impose a separate cap below Max items.

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

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

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

Turn this on for daily or weekly recurring monitoring of the same search, for example a price watch. The first run returns everything matching as NEW. Later runs normally return only NEW, UPDATED and REAPPEARED records. Ask-price moves ARE reported as UPDATED: on a resale marketplace they are the signal, not noise. Turn on Emit unchanged or Emit expired only when you also want those rows returned (and billed). State is kept separately per search, URL, seller 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 let the actor derive a key automatically from the search terms, URLs, sellers and filters, so two different searches never mix state.

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

Off by default. Turn on to also return records 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 specifically want a full snapshot every run.

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

Off by default. Turn on to also return records 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: not when Max items capped it, not when Resume was used, and not when the run was refused. This returns, and bills, extra synthetic rows, so leave it off unless you need expiry tracking.

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

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

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

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

Leave the default for the cheapest run. Change it only if a run reports that it could not read any results. Prices are quoted in US dollars on every row regardless of the exit country.

## Actor input object example

```json
{
  "mode": "search",
  "searchTerms": [
    "varsity jacket"
  ],
  "fetchSellerReviews": false,
  "includeSellerListings": false,
  "categories": [
    "outerwear"
  ],
  "urls": [
    "https://www.grailed.com/categories/footwear"
  ],
  "department": "menswear",
  "sortResultsBy": "site_order",
  "fetchDetails": false,
  "maxItems": 20,
  "maxPages": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50,
  "proxy": {
    "useApifyProxy": true
  }
}
```

# 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",
    "searchTerms": [
        "varsity jacket"
    ],
    "fetchSellerReviews": false,
    "includeSellerListings": false,
    "categories": [
        "outerwear"
    ],
    "urls": [
        "https://www.grailed.com/categories/footwear"
    ],
    "department": "menswear",
    "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/grailed-marketplace-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",
    "searchTerms": ["varsity jacket"],
    "fetchSellerReviews": False,
    "includeSellerListings": False,
    "categories": ["outerwear"],
    "urls": ["https://www.grailed.com/categories/footwear"],
    "department": "menswear",
    "fetchDetails": False,
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/grailed-marketplace-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",
  "searchTerms": [
    "varsity jacket"
  ],
  "fetchSellerReviews": false,
  "includeSellerListings": false,
  "categories": [
    "outerwear"
  ],
  "urls": [
    "https://www.grailed.com/categories/footwear"
  ],
  "department": "menswear",
  "fetchDetails": false,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  }
}' |
apify call abotapi/grailed-marketplace-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/grailed-marketplace-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/1xOQUoW4AwibPObvX/builds/e8eXP7Pn7bk0mBeS0/openapi.json
