The RealReal Scraper avatar

The RealReal Scraper

Pricing

from $3.00 / 1,000 results

Go to Apify Store
The RealReal Scraper

The RealReal Scraper

Scrape The RealReal - the largest online marketplace for authenticated luxury resale. Browse by category or designer, or fetch full product detail (price, condition, size, measurements, authentication info, images) by product URL.

Pricing

from $3.00 / 1,000 results

Rating

0.0

(0)

Developer

Crawler Bros

Crawler Bros

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

Scrape The RealReal — the world's largest online marketplace for authenticated luxury resale. Browse by department/category or by designer, or fetch complete product detail (price, condition, size, measurements, authentication info, images) for any product URL.

What this actor does

  • Five modes: byCategory, byDesigner, byUrl, discoverCategories, discoverDesigners
  • Full taxonomy coverage: browse by department (women / men / jewelry / watches / art / home / kids / beauty) and a curated set of 50+ common subcategories, plus a free-text override for any category path on the site
  • discoverCategories mode — lists the site's own real, complete, cross-department category tree (department → category → subcategory), each with a live product count, straight from the same "Refine Results" facet data the site's own sidebar reads. One cheap fetch, zero anti-bot risk. Use it to find real categoryPath/customCategoryPath values beyond the curated dropdown.
  • discoverDesigners mode — lists up to 200 real designer/brand names for a department (optionally scoped to a category), each with a live product count and internal numeric ID, from the same facet data. One cheap fetch — reliable when scoped to just a department (the default), same best-effort anti-bot caveat as byCategory/byDesigner applies if you also set a specific categoryPath/customCategoryPath, since that fetches that exact taxon page rather than the always-reliable anchor discoverCategories uses. Solves the previous "guess a free-text designerSlug" problem — every returned designerSlug is verified to resolve to real byDesigner results.
  • Designer/brand browse: any of The RealReal's thousands of designer slugs (e.g. chanel, hermes, louis-vuitton), optionally filtered to a department
  • Full item detail: price (incl. estimated retail and any discount), condition grade, size, color, material, measurements, category breadcrumb, authentication note, return policy text, disclaimer text, sustainability (carbon/water saved), images
  • Marketing browse axes: sale and new-arrivals are available as curated categoryPath options (in addition to the 50+ taxonomy categories), confirmed to return real, currently-live listings
  • Department-aware size/material: the GraphQL size-carrying attribute type differs by department (clothing CLOTHING_SIZE/FOREIGN_SIZE, shoes SHOE_SIZE, jewelry RING_SIZE, art ART_SIZE, watches CASE_DIAMETER, kids KIDS_*/INFANTS_*/TODDLERS_*) and material is METAL_TYPE for jewelry instead of PRIMARY_MATERIAL/FABRICsize/material are resolved across every known type so they populate on every department, not just clothing (verified live 2026-07-26 across all 7 departments)
  • Filters: price range, condition, color, size, material, movement type (watches), case material (watches), on-sale-only
  • Jewelry detail: carat weight (ctw, summed across stones), clarity grade, color grade, gemstone, stone type, stone shape(s) — verified live 2026-07-26 against the site's own jewelry-department attribute data
  • Watch detail: model name, reference number, collection, movement type, case material, band material, watch style(s), complications — verified live 2026-07-26 against the site's own watches-department attribute data
  • Dress/menswear detail: dress silhouette (e.g. "Wrap Dress") and men's chest size band — verified live 2026-07-27 against the site's own dresses/men's-clothing attribute data
  • Empty fields are omitted — every record only contains fields the source actually disclosed

Output per product

