ThredUp Scraper — Resale Comps & Thrift Data
Pricing
from $1.20 / 1,000 results
ThredUp Scraper — Resale Comps & Thrift Data
Live ThredUp resale intelligence: search listings by keyword for price, brand, size, condition, discount and demand signals, plus full details, seller shop inventory and sold comps. Luxury finds are tagged, never filtered, so 1,000 records stay 1,000. Slack webhooks. Free trial: 2 results.
Pricing
from $1.20 / 1,000 results
Rating
0.0
(0)
Developer
Emmanuel
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
ThredUp Real-Time Data
Live ThredUp resale intelligence — search listings by keyword for price, brand, size, condition, discount and demand signals, plus full details, seller shop inventory and completed-listing comps. Luxury finds are tagged, never filtered, so 1,000 records stay 1,000. Every record streams to your dataset the moment it is collected, in clean, structured JSON.
Free trial: on a free Apify plan, runs are capped at 2 results so you can verify the data quality. Upgrade to a paid plan for unlimited exports.
What you can do with it
ThredUp is one of the deepest secondhand catalogs online — millions of pre-owned and new-with-tags items across brands from Carhartt and Patagonia to Farm Rio and Free People. This actor turns that catalog into structured resale intelligence you can act on.
| You want to… | Use this |
|---|---|
| Find undervalued inventory (vintage single-stitch, designer denim, gorpcore, Y2K) | Listing Search with a minimum discount filter |
| Catch fresh drops before other resellers | Listing Search with Listed within (days) + webhooks |
| Price your own inventory against the live market | Listing Search + Sold Comps |
| Track a specific listing's full spec sheet | Listing Details |
| Monitor a partner shop's entire inventory | Seller Shop Listings |
| Vet a shop before you buy or partner | Seller Profile |
| Collect any ThredUp page you already have a link for | Scrape By URL |
| Get Slack/Discord deal alerts | Webhook URL |
| Ask an AI agent for comps | Apify MCP |
Who it's for: vintage & thrift resellers (cross-listing on Poshmark, eBay, Depop, Mercari, Grailed), consignment shops, resale arbitrageurs, pricing analysts, fashion researchers, and AI agents that need live resale data.
Quick start (10 results in seconds)
- Keep Listing Search enabled (it is on by default).
- Keywords are already prefilled:
carhartt detroit jacket,vintage 90s levis 501,patagonia fleece. - Click Start.
You get one row per listing — title, brand, size, condition, prices, discount, save count, images, and more — as it lands.
Features
| Feature | Checkbox | What it returns |
|---|---|---|
| Listing Search | enableListingSearch (default on) | One row per matching listing, streamed live, with keyword, rank, and total market depth. |
| Listing Details | enableListingDetails | Full records for specific item numbers or product URLs: description, fabric, care, features, pattern, measurements, MPN, and full image gallery. |
| Seller Shop Listings | enableClosetListings | One row per live item in a seller shop. (ThredUp is shop-based rather than closet-based, so this uses shop IDs — featureType stays closet_listings for cross-platform compatibility.) |
| Seller Profile | enableSellerProfile | One row per shop: shop type, live inventory depth, and completed-listing depth. |
| Sold Comps | enableSoldHistory | One row per completed listing where the marketplace publishes it — sold price, retail baseline, and discount from retail. |
| Scrape By URL | enableScrapeByUrl | Paste any ThredUp search, department, brand, shop, or product URL; the page type is detected automatically. |
Enrichment happens in place — no duplicate rows
Enrich with full listing details (searchFetchFullDetails) merges the full record into the same listing_search row and sets detailsFetched: true. You never get two rows for one listing, and partial items are never dropped — every discovered item reaches your dataset.
Grading labels tag rows — they never remove them
Luxury brand tier labels (labelLuxuryBrands) and the discount target (labelMinDiscountPercent) grade each listing instead of filtering it: a 1,000-row request stays 1,000 records, with brand_tier, is_luxury_brand, and meets_discount_target filled in for the segment you care about. Nothing is filtered for missing a tier, so output volume — and therefore per-1,000-record pricing — stays predictable. Details in Grading labels.
Input reference
Run settings
| Field | Type | Default | Notes |
|---|---|---|---|
country | US | UK | US | Market region; also drives the recommended proxy country. |
Listing Search
| Field | Type | Default | Notes |
|---|---|---|---|
enableListingSearch | boolean | true | Live keyword search. |
searchKeywords | string list | 3 prefilled thrift queries | One keyword per entry, processed in parallel. |
searchMaxResults | integer 1–1000 | 10 | Per keyword. Total rows = keywords × this number, bounded by the run limit. |
searchSort | enum | relevance | relevance, newest_first, price_low_high, price_high_low, marked_down_at_desc, recommended — these are the marketplace's own sort options. |
searchDepartment | string | – | e.g. women, men, kids, home. |
searchBrand | string | – | e.g. Carhartt. |
searchCondition | string | – | As the marketplace labels it: excellent, good, fair. |
searchMinPrice / searchMaxPrice | number | – | Price band in USD. |
searchListedWithinDays | integer 1–365 | – | Only recently listed inventory — ideal for fresh-deal alerts. |
searchFetchFullDetails | boolean | false | Merge full details into the same row. |
Grading labels — tag listings, never filter them
| Field | Type | Default | Notes |
|---|---|---|---|
labelLuxuryBrands | boolean | false | Tag each listing with the marketplace's luxury brand grouping. Nothing is removed. |
labelMinDiscountPercent | integer 1–95 | – | Savings-off-retail target to grade against; sets meets_discount_target on every row. |
Labels grade what you collected instead of deciding what you collect. Every discovered listing is written to the dataset in full, matching or not matching the label, so a labeled run returns exactly as many rows as an unlabeled one — the record count stays priceable, and there are no duplicate rows. Filter on the label fields afterwards, in your own tooling, to isolate the segment you care about.
Cost: a little extra time per keyword to resolve the tags — bounded by how many rows the run can export, so a small run stays quick and a capped run never pays for tags it will not write. The row count never changes. If tags cannot be resolved for a keyword, the run continues and the label fields are simply left blank — listings are still saved.
Run limits
| Field | Type | Default | Notes |
|---|---|---|---|
maxItems | integer 0–1,000,000 | 0 | Total rows for the whole run, across every enabled feature. 0 = no run limit. |
Narrowing filters — they define the search, and the run tells you the pool size first
Brand, department, condition, price band, and freshness narrow which inventory you are shopping. Every listing that matches is still written in full — nothing is dropped for being incomplete — but a narrower pool caps how many rows can exist. So the run measures that pool before it collects anything:
Projected output: 1000 record(s) — 1 keyword(s) x up to 1000, bounded by the run limit and by how many listings actually exist"jacket": 10,001 listing(s) available, collecting 1000Narrowing active (Brand / designer) — these reduce how many listings exist, so your output is capped by the availability above
If the pool is smaller than your target, you are told in plain numbers ("jacket": only 40 listing(s) match the current filters (you asked for 1000)) and the run collects everything that matches rather than silently under-delivering. The same numbers land in the run OUTPUT (projectedRecords, keywordPools), so a caller can price the run before paying for it.
Measured on the single keyword jacket against the live marketplace, for orientation: no filters 10,001 · listed within 1 day ≈1,700 · brand or department scoped — varies by brand. Anything that cannot be planned is reported up front, never discovered at the end.
Bulk price research: how to reliably land ~1000 rows
- Leave all narrowing filters empty — they are for deal hunting, not for bulk comps.
- Give one or two broad keywords (
jacket,jeans,sweater) and set Max results per keyword to the number you want. - Optionally set Run limit so a multi-keyword run cannot overshoot.
Measured locally with filters off: 300 rows in 7.1s, 1000 rows in 20.3s, zero duplicates. Paging is offset-stable, and repeated listings across page boundaries are skipped, so a request for 1000 unique rows delivers 1000 unique rows rather than “1000 attempted”. If the pool genuinely runs out first, the log says so instead of silently returning less.
Targeting a luxury segment without breaking that math
Luxury inventory is roughly one listing in ten for a broad keyword, and the share changes per keyword. So the actor reports it instead of guessing:
"jacket": 968 of 10,001 matching listing(s) are in the luxury group — they are tagged, not filtered out"jacket": 412 of 1000 saved listing(s) carry the luxury tag
With Luxury brand tier labels on, a 1000-row run still returns 1000 rows; the share line tells you exactly how many carry the tag, and OUTPUT.luxuryTagged / OUTPUT.luxuryShare report the same thing to a calling system. To land ~1000 luxury rows, divide by the reported share — e.g. a 9.7% share means asking for ≈10,300 rows — which is a number you can read before the run finishes instead of a number you have to guess.
Filters this marketplace does not expose
ThredUp does not offer a per-size filter on its search surface, and this actor does not fake one — size is returned on every row, so filter the dataset instead. Condition is a free-text filter rather than a fixed dropdown, matching how the marketplace itself accepts it. Everything else above is a real, working filter.
Listing Details
| Field | Type | Notes |
|---|---|---|
listingIds | string list | ThredUp item numbers, e.g. 1500810136. |
listingUrls | string list | Full product URLs. |
Seller shops (shared by three features)
| Field | Type | Default | Notes |
|---|---|---|---|
sellerIds | string list | – | Seller shop IDs. Shared by Seller Shop Listings, Seller Profile, and Sold Comps. |
sellerMaxListings | integer 1–200 | 30 | Live listings per shop. |
soldMaxItems | integer 1–200 | 30 | Completed listings per shop. |
Scrape By URL
| Field | Type | Notes |
|---|---|---|
scrapeUrls | string list | Any ThredUp URL. URLs from other sites are rejected with a clear error. |
Supported URL shapes
https://www.thredup.com/women?search_text=carhartt%20detroit%20jackethttps://www.thredup.com/women/carhartthttps://www.thredup.com/product/women-carhartt-jacket/1500810136https://www.thredup.com/shop/<shop-id>
Search URLs are read exactly as the marketplace writes them — including department_tags, brand_name_tags, price[min], price[max], condition, clearance, luxe_brand, user_promotion_discount_percent, and listed_days — so any link you copy from a ThredUp browsing session works as-is.
Alerts
| Field | Type | Default | Notes |
|---|---|---|---|
webhookUrl | string | – | Every record is POSTed here right after it is saved. Delivery never slows collection. |
webhookFormat | json | slack | json | json = full record; slack = ready-to-read message. |
proxyConfiguration | object | Apify Residential | Residential proxy is recommended for stable runs and correct regional pricing. |
Output reference
One dataset row per item, streamed as it is collected. featureType tells you which feature produced the row: listing_search, listing_details, closet_listings, seller_profile, sold_history, or scrape_by_url.
Shared core (every listing-like row)
| Field | Description |
|---|---|
featureType | Which feature produced this row. |
scrapedAt | ISO-8601 write time. |
url | Source URL. |
item_id | ThredUp item number — reuse it with Listing Details. |
item_url | Direct product link. |
title, description | Listing copy. |
main_image_url, additional_image_urls | Full gallery. |
status | available, sold, … |
current_price | Live selling price (USD). |
original_price | The listing's stated original price. |
retail_price | List price / MSRP. |
savings_amount | Retail minus current price. |
discount_percentage | Percent off retail as published. Negative means priced above retail. |
currency | USD. |
brand, brand_id | Brand identity. |
size, size_scale | Size plus the size system (ALPHA, NUMERIC). |
condition, quality_code, quality_type, condition_description | Condition as published. |
category, category_tags, department, department_tags | Taxonomy. |
color, colors, style_tags, material | Attributes. |
likes_count | Shopper saves — a live demand signal. |
is_sold | Completion flag. |
seller_id, seller_type, seller_on_vacation, seller_covered_shipping, seller_shipping_cost | Seller identity when the listing belongs to a marketplace shop. |
shipping_cost, free_shipping | Shipping economics. |
detailsFetched | true once full details were merged in. |
Feature-specific fields
| Field group | Fields | Where |
|---|---|---|
| Search context | search_keyword, position, total_results_available | listing_search |
| Detail enrichment | fabric, care_instructions, features, pattern, measurements, measurements_display, mpn, mpn_title, size_detailed, photo_count | listing_details, and enriched search rows |
| Shop inventory | seller_shop_id, shop_total_listings | closet_listings |
| Shop profile | seller_type, shop_total_listings, shop_total_sold, average_rating, ratings_count, is_marketplace_shop | seller_profile |
| Comps | sold_price, discount_from_retail_percentage, shop_total_sold | sold_history |
| URL runs | pageType, item_id | scrape_by_url |
| Grading labels | brand_tier, is_luxury_brand, meets_discount_target | listing_search (present only when the matching label is switched on) |
Runs also write an OUTPUT summary: totalPushed, spendingLimitReached, a paywall object describing the tier and whether a cap was applied, the volume projection (projectedRecords, keywordPools with each keyword's available and luxuryAvailable), the narrowing and label controls that were active (narrowingActive, labelsActive), and the label results (luxuryTagged, luxuryShare, discountTargets).
Price field semantics: current_price is the live price a shopper pays, original_price is the item's stated original price, and retail_price is the list price. discount_percentage is computed against retail_price, so it is directly comparable with comps from other marketplaces.
Deal alerts with webhooks
Add a Slack (or Discord) incoming-webhook URL as webhookUrl, set webhookFormat to slack, and every fresh listing shows up as a message the moment it is collected — with title, brand, price, discount, and a direct link. Webhook delivery is fire-and-forget, so alerting never slows the run, and a failed delivery never costs you a dataset row.
Example Slack card:
🛍️ Carhartt Detroit Jacket — $41.99 (48% off $80 retail)Brand: Carhartt · Size: M · Condition: Goodhttps://www.thredup.com/product/...
For JSON consumers you get the full record — ideal for pricing bots, cross-listing tools, and your own dashboards.
AI agents & MCP
Connect the Apify MCP server and ask questions in plain language:
- "What is the average price of a 90s Carhartt J97 jacket right now on ThredUp?"
- "Find Patagonia fleece listings under $40 that are at least 50% off retail."
- "How many Carhartt jackets are listed and how deep is that market?"
Because every row carries brand, size, condition, current_price, retail_price, discount_percentage, and likes_count, an agent can compute price bands, discount distributions, and demand signals without extra work. Prefer the Dataset views (overview, search, details, closet_listings, seller_profile, sold_history, scrape_by_url) for clean, focused tables.
Pricing, free tier, and limits
- Billing: pay-per-event, charged per result written (
result). You only pay for rows you actually receive. - Spending limits: the actor respects your Apify spending limit. When it is reached the run stops cleanly, writes its summary, and exits gracefully — no error, no partial garbage.
- Free plan: capped at 2 results per run, then the run stops with a clear upgrade message. A hard
blockmode is available to account owners for validation-only runs. - Memory & runtime: 512 MB default with a 10,000-second timeout; memory stays flat during 10,000+ item runs because rows stream to the dataset instead of buffering.
FAQ
How fresh is the data? Every run reads live marketplace inventory at run time — there is no stale cache.
Do I need a proxy?
On Apify, residential proxy is configured by default and recommended. Locally, set your proxy in .env (see .env.example).
Why did a keyword return no rows? The narrowing filters were too tight (a rare brand combined with a narrow price band and a short freshness window). Loosen one filter at a time. Note that grading labels never cause this — turning on luxury tagging cannot reduce your row count.
Why is some optional field empty? Some listings genuinely publish no fabric, measurements, or condition notes. The actor never invents values — empty means the marketplace did not publish it.
Can I get one row per listing with everything in it?
Yes: enable Enrich with full listing details and you get the search row plus the full record in a single row (detailsFetched: true).
Where do seller shop IDs come from? From ThredUp partner shop links you already have. The actor never guesses shop IDs: if a value does not resolve to a shop, that feature logs it clearly and the rest of the run continues normally.
How do I keep dataset size and cost predictable?
Use searchMaxResults, sellerMaxListings, soldMaxItems, and your Apify spending limit. Labels never change the row count, narrowing filters report their pool size up front in the log and in OUTPUT.projectedRecords, and rows stream as they are found — so a spending cutoff never loses what you already paid for.
Local development
npm installcp .env.example .env # add your proxy settingscp local.input.example.json local.input.jsonnpm run start:local # streams to output/local_results.jsonl
npm run typechecknpm run build
License
ISC.