# 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)
- **Features:** Limited permissions
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 1 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?

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

## Grailed Scraper: Listings, Ask Prices & Seller Profiles

Grailed Scraper turns Grailed, the menswear and streetwear resale marketplace, into a clean dataset. Get listings with the current ask price and the store's own ask history (every prior asking price on a listing), seller profiles with their rating average, rating count and rating tags, and seller reviews. Search by keyword, walk the store's own department, category, designer, condition and size facets, watch specific sellers, or paste any store URL, then export to JSON, CSV or Excel, or pull the results straight into your app through the API.

### Why This Scraper?

- **Four ways to find listings.** Search by keyword, walk any combination of the store's own facets (department, category, designer, condition, size, price), watch named sellers, or paste store URLs directly.
- **Ask price and ask history on every row.** Each listing carries the store's own record of every prior asking price, so a price cut is visible, not just the current number.
- **Listing detail on demand.** Switch on Fetch listing details for the description, measurement table, photo set and deeper seller fields, only when you need them, billed only then.
- **Sellers, reviews and closets.** Read a seller's profile, pull their reviews as their own rows, or walk their whole on-sale closet like a normal search.
- **Built for monitoring.** Incremental mode returns only new, updated and reappeared listings on repeat runs, treats an ask price move as a real change (not noise), and a refused run fails loudly instead of returning an empty dataset.
- **URL mode for direct control.** Paste a single listing, a category, a designer, a search page or a seller profile URL and it is read exactly as scoped, with page and price filters still applying.
- **Resume support.** Continue an interrupted crawl from a previous run without returning or paying for records it already collected.

### Use Cases

- **Resale price research:** track ask price moves and the store's own price-drop history across designers and categories to spot markdowns and value.
- **Sourcing and reselling:** monitor specific categories, designers or sizes for new listings the moment they post.
- **Seller and reputation research:** pull a seller's profile, rating average, rating tags and reviews before buying or partnering.
- **Marketplace analytics:** build historical datasets of listings, prices and conditions for trend and demand analysis.
- **Personal shopping alerts:** schedule Incremental mode against a saved search and get only what is new or changed.

### Data You Get

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

| Field | Example |
| --- | --- |
| `recordId` / `rowType` | `"00000001"` / `"product"` (also `"seller"`, `"review"`) |
| `listingId` | `"00000001"` |
| `title` / `designer` | `"Sample Wool Sweater"` / `"Sample Designer"` |
| `price` | `210.0` |
| `originalPrice` / `askHistory` | `260.0` / `[260.0, 235.0, 210.0]` |
| `size` / `condition` / `conditionLabel` | `"m"` / `"is_gently_used"` / `"Gently Used"` |
| `categoryPath` / `department` | `"tops.sweaters_knitwear"` / `"menswear"` |
| `shipping` / `heat` / `measurementCount` | `{"us": {"amount": 12.99, "enabled": true}, "ca": {...}, "uk": {...}, "eu": {...}, "asia": {...}, "au": {...}, "other": {...}}` / `4698.4` / `3` |
| `sellerAvatarUrl` | the seller avatar image address |
| `prettySize` / `wasDigitallyAuthenticated` | with Fetch details on: `"M"` / `false` |
| `sold` / `soldAt` / `soldPrice` | `false` / `null` / `null` |
| `sellerUsername` / `sellerRating` / `sellerRatingCount` / `sellerSales` | `"sample_seller_00"` / `4.9` / `32` / `64` |
| `coverPhoto` / `shippingUs` | cover image URL / `12.0` |
| `buyNow` / `makeOffer` | `true` / `true` |
| `styles` / `badges` | `["luxury"]` / `[]` |
| `url` | link to the listing, seller or category page |
| `description` / `measurements` / `photos` | full text, the measurement table, photo URLs (with Fetch listing details on) |
| `sellerSince` / `sellerLocation` / `sellerListingsForSale` | seller-page fields (with Fetch listing details on) |
| `changeType` / `changedFields` / `firstSeenAt` / `lastSeenAt` | Incremental mode change tracking fields |
| `scrapedAt` | `"2026-01-01T00:00:00Z"` |