FieldNotes
productId, sku
variantIdThe specific listing's internal variant ID
name, brand, brandIdbrandId is the brand's internal numeric designer ID
department, categoryPath, categoryBreadcrumb[]breadcrumb only on byUrl
conditionAs Is / Fair / Good / Very Good / Excellent / Pristine
conditionNotesbyUrl only — the item's real free-text condition description (e.g. "No visible signs of wear", "Minor scratches throughout metal; Minor wear at strap"), distinct from the condition grade above
size, color, gender, materialsize/material resolved across department-specific attribute types (clothing/shoe/ring/art/watch/kids sizes; fabric or metal-type material) so they populate on every department, not just clothing
artistIdArt department only — the piece's internal artist ID (artist name is already in brand)
caratWeight, clarityGrade, colorGrade, gemstone, stoneType, stoneShapes[]Jewelry department only — caratWeight is the total carat weight summed across all stones of that type on the item
watchModel, referenceNumber, watchCollection, movementType, caseMaterial, bandMaterial, watchStyles[], complications[]Watches department only
dressSilhouetteDresses category only — e.g. "Wrap Dress", "Slip Dress", "Evening Gown" (verified live 2026-07-27: 100% populated on real dresses listings)
chestSizeMen's clothing only — chest size band (e.g. "42", "48 +"), when the designer reports it alongside size
priceUsd, priceFormattedcurrent price
originalPriceUsd, originalPriceFormattedonly present if discounted from a prior TRR price
estRetailPriceUsd, estRetailPriceFormattedbrand-new retail estimate, when disclosed
discountText, discountPercentonly present when the item is on sale — in practice rare/absent, since the browse-listing API this actor reads does not reliably expose a markdown signal (see onSaleOnly note under Input below)
images[], imageUrldirect CDN URLs, not hotlink-protected
productUrlcanonical therealreal.com/products/... URL
availability, quantity, obsessionCount, waitlisted, editorsPick, badgebadge is the merchandising label text only (e.g. "Good Value", "New")
buyNowEligiblebyUrl only — whether the site offers an instant "Buy It Now" fast-checkout path for this item vs its normal add-to-bag flow
description, measurements[], authenticationInfo, returnable, returnPolicyText, disclaimerText, categoryLabel, carbonSavedKg, waterSavedLitersbyUrl only
valuationReportUrlbyUrl only, watches — link to the item's real appraisal/valuation report page
availablePolicies[]byUrl only — {name, priceUsd, sku} optional add-on protection/warranty plans, when offered (e.g. watches)
glossaryTerms[]byUrl only — {name, description} item-specific terminology (e.g. watch complications), when the site discloses it
slugbyUrl only — the product's URL slug
sizeGuidebyUrl only — {guideType: "table", columnHeaders[], rows[][]} (a real size-conversion table, e.g. shoes' US/Italy/UK/France/Japan) or {guideType: "document", description, downloadUrl} (a downloadable PDF chart, e.g. rings), when the department offers one
measurementGuidebyUrl only — {imageUrl, measurements: [{label, description}]} general how-to-measure diagram/instructions for the department, distinct from the item's own measurements[]
firstLookProductbyUrl only — present (true) only when the item is early-access-eligible for registered members
recordType: "product", scrapedAt

Output — category taxonomy (recordType: "category", mode=discoverCategories)

FieldNotes
nameHuman-readable category/subcategory name
departmentTop-level department slug, e.g. women
categoryPathPath after the department, e.g. handbags/totes — absent for a top-level department node
depth1 = department, 2 = category, 3 = subcategory
productCountLive count of items currently listed under this node
categoryUrlhttps://www.therealreal.com/shop/<department>[/<categoryPath>]
recordType, scrapedAt

Output — designer facet (recordType: "designer", mode=discoverDesigners)

FieldNotes
designerNameReal designer/brand display name
designerSlugDerived slug — verified to resolve to real byDesigner results
designerIdThe RealReal's internal numeric designer ID
productCountLive count of items currently listed for this designer within the requested department/category scope
departmentThe department this facet was scoped to
designerUrlhttps://www.therealreal.com/designers/<designerSlug>[/<department>]
recordType, scrapedAt

Input

FieldTypeModeDescription
modeselectbyCategory / byDesigner / byUrl / discoverCategories / discoverDesigners
genderselectbyCategory / discoverDesignersDepartment: women / men / jewelry / watches / art / home / kids / beauty
categoryPathselectbyCategory / discoverDesignersCurated subcategory (50+ options)
customCategoryPathstringbyCategory / discoverDesigners (override); discoverCategories (prefix filter)Free-text override, e.g. clothing/dresses/maxi. Each /-separated segment is sanitized independently: a segment containing anything other than lowercase letters/digits/hyphens (path-traversal .., HTML/script injection, unicode/accented text, stray spaces, etc.) is silently dropped rather than rejecting the whole path — e.g. clothing/drésses (accented typo) normalizes to just clothing, browsing/listing the whole Clothing department rather than 0 results. This is deliberate (round-2 QA: a rejected-whole-path approach let a ../../etc/passwd-style segment reach the upstream URL builder and 422); it means a badly-typed subcategory silently widens to its nearest valid ancestor instead of failing — copy the exact slug from discoverCategories output or a real therealreal.com URL to avoid this.
designerSlugstringbyDesignere.g. chanel, gucci, hermes
designerGenderselectbyDesignerRestrict to a department
productUrlsarraybyUrlFull product URLs
minPrice / maxPriceintallUSD
conditionarray (select)allAs Is / Fair / Good / Very Good / Excellent / Pristine
colorselectall19 curated colors
sizestringallExact size match (case-insensitive), free text since the vocabulary is department-specific (e.g. M, 8.5, 6, 36mm)
materialstringallSubstring match against material (case-insensitive), free text since the vocabulary is department-specific (e.g. Gold matches Yellow Gold/White Gold/Rose Gold, Leather, Silk, Cashmere). Not populated on every item/department — verified live 2026-07-26: reliably populated on jewelry (METAL_TYPE, 120/120 sampled), absent on the sampled clothing/dresses/clothing/coats listings.
movementTypeselectallWatches only — Automatic / Quartz / Manual / Co-Axial Escapement Automatic / Spring Drive / SolarBeat™
caseMaterialselectallWatches only — Yellow Gold / Rose Gold / White Gold / Platinum / Silver / Stainless Steel / Stainless Steel and Gold / Titanium / Ceramic / Other
onSaleOnlyboolallOnly discounted items. Known limitation (confirmed live, QA round after feature-expansion pass 3): therealreal.com's own browse-listing GraphQL query never populates a real discount/markdown signal — price.discount is null and price.original == price.final for every item returned, even on the site's own literal sale merchandising collection (0/120 sampled items had a genuine markdown across women/sale, men/sale, and ordinary categories). This means onSaleOnly=true may legitimately return 0 records on any category/designer; the run's status message calls this out explicitly rather than suggesting you loosen your filters.
sortByselectallnewest / priceAsc / priceDesc / mostObsessed
maxItemsintall1–500, default 30

Example: Chanel handbags under $3,000

{
"mode": "byDesigner",
"designerSlug": "chanel",
"designerGender": "women",
"maxPrice": 3000,
"maxItems": 50
}

Example: pristine-condition women's dresses, sorted cheapest first

{
"mode": "byCategory",
"gender": "women",
"categoryPath": "clothing/dresses",
"condition": ["Pristine"],
"sortBy": "priceAsc",
"maxItems": 50
}

Example: discover the real category taxonomy under "handbags"

{
"mode": "discoverCategories",
"customCategoryPath": "handbags",
"maxItems": 100
}

Example: discover the top designers for women's shoes

{
"mode": "discoverDesigners",
"gender": "women",
"categoryPath": "shoes",
"maxItems": 50
}

Use cases

  • Resale price benchmarking — track live prices, discounts, and estimated retail value for designer items across categories or a specific brand.
  • Inventory monitoring for resellers — watch a designer's or category's live listings (obsessionCount, availability, badge) to spot fast-moving or newly-listed pieces.
  • Market research on luxury resale trends — analyze condition, size, and color distribution across a department or designer's current catalog.
  • Sustainability reporting — aggregate carbonSavedKg/waterSavedLiters from byUrl fetches to quantify the environmental impact of resale.
  • Catalog/taxonomy mapping — use discoverCategories and discoverDesigners to build a complete, accurate picker UI without guessing slugs.
  • Authentication & provenance research — pull authenticationInfo and full item detail for due-diligence on a specific listed piece.

FAQ

Is this affiliated with The RealReal? No. This is an independent third-party actor that reads The RealReal's own publicly rendered pages. It is not affiliated with, endorsed by, or supported by The RealReal, Inc.

Why does this actor fetch through a reader-proxy instead of therealreal.com directly? therealreal.com is protected by PerimeterX, which blocks direct and TLS-impersonated automated requests alike (see "Data Source & anti-bot escalation" below for the full evidence). Routing through a public server-side-rendering reader-proxy reliably renders the site's real, live data without requiring cookies, login, or a paid residential proxy.

Why do some categories or byUrl fetches return 0 records? A small number of high-traffic pages (the Handbags department, bare aggregate category pages, and individual product-detail pages) are currently subject to a tighter PerimeterX block than the rest of the site. The actor retries and fails soft with a clear status message rather than guessing or fabricating data — treat those paths as best-effort and prefer a specific subcategory (e.g. clothing/dresses, shoes/sneakers) or byDesigner for guaranteed-reliable results.

Why does onSaleOnly=true sometimes return 0 records? The RealReal's own browse-listing data does not currently expose a genuine discount/markdown signal on any sampled category, including its own sale merchandising collection — this is an upstream data-availability limitation, not a filter bug. originalPriceUsd/discountPercent are still populated correctly whenever the source does disclose a real discount.

Is there a live keyword-search mode? No. The RealReal's search results are rendered client-side after the page loads, so there's no server-rendered snapshot to read. Use byCategory or byDesigner (and discoverCategories/discoverDesigners to find real values) instead.

How fresh is the data? Every fetch reads The RealReal's live, currently-listed inventory at run time — prices, availability, and product counts reflect the site as of the moment the actor runs.

Does this actor require login, cookies, or a paid proxy? No. It runs on the Apify free plan with no login, no cookies, and no paid proxy of any kind.

Data Source & anti-bot escalation

therealreal.com is protected by PerimeterX, which serves a px-captcha "Access to this page has been denied" 403 on essentially every automated request. This was verified through a genuine escalation ladder before falling back to any workaround:

  1. Tier 2 — curl_cffi with browser TLS impersonation. Requests to the homepage, category, and product pages with chrome131, chrome124, and safari184 impersonation plus a full realistic header set (Accept-Language, sec-ch-ua, sec-fetch-*, etc.) all returned HTTP 403 with an identical PerimeterX px-captcha body, regardless of impersonated browser.
  2. Tier 3 — headless Playwright with stealth. Chromium launched headless with --disable-blink-features=AutomationControlled and playwright_stealth applied, hitting the real homepage, also returned HTTP 403 with the same px-captcha page — even from a non-datacenter (residential ISP) source IP, which is a stronger signal than Apify's own datacenter/AUTO-proxy exits would produce.
  3. Reader-proxy fallback (r.jina.ai). Both direct tiers genuinely failed, so per the project's anti-bot decision tree a neutral third-party mirror was tried: r.jina.ai, a public server-side-rendering reader-proxy. It reliably renders therealreal.com's real, live Next.js pages and returns the same __NEXT_DATA__ GraphQL payload the site's own React frontend consumes — including full product listings, prices, images, and (on product-detail pages) condition, size, measurements, and authentication copy. This actor fetches every page through r.jina.ai, not the origin directly.

This is a documented, third-party architectural dependency, not something Apify or this project operates or guarantees:

  • If r.jina.ai is unavailable or rate-limited, this actor will fail soft (0 records + a clear Actor.set_status_message) rather than crash.
  • Every fetch is retried (up to 4 attempts, exponential backoff, rotating TLS-impersonation profile) before giving up on a single page.
  • A content-based quality-retry check additionally verifies the actual returned items (not just the echoed request) match what was requested — e.g. for byCategory, that a majority of returned products' own URLs fall under the requested department/category; for byDesigner, that a majority of returned products' brand slugifies to the requested designer. This was added after cloud verification caught a real upstream bug: the site's echoed GraphQL variables correctly reflect the requested taxon/designer even when the underlying query silently falls back to a generic, unfiltered item set (reproduced with both an invalid sub-category and a nonexistent designer slug) — so the echoed variables alone are not trustworthy proof of filtering. If a page never returns genuinely matching data after all retries, that page is skipped (0 records + set_status_message) rather than shipping wrong-category records. See edges_match_category / edges_match_designer in src/helpers.py.
  • Certain high-traffic, bare/aggregate category pages — e.g. women's Handbags (women/handbags) and the bare Clothing/Accessories department pages (women/clothing, women/accessories, no subcategory) — were observed to be hard-blocked by PerimeterX consistently more often than specific subcategory pages, likely because they're the site's highest-traffic pages. Re-confirmed live (2026-07-25, QA round 3): categoryPath=clothing (no subcategory) reproducibly returned 0 records (consistent px-captcha payload, ~5.7 KB, via the reader-proxy) on 2 separate attempts a few minutes apart, while categoryPath=clothing/dresses and every other specific subcategory tested rendered reliably (including a clean 500/500-item maxItems stress run). Update (2026-07-26, round-4 QA): the block has since widened to cover the entire Handbags department, including its named subcategories (handbags/totes, handbags/shoulder-bags, handbags/crossbody-bags, handbags/clutches all reproducibly returned 0 records / px-captcha, verified both through the actor and via a bare reader-proxy probe outside the actor entirely, ruling out an actor-code cause) — this is a change from the state recorded in QA round 3, where handbags subcategories rendered reliably; PerimeterX's protection on this specific high-value department has evidently tightened since. clothing/dresses, shoes, and the bare women department page were re-confirmed reliable in the same probe session, so this is category-specific, not a general/session-wide block. The actor still attempts bare/aggregate and Handbags-department pages with full retry logic; if a page fails, Actor.set_status_message reports this clearly rather than emitting 0 records silently. Recommendation: pick a specific subcategory outside the Handbags department (e.g. clothing/dresses, shoes/sneakers) for reliable results; treat any Handbags path (including named subcategories) as best-effort until PerimeterX's stance on that department changes again.
  • byUrl (product-detail) fetches are currently blocked (re-verified 2026-07-25, adversarial QA round 1): PerimeterX now serves the px-captcha block page through r.jina.ai for individual /products/... detail pages consistently — reproduced independently on 3 separate fresh, real, currently-listed product URLs (including URLs pulled live from this actor's own byCategory output moments earlier) both through the actor itself and via a bare curl against r.jina.ai outside the actor entirely, ruling out an actor-code cause. byCategory and byDesigner listing pages are unaffected and continue to render reliably through the same reader-proxy path. This is a change from the state recorded in earlier verification rounds (see the Round 4b table below, where a byUrl call did return 1 real record) — PerimeterX's product-detail-page policy has evidently tightened since. The actor already handles this correctly (fails soft: 0 records + "No products resolved from the given URLs." status message, no crash, no timeout), so no code change was needed; byUrl should currently be treated as best-effort like the Handbags category, not as reliable as byCategory/byDesigner.
  • Free-text keyword search is not supported in this version: The RealReal's /shop/search?q=... route does not apply the query server-side in its initial render (the real search executes as a client-side call after hydration, which is invisible to a server-rendered snapshot), and no public GET-based search API endpoint was found. Category browse and designer browse cover the vast majority of realistic use cases; a future version could add search if the site exposes a stable API for it.

Feature-surface expansion (2026-07-26, 5th pass) — investigation evidence

A further pass re-checked whether an interactive UI element (sort control, "Refine Results" sidebar) had gone unreverse-engineered. Findings:

Added:

  • discoverCategories / discoverDesigners modes — every byCategory/ byDesigner listing fetch's serverResult.data.products payload already carries a aggregations array: this is the exact data that powers the site's own "Refine Results" sidebar (Categories, Designers, Condition, Color, sizes, etc.), previously read only for edges/pageInfo and never for aggregations. Two facets are genuinely new, zero-extra-cost feature surface (same fetch already being made): the real, complete, cross- department category tree (verified 3 levels deep with live product counts) and up to 200 real designer names per department/category scope (with live product counts and internal numeric IDs) — both confirmed live 2026-07-26. A designer name's derived slug was spot-checked against real byDesigner fetches: Acne Studiosacne-studios → 9,095 items; 3.1 Phillip Lim3-1-phillip-lim → 8,487 items — both real, matching counts, not fabricated.
  • beauty added to the gender department enum — discovered as an 8th top-level department in the same aggregations "Categories" facet (not previously listed); confirmed live via shop/beauty (HTTP 200, 6 real listed items — a genuinely small but real department, honestly small rather than silently dropped).

Investigated and rejected:

  • Server-side sort control — the listing GraphQL response's sortByOptions field reveals the upstream does support 5 real sort values (NEWEST, LOWEST_PRICE, HIGHEST_PRICE, SOLD, MOST_OBSESSED), one more (SOLD/"Sold Items First") than this actor's existing local sortBy reorder. However, no URL query parameter (tried sortBy, sort, orderBy, sort_by, sortByValue, both upper/lower- cased values) was found that changes sortByOptions.selected away from NEWEST on the server-rendered response — the sort control appears to be driven by a client-side GraphQL variable set after hydration, which the reader-proxy's static-render snapshot cannot reach (same class of limitation as the already-documented keyword-search rejection above). sortBy therefore remains a local post-fetch reorder of the already- fetched maxItems-worth of newest-first records, not a real upstream sort parameter, and SOLD was not added (no reachable way to request it).
  • Combined category+designer browse query variable — investigated whether a where.booleans/where.buckets GraphQL variable could be passed via URL to filter a category page to one designer (or vice versa) in a single fetch; no such query-string mechanism was found through the static-render path (same client-side-only limitation as sort, above). discoverDesigners mode already lets a caller find the right slug to then run as a separate byDesigner fetch (with designerGender for department scoping) as a practical two-step workaround.

Feature-surface expansion (2026-07) — investigation evidence

A follow-up pass re-verified the full upstream surface through the reader-proxy and checked whether direct fetch had become viable. Findings:

Added:

  • sale and new-arrivals curated categoryPath options — confirmed via reader-proxy fetch of shop/women/sale (HTTP 200, 1.8 MB payload, 120 real product edges) and shop/women/new-arrivals / shop/new-arrivals (HTTP 200, ~1.8 MB, 120 real edges). These are reachable today through the existing customCategoryPath mechanism too; adding them to the curated dropdown just makes them discoverable without needing the free-text override.
  • returnPolicyTextproduct.returnPolicy.description (e.g. "This item can be returned for credit card refund. Return Authorization requests must be made within 14 days..."), confirmed present on a real fetched product detail page (SKU WEX127910) but not previously captured (only the returnable boolean was).
  • disclaimerTextproduct.disclaimers[].message (e.g. "Coupons and discounts are not available for this product."), confirmed present on the same real fetch.
  • categoryLabelproduct.category.label (e.g. "Women's Fashion & Accessories"), a human-readable top-level category name distinct from the existing categoryBreadcrumb.

Investigated and rejected:

  • Seller/consignor infoproduct.representative and product.storeContactInfo are both null on every real product detail page fetched (SKU WEX127910 dress, SKU CHA1467696 Chanel boots — 2 different departments). The RealReal does not expose per-item consignor identity publicly (consistent with its consignment-anonymity model); no field to add.
  • Price-drop / promotion historyproduct.promotion and product.valuationReportUrl are null on every real fetch above. At the listing-GraphQL level, price.original equaled price.final (no diff) and price.discount was null across 240 sampled listing items spanning 2 categories (women/clothing/dresses, women/shoes/sneakers) and the dedicated shop/women/sale listing (120 items, still 0 with a real discount). The existing originalPriceUsd/discountPercent fields already handle a discount correctly when present (per the shipped omit-empty contract) — there is simply no live discounted inventory to observe right now, and no separate price-history field exists upstream to add.
  • "Similar Items" / "You may also like" — confirmed present as a UI section (data-testid="similar-items-carousel") on product detail pages, but its <div class="swiper-wrapper"> is server-rendered empty; the carousel is populated by a client-side XHR call after hydration, which the reader-proxy's static HTML/__NEXT_DATA__ snapshot cannot see. No reliable server-rendered data source for this exists.
  • Item condition report detailpreviously marked "already fully captured" correction (2026-07-27 pass): this was wrong. A fresh key-by-key diff of 3 real product-detail pages found the site's own condition-type attribute carries a real, distinct free-text note (e.g. "No visible signs of wear", "Minor scratches throughout metal; Minor wear at strap") that was never being extracted — not because the field didn't exist, but because of a real dead-code bug (see "Bug fix" below) in the attribute-type lookup that silently failed on product-detail-page attribute shapes. Now shipped as conditionNotes.

Bug fix (2026-07-27 pass) — dead-code attribute-type mismatch on byUrl: Listing pages (byCategory/byDesigner) encode compound GraphQL attribute types as SCREAMING_SNAKE_CASE (e.g. CLOTHING_SIZE, RING_SIZE, CASE_MATERIAL), but product-detail pages (byUrl) encode the exact same attributes as lower-hyphen-case (e.g. clothing-size, ring-size, case-material) — confirmed live against 3 real product-detail pages spanning clothing/jewelry/watches. The attribute-type comparison only normalized case, not the hyphen-vs-underscore difference, so every compound (multi-word) attribute type — size and every jewelry/watch-specific field — silently extracted as absent on every single byUrl record, site-wide, since those fields were introduced. Single-word types (COLOR, CONDITION, FABRIC, GENDER, MOVEMENT) happened to match by coincidence, which is why this went unnoticed across 13 prior expansion passes. Fixed by normalizing both hyphens and underscores before comparing (_norm_attr_type in src/helpers.py); added regression tests covering the hyphenated shape for _attr_first/_attr_all/_size_value/_material_value and an end-to-end build_detail_record test.

  • Direct fetch (tiered fallback) for category listings or item detail — re-tested curl_cffi against both a category URL (shop/women/clothing/dresses) and a product URL, with chrome124, safari17_0, and chrome131 impersonation plus a full realistic header set (Accept, Accept-Language, sec-ch-ua, sec-fetch-dest/mode/site/user, upgrade-insecure-requests). All 6 combinations returned HTTP 403 with the identical PerimeterX px-captcha body ("Access to this page has been denied", 5703 bytes). No route is currently fetchable directly; the reader-proxy-always architecture is unchanged.
  • Designer page reliability note — during this pass, the bare /designers/<slug> URL (no department segment) was observed hard-blocked (px-captcha) on 4/4 consecutive attempts across 4 different designer slugs, while the department-qualified /designers/<slug>/<department> URL succeeded normally. This actor's designerGender input already defaults to women (always populated in build_designer_url), so the shipped default path is unaffected; this is noted here as an observed reliability characteristic of the bare aggregate page (similar to the already-documented Handbags category), not a regression.

Cloud Verification

All 7 rounds of the project's standard cloud-verification cadence were run on Apify actor omnipotent_org/therealreal-scraper (Actor ID MzsAttKd5TTQCldB4), a QA dummy account — never crawlerbros. Every run below was re-confirmed live against the Apify platform API (GET /v2/acts/{actorId}/runs and GET /v2/datasets/{id}), not recalled from memory. All 20 runs are SUCCEEDED.

RoundPurposeRun IDDataset IDItems
1Default prefill (byCategory / women / clothing/dresses)1j23jG5kIo0c5HvkpC9aMbbjdX9GJV6HxE20
2Non-default mode + filter (byDesigner chanel/women)xNVT4I3vEPkd3Z7tn3n9hg3cJX3v9rhpU225
3URL accessibility (sampled from Round 1/2 datasets, see below)
4aBoundary: maxItems=1Gi5M8CgIjPsXhDPnpbHoeezaNsDIaWq6tS1
4aBoundary: maxItems=500 (department-wide)26j96IgeBtRXgyfMp1IxF8IsHe5u14axyu500
4aAll filters combined (price/condition/department)F5JMHNq1vVGGkFIpFvkW6PxPyQGWe8g1WO30
4bEdge: inverted price range (minPrice=900 > maxPrice=10)6KwegVrSuuZq09dXtqqnsgitJaoWQCZvcU0 (clean, no crash)
4bEdge: bogus designer slug (zzz-not-a-real-designer-xyz)ngjZBB6bgGynrCTN1hQEF8O9fmKn3uZUaf0 (correctly rejected by content-based quality-check — see below)
4bEdge: unicode designer slug (シャネル)VpganonuoGElO5E7TG06Kc95P3zdQMLauT0 (clean, no UnicodeEncodeError)
4bEdge: byUrl with 1 malformed + 1 non-product + 1 valid URLPzzgl1KpTYlEj44VOYYC6NjHUO1CMOXIL81 (invalid entries skipped, no crash)
5Repeat run 1/3 (same input)uQnsDuYg7H6C81qNITq2beTlfYUiMPaePb25
5Repeat run 2/35jxjKNlnLz4WSoQHk3bRmL4x5yPirwlDyb25
5Repeat run 3/3G56NhWM4pAXaGgsCcMBY3S2teqf1dRSaNo25 (0% variance across all 3)
6Combined filters (price + color + sort)9F0Zanzs1YzZBPgUUnwPC6Mb48b8TfCCwT30
6Multi-URL batch with 1 intentional duplicateUzZ0szx78fCn1nNRPd5hUpToTRf8a7sXvU3 (deduped from 4 input URLs)
7aInternational/non-default department (jewelry/rings)abexA7u0UFsjVggjw8imF2ocjZyhiVbtmU15
7bPagination exact: maxItems=120 (= 1 page)jPS7qreCyHCIkqVYiLSJBfcy2I99kCveMT120
7bPagination exact: maxItems=130 (spans 2 pages)mUeTNAGPUbCq5UwAjJvH1pJYkzy67YwSaO130
7cLong-running: maxItems=400, full departmenttahlEWL3PfDK9b1ycz9nX6C813Jt2VClJL400 (completed in ~41s, no timeout, no slowdown)

Round 1 sample record (default prefill, C9aMbbjdX9GJV6HxE)

{
"productId": "53030673", "sku": "OSC282427", "name": "V-Neck Knee-Length Dress",
"brand": "Oscar de la Renta", "department": "women", "categoryPath": "clothing/dresses",
"condition": "Excellent", "size": "S", "color": "Blue", "priceUsd": 235, "priceFormatted": "$235.00",
"imageUrl": "https://product-images.therealreal.com/OSC282427_1_enlarged.jpg",
"productUrl": "https://www.therealreal.com/products/women/clothing/dresses/oscar-de-la-renta-halterneck-knee-length-dress-vkmq9",
"availability": "AVAILABLE", "quantity": 1, "obsessionCount": 7, "recordType": "product",
"scrapedAt": "2026-07-24T18:26:12.479548+00:00"
}

Round 2 sample record (byDesigner chanel/women, 3n9hg3cJX3v9rhpU2) — confirms filter honored (all 25 records are brand: "Chanel", gender: "Women")

{
"productId": "53211495", "sku": "CHA1467696", "name": "2015 Interlocking CC Logo Boots",
"brand": "Chanel", "department": "women", "categoryPath": "shoes/boots", "condition": "Fair",
"size": "IT 37.5", "priceUsd": 265, "estRetailPriceUsd": 1150,
"productUrl": "https://www.therealreal.com/products/women/shoes/boots/chanel-2015-interlocking-cc-logo-boots-voi93",
"recordType": "product", "scrapedAt": "2026-07-24T18:26:57.233195+00:00"
}

Round 4b sample record (byUrl, full detail fields, YYC6NjHUO1CMOXIL8)

{
"productId": "53448685", "sku": "WEX127910", "name": "Skirt Set w/ Tags", "brand": "Alexis",
"condition": "Pristine", "categoryBreadcrumb": ["Skirt Sets", "Suits and Sets", "Clothing", "Women"],
"priceUsd": 335, "measurements": [{"label": "Bust", "value": "31.25\""}, {"label": "Waist", "value": "28.25\""}],
"authenticationInfo": "We authenticate every item with a rigorous process overseen by experts. ...",
"carbonSavedKg": 1.24, "waterSavedLiters": 4.11, "returnable": true, "recordType": "product"
}

Round 3 — URL accessibility (re-verified via clean curl -ILSs, no Referer/cookies)

  • https://product-images.therealreal.com/OSC282427_1_enlarged.jpg200 image/jpeg
  • https://product-images.therealreal.com/CHA1467696_1_enlarged.jpg200 image/jpeg
  • https://product-images.therealreal.com/GUC2102612_1_enlarged.jpg (sampled from the Round 7c 400-item run) → 200 image/jpeg
  • productUrl fields (e.g. .../products/women/shoes/boots/chanel-...) → 403 from a plain curl, as documented above (PerimeterX; loads normally in a real browser).

Local test suite

python -m pytest tests/ -q320 passed, 0 failed (re-run and re-confirmed after the content-based quality-check fix above).

Bug found and fixed during cloud verification

Round 4b's bogus-designer-slug test (zzz-not-a-real-designer-xyz) initially returned 10 unrelated records instead of 0 — root cause: the site's GraphQL variables echo the requested (bogus) slug even when it silently falls back to a generic, unfiltered listing. Fixed by adding edges_match_designer / edges_match_category (content-based verification against the returned items, not the echoed request) and re-verified live: the same input now correctly returns 0 records with a clear set_status_message (run ngjZBB6bgGynrCTN1, dataset hQEF8O9fmKn3uZUaf).

Notes

  • No login, cookies, or paid proxy required — the actor works on the Apify free plan.
  • Product images are served from product-images.therealreal.com, which is not hotlink-protected (verified via a clean curl -ILSs, no Referer/cookies needed).
  • productUrl values point at therealreal.com itself; loading them directly in a plain HTTP client (e.g. curl) will hit the same PerimeterX wall described above — they load normally in a real browser.