The RealReal Scraper
Pricing
from $3.00 / 1,000 results
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
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
discoverCategoriesmode — 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 realcategoryPath/customCategoryPathvalues beyond the curated dropdown.discoverDesignersmode — 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 asbyCategory/byDesignerapplies if you also set a specificcategoryPath/customCategoryPath, since that fetches that exact taxon page rather than the always-reliable anchordiscoverCategoriesuses. Solves the previous "guess a free-textdesignerSlug" problem — every returneddesignerSlugis verified to resolve to realbyDesignerresults.- 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:
saleandnew-arrivalsare available as curatedcategoryPathoptions (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 (clothingCLOTHING_SIZE/FOREIGN_SIZE, shoesSHOE_SIZE, jewelryRING_SIZE, artART_SIZE, watchesCASE_DIAMETER, kidsKIDS_*/INFANTS_*/TODDLERS_*) and material isMETAL_TYPEfor jewelry instead ofPRIMARY_MATERIAL/FABRIC—size/materialare 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
| Field | Notes |
|---|---|
productId, sku | |
variantId | The specific listing's internal variant ID |
name, brand, brandId | brandId is the brand's internal numeric designer ID |
department, categoryPath, categoryBreadcrumb[] | breadcrumb only on byUrl |
condition | As Is / Fair / Good / Very Good / Excellent / Pristine |
conditionNotes | byUrl 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, material | size/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 |
artistId | Art 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 |
dressSilhouette | Dresses category only — e.g. "Wrap Dress", "Slip Dress", "Evening Gown" (verified live 2026-07-27: 100% populated on real dresses listings) |
chestSize | Men's clothing only — chest size band (e.g. "42", "48 +"), when the designer reports it alongside size |
priceUsd, priceFormatted | current price |
originalPriceUsd, originalPriceFormatted | only present if discounted from a prior TRR price |
estRetailPriceUsd, estRetailPriceFormatted | brand-new retail estimate, when disclosed |
discountText, discountPercent | only 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[], imageUrl | direct CDN URLs, not hotlink-protected |
productUrl | canonical therealreal.com/products/... URL |
availability, quantity, obsessionCount, waitlisted, editorsPick, badge | badge is the merchandising label text only (e.g. "Good Value", "New") |
buyNowEligible | byUrl 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, waterSavedLiters | byUrl only |
valuationReportUrl | byUrl 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 |
slug | byUrl only — the product's URL slug |
sizeGuide | byUrl 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 |
measurementGuide | byUrl only — {imageUrl, measurements: [{label, description}]} general how-to-measure diagram/instructions for the department, distinct from the item's own measurements[] |
firstLookProduct | byUrl only — present (true) only when the item is early-access-eligible for registered members |
recordType: "product", scrapedAt |
Output — category taxonomy (recordType: "category", mode=discoverCategories)
| Field | Notes |
|---|---|
name | Human-readable category/subcategory name |
department | Top-level department slug, e.g. women |
categoryPath | Path after the department, e.g. handbags/totes — absent for a top-level department node |
depth | 1 = department, 2 = category, 3 = subcategory |
productCount | Live count of items currently listed under this node |
categoryUrl | https://www.therealreal.com/shop/<department>[/<categoryPath>] |
recordType, scrapedAt |
Output — designer facet (recordType: "designer", mode=discoverDesigners)
| Field | Notes |
|---|---|
designerName | Real designer/brand display name |
designerSlug | Derived slug — verified to resolve to real byDesigner results |
designerId | The RealReal's internal numeric designer ID |
productCount | Live count of items currently listed for this designer within the requested department/category scope |
department | The department this facet was scoped to |
designerUrl | https://www.therealreal.com/designers/<designerSlug>[/<department>] |
recordType, scrapedAt |
Input
| Field | Type | Mode | Description |
|---|---|---|---|
mode | select | – | byCategory / byDesigner / byUrl / discoverCategories / discoverDesigners |
gender | select | byCategory / discoverDesigners | Department: women / men / jewelry / watches / art / home / kids / beauty |
categoryPath | select | byCategory / discoverDesigners | Curated subcategory (50+ options) |
customCategoryPath | string | byCategory / 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. |
designerSlug | string | byDesigner | e.g. chanel, gucci, hermes |
designerGender | select | byDesigner | Restrict to a department |
productUrls | array | byUrl | Full product URLs |
minPrice / maxPrice | int | all | USD |
condition | array (select) | all | As Is / Fair / Good / Very Good / Excellent / Pristine |
color | select | all | 19 curated colors |
size | string | all | Exact size match (case-insensitive), free text since the vocabulary is department-specific (e.g. M, 8.5, 6, 36mm) |
material | string | all | Substring 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. |
movementType | select | all | Watches only — Automatic / Quartz / Manual / Co-Axial Escapement Automatic / Spring Drive / SolarBeat™ |
caseMaterial | select | all | Watches only — Yellow Gold / Rose Gold / White Gold / Platinum / Silver / Stainless Steel / Stainless Steel and Gold / Titanium / Ceramic / Other |
onSaleOnly | bool | all | Only 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. |
sortBy | select | all | newest / priceAsc / priceDesc / mostObsessed |
maxItems | int | all | 1–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/waterSavedLitersfrombyUrlfetches to quantify the environmental impact of resale. - Catalog/taxonomy mapping — use
discoverCategoriesanddiscoverDesignersto build a complete, accurate picker UI without guessing slugs. - Authentication & provenance research — pull
authenticationInfoand 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:
- Tier 2 — curl_cffi with browser TLS impersonation. Requests to the homepage, category, and product pages with
chrome131,chrome124, andsafari184impersonation 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. - Tier 3 — headless Playwright with stealth. Chromium launched headless with
--disable-blink-features=AutomationControlledandplaywright_stealthapplied, 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. - 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; forbyDesigner, 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 GraphQLvariablescorrectly 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 echoedvariablesalone 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. Seeedges_match_category/edges_match_designerinsrc/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 (consistentpx-captchapayload, ~5.7 KB, via the reader-proxy) on 2 separate attempts a few minutes apart, whilecategoryPath=clothing/dressesand every other specific subcategory tested rendered reliably (including a clean 500/500-itemmaxItemsstress 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/clutchesall 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 barewomendepartment 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_messagereports 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 thepx-captchablock 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 ownbyCategoryoutput moments earlier) both through the actor itself and via a barecurlagainstr.jina.aioutside the actor entirely, ruling out an actor-code cause.byCategoryandbyDesignerlisting 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 abyUrlcall 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;byUrlshould currently be treated as best-effort like the Handbags category, not as reliable asbyCategory/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/discoverDesignersmodes — everybyCategory/byDesignerlisting fetch'sserverResult.data.productspayload already carries aaggregationsarray: this is the exact data that powers the site's own "Refine Results" sidebar (Categories, Designers, Condition, Color, sizes, etc.), previously read only foredges/pageInfoand never foraggregations. 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 realbyDesignerfetches:Acne Studios→acne-studios→ 9,095 items;3.1 Phillip Lim→3-1-phillip-lim→ 8,487 items — both real, matching counts, not fabricated.beautyadded to thegenderdepartment enum — discovered as an 8th top-level department in the sameaggregations"Categories" facet (not previously listed); confirmed live viashop/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
sortByOptionsfield 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 localsortByreorder. However, no URL query parameter (triedsortBy,sort,orderBy,sort_by,sortByValue, both upper/lower- cased values) was found that changessortByOptions.selectedaway fromNEWESTon 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).sortBytherefore remains a local post-fetch reorder of the already- fetched maxItems-worth of newest-first records, not a real upstream sort parameter, andSOLDwas not added (no reachable way to request it). - Combined category+designer browse query variable — investigated
whether a
where.booleans/where.bucketsGraphQL 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).discoverDesignersmode already lets a caller find the right slug to then run as a separatebyDesignerfetch (withdesignerGenderfor 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:
saleandnew-arrivalscuratedcategoryPathoptions — confirmed via reader-proxy fetch ofshop/women/sale(HTTP 200, 1.8 MB payload, 120 real product edges) andshop/women/new-arrivals/shop/new-arrivals(HTTP 200, ~1.8 MB, 120 real edges). These are reachable today through the existingcustomCategoryPathmechanism too; adding them to the curated dropdown just makes them discoverable without needing the free-text override.returnPolicyText—product.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 (SKUWEX127910) but not previously captured (only thereturnableboolean was).disclaimerText—product.disclaimers[].message(e.g. "Coupons and discounts are not available for this product."), confirmed present on the same real fetch.categoryLabel—product.category.label(e.g."Women's Fashion & Accessories"), a human-readable top-level category name distinct from the existingcategoryBreadcrumb.
Investigated and rejected:
- Seller/consignor info —
product.representativeandproduct.storeContactInfoare bothnullon every real product detail page fetched (SKUWEX127910dress, SKUCHA1467696Chanel 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 history —
product.promotionandproduct.valuationReportUrlarenullon every real fetch above. At the listing-GraphQL level,price.originalequaledprice.final(no diff) andprice.discountwasnullacross 240 sampled listing items spanning 2 categories (women/clothing/dresses,women/shoes/sneakers) and the dedicatedshop/women/salelisting (120 items, still 0 with a real discount). The existingoriginalPriceUsd/discountPercentfields 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 detail —
previously 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 owncondition-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 asconditionNotes.
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_cffiagainst both a category URL (shop/women/clothing/dresses) and a product URL, withchrome124,safari17_0, andchrome131impersonation 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'sdesignerGenderinput already defaults towomen(always populated inbuild_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.
| Round | Purpose | Run ID | Dataset ID | Items |
|---|---|---|---|---|
| 1 | Default prefill (byCategory / women / clothing/dresses) | 1j23jG5kIo0c5Hvkp | C9aMbbjdX9GJV6HxE | 20 |
| 2 | Non-default mode + filter (byDesigner chanel/women) | xNVT4I3vEPkd3Z7tn | 3n9hg3cJX3v9rhpU2 | 25 |
| 3 | URL accessibility (sampled from Round 1/2 datasets, see below) | – | – | – |
| 4a | Boundary: maxItems=1 | Gi5M8CgIjPsXhDPnp | bHoeezaNsDIaWq6tS | 1 |
| 4a | Boundary: maxItems=500 (department-wide) | 26j96IgeBtRXgyfMp | 1IxF8IsHe5u14axyu | 500 |
| 4a | All filters combined (price/condition/department) | F5JMHNq1vVGGkFIpF | vkW6PxPyQGWe8g1WO | 30 |
| 4b | Edge: inverted price range (minPrice=900 > maxPrice=10) | 6KwegVrSuuZq09dXt | qqnsgitJaoWQCZvcU | 0 (clean, no crash) |
| 4b | Edge: bogus designer slug (zzz-not-a-real-designer-xyz) | ngjZBB6bgGynrCTN1 | hQEF8O9fmKn3uZUaf | 0 (correctly rejected by content-based quality-check — see below) |
| 4b | Edge: unicode designer slug (シャネル) | VpganonuoGElO5E7T | G06Kc95P3zdQMLauT | 0 (clean, no UnicodeEncodeError) |
| 4b | Edge: byUrl with 1 malformed + 1 non-product + 1 valid URL | Pzzgl1KpTYlEj44VO | YYC6NjHUO1CMOXIL8 | 1 (invalid entries skipped, no crash) |
| 5 | Repeat run 1/3 (same input) | uQnsDuYg7H6C81qNI | Tq2beTlfYUiMPaePb | 25 |
| 5 | Repeat run 2/3 | 5jxjKNlnLz4WSoQHk | 3bRmL4x5yPirwlDyb | 25 |
| 5 | Repeat run 3/3 | G56NhWM4pAXaGgsCc | MBY3S2teqf1dRSaNo | 25 (0% variance across all 3) |
| 6 | Combined filters (price + color + sort) | 9F0Zanzs1YzZBPgUU | nwPC6Mb48b8TfCCwT | 30 |
| 6 | Multi-URL batch with 1 intentional duplicate | UzZ0szx78fCn1nNRP | d5hUpToTRf8a7sXvU | 3 (deduped from 4 input URLs) |
| 7a | International/non-default department (jewelry/rings) | abexA7u0UFsjVggjw | 8imF2ocjZyhiVbtmU | 15 |
| 7b | Pagination exact: maxItems=120 (= 1 page) | jPS7qreCyHCIkqVYi | LSJBfcy2I99kCveMT | 120 |
| 7b | Pagination exact: maxItems=130 (spans 2 pages) | mUeTNAGPUbCq5UwAj | JvH1pJYkzy67YwSaO | 130 |
| 7c | Long-running: maxItems=400, full department | tahlEWL3PfDK9b1yc | z9nX6C813Jt2VClJL | 400 (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.jpg→200 image/jpeghttps://product-images.therealreal.com/CHA1467696_1_enlarged.jpg→200 image/jpeghttps://product-images.therealreal.com/GUC2102612_1_enlarged.jpg(sampled from the Round 7c 400-item run) →200 image/jpegproductUrlfields (e.g..../products/women/shoes/boots/chanel-...) →403from a plaincurl, as documented above (PerimeterX; loads normally in a real browser).
Local test suite
python -m pytest tests/ -q → 320 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 cleancurl -ILSs, no Referer/cookies needed). productUrlvalues 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.