Seller rows carry the full profile instead (rating average, count and tags, trusted/verified/quick-responder/speedy-shipper flags, location, member since, purchase count). Review rows carry the rating, the note, the tags and the listing the review refers to.

### How to Use

1. Pick a **mode**: `search` (by keyword and facets) or `url` (paste Grailed links).
2. In search mode, add search terms and narrow with the department, category, designer, condition, size and price facets, or name sellers to read their profiles. In URL mode, paste listing, category, designer, seller or search URLs instead.
3. Turn on **Fetch listing details** if you want the description, measurement table and photos on every row.
4. Set **Max items** to control run size and cost, then click **Start**.

**Search by keyword and facets:**

```json
{
  "mode": "search",
  "searchTerms": ["varsity jacket"],
  "categories": ["outerwear"],
  "conditions": ["gently used"],
  "maxItems": 50
}
```

**Read a seller with their reviews and closet:**

```json
{
  "mode": "search",
  "searchTerms": [],
  "sellerUsernames": ["sample_seller_00"],
  "fetchSellerReviews": true,
  "includeSellerListings": true,
  "maxItems": 50
}
```

Supported URL shapes:

| Page | Example |
|---|---|
| Search | `/shop/search?query=jacket` (also `&page=2`; `&sort=price_asc`, `price_desc` or `newest` sets the row ordering) |
| Category section, subcategory, all | `/categories/footwear`, `/categories/outerwear`, `/categories/all` (also `?page=2`) |
| Womenswear category | `/categories/womenswear/tops` |
| Designer | `/designers/rick-owens` |
| Designer plus category | `/designers/rick-owens/footwear` |
| Seller profile and seller feedback | `/username`, `/username/feedback` |
| Single listing | `/listings/12345678` |

Browse collection (`/browse/...`), shop curation (`/shop/staff-picks`) and collections index pages are not feed scopes: each is skipped with a warning in the log, never silently read as zero rows.

Left untouched, the URL list runs one example of each supported page type (search with a sort, categories, womenswear category, designer, designer plus category, seller, seller feedback), so a bare URL-mode run scrapes a small sample of each rather than nothing. Replace it with your own links.

**Paste store URLs:**

```json
{
  "mode": "url",
  "urls": [
    "https://www.grailed.com/categories/footwear",
    "https://www.grailed.com/listings/00000001"
  ],
  "maxItems": 40
}
```

**Price watch with Incremental mode:**

```json
{
  "mode": "search",
  "searchTerms": ["Rick Owens jacket"],
  "minPriceUsd": 100,
  "maxPriceUsd": 800,
  "incrementalMode": true,
  "stateKey": "rick-owens-jacket-watch"
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/grailed-marketplace-scraper").call(run_input={"mode": "search", "searchTerms": ["varsity jacket"], "maxItems": 50})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["title"], row["price"], row["sellerUsername"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/grailed-marketplace-scraper').call({ mode: 'search', searchTerms: ['varsity jacket'], 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.

#### How the page bound and Max items shape a run

The store's own listing feed serves at most 25 pages of 40 rows per scope (1000 rows), the same ceiling its own website lives with; **Max pages** is an optional extra safety bound per source, off by default. Reaching deeper into a large catalogue means narrowing the scope with more facets, not raising the page bound. **Max items** is the single cap shared across every search term, seller and URL in one run; set it to `0` for unlimited. Each source gets an even share of it (Max items divided by the number of sources, rounded up), and a source that runs out of results early does not pass its unused share on, so a run whose first term has only a handful of matches can end below Max items.

Max pages counts the store's own page numbers from page 1, not pages walked from where a pasted URL starts. A URL ending in `?page=3` with Max pages set to `2` reads nothing and reports that nothing matched; leave Max pages at `0` or set it above the start page.

In Incremental mode, each source's share counts listings **scanned**, not listings returned: unchanged listings are suppressed but still use up the share. The store serves listings in its own ranking, not newest first, so a scheduled run with a small Max items looks at the same top slice every time and never reaches a new or repriced listing ranked below it. For monitoring, set Max items well above the number of listings your search matches, or `0`.

That sharing has one sharp edge worth knowing: when Max items caps a run mid-page, every row the actor looked at on that page counts as collected, including the rows the cap left no room to keep. If a run combines several overlapping search terms or seller scopes and the first one gets capped on a page that a later term would also have matched, the later term will not offer those rows again, since they already look handled. For example, running the search terms `"coat"` and `"jacket"` together with a small Max items can return fewer rows than Max items even though both terms still have unseen matches, because `"coat"`'s capped page silently claimed listings `"jacket"` would also have returned. Workaround: give a broad or overlapping search term its own run (or its own generous Max items), or raise Max items enough that a shared page clears in one pass before the run moves to its next term or seller.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset ID and the actor skips every record it already collected there, so you don't pay twice.
- **Incremental mode** (`incrementalMode`) is for scheduled runs over the same search, sellers or URLs. Each row is classified `NEW`, `UPDATED` (with `changedFields`; an ask price move is reported as UPDATED, since on a resale marketplace it is the signal, not noise), `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 is compared:** every field of the row (ask price, original price, price history, sold state and sold price, title, description, size, condition, category, designers, badges, seller details). **What is excluded:** only the scrape time by default; every other field, including photo links, the seller's follower count and the bump time, is compared. Fields you name in `ignoreFieldsForChanges` stay in the row but never trigger `UPDATED` and never appear in `changedFields`.
  - `ignoreFieldsForChanges` adds more fields to ignore. A dotted name such as `shipping.other` ignores that key inside the nested block (or inside every element of a list) while the rest of it stays compared. Plain names include `followerCount`, `bumpedAt`, `heat` and `coverPhoto`.
  - `changeScope` set to `priceAndStock` limits `UPDATED` to a change of the ask price, original price, price history or sold state (sold flag, date, price); edits to anything else never create a row. Seller and review rows are always compared in full. Default is `all`.
  - `compactUpdates` makes `UPDATED` and `EXPIRED` rows short (ids, url, title, current price and sold state, plus `priceChanges` with the old and new value); `NEW` rows stay complete. The listing details event is not charged for a compact row, since it no longer carries the details.
  - `expireAfterMissedScans` (default `1`) marks a listing `EXPIRED` only after it was missing from that many full scans in a row. `EXPIRED` means the listing is no longer found in the search, it cannot tell a sold or removed listing from one that only dropped out of the results; use the sold fields for sale status. A run that started at a later results page (`?page=N`), was capped (including a per-seller review limit), or was refused never marks anything `EXPIRED`.
  - All of these are off by default, so existing output is unchanged. Changing `changeScope` or `ignoreFieldsForChanges` keeps the saved baseline: the next run compares against it under the new rule and does not bill the tracked listings as new.

### 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 record, 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 record pages 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` | How to start the scrape: `search` runs store searches; `url` processes the pasted store URLs (the search-mode facets do not apply there). |
| `searchTerms` | array | `["varsity jacket"]` | Search mode only. Each term is walked as its own scope with the facets below applied. |
| `sellerUsernames` | array | (none) | Search mode only. Read each named username as a seller profile; the two toggles below add its reviews and its on-sale listings. |
| `fetchSellerReviews` | boolean | `false` | Applies to sellers named in Seller usernames and to seller profile or feedback URLs pasted in URL mode. Read each seller's reviews as their own rows (`rowType` `review`). |
| `maxReviewsPerSeller` | integer | empty | Applies to sellers named in Seller usernames and to seller profile or feedback URLs pasted in URL mode. Stop reading each seller's reviews after this many review rows (1 to 1000); leave empty for no per-seller limit (bounded only by Max items). Every review row is a dataset row of its own. A seller whose reviews were cut by this limit never marks reviews `EXPIRED`. |
| `includeSellerListings` | boolean | `false` | Applies to sellers named in Seller usernames and to seller profile or feedback URLs pasted in URL mode. Also walk each seller's on-sale listings as normal product rows. |
| `categories` | array | `["outerwear"]` | Search mode only. Only browse these categories; a name the store does not serve stops the run with a clear message. |
| `designers` | array | (none) | Search mode only. Only browse these designers, by the store's own name. |
| `conditions` | array | (none) | Search mode only. Only browse these conditions: `new`, `gently used`, `used` or `worn`. |
| `sizes` | array | (none) | Search mode only. Only browse these sizes, exactly as the store labels them. |
| `urls` | array | one example per supported page type (see the table below) | URL mode only. Paste listing, search, category, designer, designer plus category, seller or seller feedback URLs; add `?page=3` to a browse URL to start walking from that page. |
| `department` | string | `menswear` | Which storefront department to read: `menswear`, `womenswear` or `all` (both). Applies in both modes. |
| `minPriceUsd` | integer | (none) | Only keep listings whose current ask price is at least this many US dollars. Applies in both modes. |
| `maxPriceUsd` | integer | (none) | Only keep listings whose current ask price is at most this many US dollars. Applies in both modes. |
| `sortResultsBy` | string | `site_order` | Arranges the rows this run collected, after they are read. Applies in both modes. |
| `fetchDetails` | boolean | `false` | Fetch each listing's own page for the description, the measurement table, the photo set and the deeper seller fields. Charged once per record. |
| `maxItems` | integer | `20` | The single cap on this run: stop after collecting this many records across every search term, seller and URL. Set `0` for unlimited. |
| `maxPages` | integer | `0` | Safety bound on feed pages walked per search term, seller or URL. `0` means no separate page limit. |
| `resumeFromRunId` | string | (none) | Paste a previous run ID or dataset ID to continue a large crawl without returning or charging for records already collected there. |
| `incrementalMode` | boolean | `false` | Turn on for recurring monitoring of the same search: later runs return only NEW, UPDATED and REAPPEARED records. |
| `stateKey` | string | (none) | Name or share a monitoring campaign's stored state. Leave empty to derive one automatically from the search, URLs, sellers and filters. |
| `emitUnchanged` | boolean | `false` | Also return (and bill) records that have not changed since the last run. |
| `emitExpired` | boolean | `false` | Also return (and bill) records no longer found, once a run has fully scanned the tracked scope. |
| `changeScope` | string | `all` | `priceAndStock` returns `UPDATED` only when the price or sold state changed. |
| `compactUpdates` | boolean | `false` | Short `UPDATED` and `EXPIRED` rows with `priceChanges`. |
| `expireAfterMissedScans` | integer | `1` | Full scans a listing must be missing from before it is marked `EXPIRED`. |
| `ignoreFieldsForChanges` | array | (none) | Extra field names (or dotted paths) that never trigger `UPDATED`. |
| `mcpConnectors` | array | (none) | Optional: send a summary of each record to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which record pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on records written to each connector per run. |
| `proxy` | object | Apify Proxy | Connection settings. |

### Output Example

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

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

**Output views.** The dataset has a view per record kind, so each table shows only its own columns: **Overview** (everything), **Listings**, **Listing details** (description, measurements, photos, deeper seller fields), **Sellers**, **Seller reviews** and **Changes** (Incremental mode rows with `changeType` and `changedFields`). A run summary is also saved to the run's key-value store under `OUTPUT`.

### Plan Requirement

Search, category, designer and seller pages work on the default Apify Proxy, which includes datacenter connections, so no residential proxy is needed for them. Listing detail pages (Fetch listing details, or a pasted single-listing URL) sit behind a stronger store challenge; they are read through a backup connection, and a given detail page can still fail to load, in which case the row is returned without the detail fields. If the selected connection is unavailable (for example on an Apify free plan, which does not include residential proxies), the run logs "The selected connection is unavailable to this run" and continues on a backup connection, noting "Served over the backup connection" when that lane served the pages. If nothing could be read, the run stops with a plain message (proxy access refused for the account, no results because residential proxies are unavailable and the backup connection could not load the pages, or no results page loaded at all) rather than returning an empty dataset that looks like "nothing matched". Upgrading your plan or supplying your own proxy URLs under Connection gives more headroom for large or frequent runs.

### FAQ

#### How much does it cost?

Pricing is pay per event: one **Actor Start** event per GB of run memory when the run starts, one event per record returned (a listing, seller profile or review), and one **Listing details** event per listing only when Fetch listing details is on (not charged for compact `UPDATED` or `EXPIRED` rows, which carry no details). Unchanged records in Incremental mode are not charged unless Emit unchanged is on. The **Pricing** tab shows the current rates. Use **Max items** to cap the cost of any run.

#### Is it legal to scrape Grailed?

This actor collects only publicly available marketplace data. You are responsible for how you use it: follow Grailed's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Listing photos and designer names may carry their own rights separate from the plain facts (price, size, condition) attached to a listing.

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

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new, updated and reappeared records, and unchanged ones are not billed unless you turn on Emit unchanged. Unchanged listings still count toward each source's share of Max items, so give a monitoring run a Max items above the size of the search (or `0`), otherwise listings ranked below that slice are never checked.

#### Why did a run with several search terms return fewer rows than I expected?

Every search term, seller and URL in one run shares a single Max items budget and a single dedup check. When the budget caps a walk mid-page, every row on that page is marked as handled, even the ones the cap left no room to keep. A later, overlapping search term or seller scope in the same run then treats those rows as already collected and does not offer them again, so the total can land below Max items even though the store still has matching listings. Run heavily overlapping searches on their own, or raise Max items so a shared page clears before the run moves on.

#### 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 "nothing matched this search" is never confused with "nothing could be read". Run it again in a few minutes, or change the Proxy configuration. If only some URLs, listings or sellers could not be read, you still get what was found.

#### 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.

### 🔗 More scrapers you might like

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

<table>
<tr><td>🏷️ <a href="https://apify.com/abotapi/poshmark-scraper"><b>Poshmark Scraper</b></a><br>Scrape Poshmark fashion-resale listings: keyword search, brand and category browse, sold...</td><td>🧩 <a href="https://apify.com/abotapi/ssense-product-scraper"><b>SSENSE Scraper</b></a><br>Scrape SSENSE by keyword, category, designer or URL. Every row carries the selling price...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/depop-scraper"><b>Depop Scraper</b></a><br>Scrape Depop search results, listings, shops, and reviews to JSON, CSV, or Excel. Extract...</td><td>🏷️ <a href="https://apify.com/abotapi/vinted-marketplace-scraper"><b>Vinted Multi-Country Scraper</b></a><br>Scrape Vinted across 27 country marketplaces: keyword search with brand, price and...</td></tr>
<tr><td>👗 <a href="https://apify.com/abotapi/goat-sneaker-scraper"><b>GOAT Scraper</b></a><br>Scrape GOAT (goat.com), the sneaker and streetwear resale marketplace. Search or paste...</td><td>🛒 <a href="https://apify.com/abotapi/mercari-jp-scraper"><b>Mercari Japan Scraper</b></a><br>Scrape Mercari Japan listings, sellers and reviews at scale. Extract names, prices...</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/grailed-marketplace-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`):

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. Applies to sellers named in Seller usernames and to seller profile or feedback URLs pasted in URL mode, not to listings found by search or browse.

## `maxReviewsPerSeller` (type: `integer`):

Stop reading a seller's reviews after this many review rows. Leave empty for no per-seller limit (all of a seller's reviews, bounded only by Max items). Applies only when 'Fetch seller reviews' is on, and each seller counts separately. Applies to sellers named in Seller usernames and to seller profile or feedback URLs pasted in URL mode. In Incremental mode with Emit expired records, a seller whose reviews were cut by this limit never marks reviews EXPIRED.

## `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). Applies to sellers named in Seller usernames and to seller profile or feedback URLs pasted in URL mode.

## `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) designer plus category URLs (https://www.grailed.com/designers/designer-name/footwear), or seller profile and seller feedback URLs (https://www.grailed.com/username). A sort= value of price_asc, price_desc or newest on a pasted URL sets the row ordering when the Order the returned rows by field is left at the store ranking. Browse, collections and shop curation pages are skipped with a warning. Left untouched, the field scrapes one example of each supported page type. 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.

## `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.

## `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. What is compared: every field of the record, including the ask price, original price, price history, sold state and sold price, title, description, size, condition, category, designers, badges and seller details, so an ask-price move is reported as UPDATED (on a resale marketplace it is the signal, not noise). What is excluded: only the scrape time by default; name any field that turns out to be noisy in Ignore fields when detecting changes (it stays in the row, it just never triggers UPDATED), or limit changes to price and sold state with What counts as a change. 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 a per-seller review limit cut a seller's reviews, 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.

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

All fields (default): any change to the listing record, except the scrape time and the fields you list under Ignore fields for changes, returns an UPDATED row. Price and sold state only: only a change to the ask price, original price, price history, sold flag, sold date or sold price returns an UPDATED row; edits to the description, badges, size, seller details or other fields never do. Seller and review rows are always compared in full. Changing this option keeps the saved baseline; the next run compares against it under the new rule.

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

Off by default. When on, UPDATED rows (and EXPIRED rows) are short: listing id, url, title, designer, size, current price and sold state, plus priceChanges showing the old and new value of each moved price or sold field. NEW rows always carry the complete record. Compact rows do not carry the listing description or measurements, so the listing details fee is not charged for them.

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

Only used with Emit expired records. A listing is marked EXPIRED only after it has been missing from this many full scans in a row (default 1). Raise it (for example 3) so a listing that briefly drops out of the search results is not reported as gone. A listing that comes back resets the count. EXPIRED means the listing is no longer found in the search; without opening the listing it cannot tell a sold or removed listing from one that only dropped out of the results, so use the sold fields for sale status.

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

Optional. Field names to exclude from UPDATED change detection, in addition to the scrape time, which is always ignored. Use this if a field turns out to be noisy in your setup, for example followerCount, bumpedAt, heat or coverPhoto. Use a dotted name such as shipping.other to ignore a key inside a nested block (or inside every element of a list) while the rest of it stays compared. The ignored fields stay in the returned rows.

## `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.

## Actor input object example

```json
{
  "mode": "search",
  "searchTerms": [
    "varsity jacket"
  ],
  "fetchSellerReviews": false,
  "includeSellerListings": false,
  "categories": [
    "outerwear"
  ],
  "urls": [
    "https://www.grailed.com/shop/search?query=jacket",
    "https://www.grailed.com/shop/search?query=jacket&page=2",
    "https://www.grailed.com/shop/search?query=jacket&sort=price_asc",
    "https://www.grailed.com/categories/footwear",
    "https://www.grailed.com/categories/footwear?page=2",
    "https://www.grailed.com/categories/outerwear",
    "https://www.grailed.com/categories/all",
    "https://www.grailed.com/categories/womenswear/tops",
    "https://www.grailed.com/categories/womenswear/tops?page=2",
    "https://www.grailed.com/categories/tops/short-sleeve-shirts",
    "https://www.grailed.com/designers/rick-owens",
    "https://www.grailed.com/designers/rick-owens/footwear",
    "https://www.grailed.com/dacoziest",
    "https://www.grailed.com/dacoziest/feedback",
    "https://www.grailed.com/listings/106977873",
    "https://www.grailed.com/listings/108776060-julius-julius-washed-and-distressed-denim-jacket",
    "https://grailed.com/listings/109485837"
  ],
  "department": "menswear",
  "sortResultsBy": "site_order",
  "fetchDetails": false,
  "maxItems": 20,
  "maxPages": 0,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "changeScope": "all",
  "compactUpdates": false,
  "expireAfterMissedScans": 1,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

## `listings` (type: `string`):

No description

## `details` (type: `string`):

No description

## `sellers` (type: `string`):

No description

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

No description

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

No description

## `summary` (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/shop/search?query=jacket",
        "https://www.grailed.com/shop/search?query=jacket&page=2",
        "https://www.grailed.com/shop/search?query=jacket&sort=price_asc",
        "https://www.grailed.com/categories/footwear",
        "https://www.grailed.com/categories/footwear?page=2",
        "https://www.grailed.com/categories/outerwear",
        "https://www.grailed.com/categories/all",
        "https://www.grailed.com/categories/womenswear/tops",
        "https://www.grailed.com/categories/womenswear/tops?page=2",
        "https://www.grailed.com/categories/tops/short-sleeve-shirts",
        "https://www.grailed.com/designers/rick-owens",
        "https://www.grailed.com/designers/rick-owens/footwear",
        "https://www.grailed.com/dacoziest",
        "https://www.grailed.com/dacoziest/feedback",
        "https://www.grailed.com/listings/106977873",
        "https://www.grailed.com/listings/108776060-julius-julius-washed-and-distressed-denim-jacket",
        "https://grailed.com/listings/109485837"
    ],
    "department": "menswear",
    "fetchDetails": false,
    "proxy": {
        "useApifyProxy": true
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "changeScope": "all",
    "compactUpdates": false,
    "expireAfterMissedScans": 1,
    "ignoreFieldsForChanges": []
};

// 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/shop/search?query=jacket",
        "https://www.grailed.com/shop/search?query=jacket&page=2",
        "https://www.grailed.com/shop/search?query=jacket&sort=price_asc",
        "https://www.grailed.com/categories/footwear",
        "https://www.grailed.com/categories/footwear?page=2",
        "https://www.grailed.com/categories/outerwear",
        "https://www.grailed.com/categories/all",
        "https://www.grailed.com/categories/womenswear/tops",
        "https://www.grailed.com/categories/womenswear/tops?page=2",
        "https://www.grailed.com/categories/tops/short-sleeve-shirts",
        "https://www.grailed.com/designers/rick-owens",
        "https://www.grailed.com/designers/rick-owens/footwear",
        "https://www.grailed.com/dacoziest",
        "https://www.grailed.com/dacoziest/feedback",
        "https://www.grailed.com/listings/106977873",
        "https://www.grailed.com/listings/108776060-julius-julius-washed-and-distressed-denim-jacket",
        "https://grailed.com/listings/109485837",
    ],
    "department": "menswear",
    "fetchDetails": False,
    "proxy": { "useApifyProxy": True },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "changeScope": "all",
    "compactUpdates": False,
    "expireAfterMissedScans": 1,
    "ignoreFieldsForChanges": [],
}

# 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/shop/search?query=jacket",
    "https://www.grailed.com/shop/search?query=jacket&page=2",
    "https://www.grailed.com/shop/search?query=jacket&sort=price_asc",
    "https://www.grailed.com/categories/footwear",
    "https://www.grailed.com/categories/footwear?page=2",
    "https://www.grailed.com/categories/outerwear",
    "https://www.grailed.com/categories/all",
    "https://www.grailed.com/categories/womenswear/tops",
    "https://www.grailed.com/categories/womenswear/tops?page=2",
    "https://www.grailed.com/categories/tops/short-sleeve-shirts",
    "https://www.grailed.com/designers/rick-owens",
    "https://www.grailed.com/designers/rick-owens/footwear",
    "https://www.grailed.com/dacoziest",
    "https://www.grailed.com/dacoziest/feedback",
    "https://www.grailed.com/listings/106977873",
    "https://www.grailed.com/listings/108776060-julius-julius-washed-and-distressed-denim-jacket",
    "https://grailed.com/listings/109485837"
  ],
  "department": "menswear",
  "fetchDetails": false,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "changeScope": "all",
  "compactUpdates": false,
  "expireAfterMissedScans": 1,
  "ignoreFieldsForChanges": []
}' |
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/gJjgUu5i0xHnP3anY/openapi.json
