ThredUp Scraper — Resale Comps & Thrift Data avatar

ThredUp Scraper — Resale Comps & Thrift Data

Pricing

from $1.20 / 1,000 results

Go to Apify Store
ThredUp Scraper — Resale Comps & Thrift Data

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

Emmanuel

Maintained by Community

Actor 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 resellersListing Search with Listed within (days) + webhooks
Price your own inventory against the live marketListing Search + Sold Comps
Track a specific listing's full spec sheetListing Details
Monitor a partner shop's entire inventorySeller Shop Listings
Vet a shop before you buy or partnerSeller Profile
Collect any ThredUp page you already have a link forScrape By URL
Get Slack/Discord deal alertsWebhook URL
Ask an AI agent for compsApify 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)

  1. Keep Listing Search enabled (it is on by default).
  2. Keywords are already prefilled: carhartt detroit jacket, vintage 90s levis 501, patagonia fleece.
  3. Click Start.

You get one row per listing — title, brand, size, condition, prices, discount, save count, images, and more — as it lands.


Features

FeatureCheckboxWhat it returns
Listing SearchenableListingSearch (default on)One row per matching listing, streamed live, with keyword, rank, and total market depth.
Listing DetailsenableListingDetailsFull records for specific item numbers or product URLs: description, fabric, care, features, pattern, measurements, MPN, and full image gallery.
Seller Shop ListingsenableClosetListingsOne 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 ProfileenableSellerProfileOne row per shop: shop type, live inventory depth, and completed-listing depth.
Sold CompsenableSoldHistoryOne row per completed listing where the marketplace publishes it — sold price, retail baseline, and discount from retail.
Scrape By URLenableScrapeByUrlPaste 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

FieldTypeDefaultNotes
countryUS | UKUSMarket region; also drives the recommended proxy country.
FieldTypeDefaultNotes
enableListingSearchbooleantrueLive keyword search.
searchKeywordsstring list3 prefilled thrift queriesOne keyword per entry, processed in parallel.
searchMaxResultsinteger 1–100010Per keyword. Total rows = keywords × this number, bounded by the run limit.
searchSortenumrelevancerelevance, newest_first, price_low_high, price_high_low, marked_down_at_desc, recommended — these are the marketplace's own sort options.
searchDepartmentstring–e.g. women, men, kids, home.
searchBrandstring–e.g. Carhartt.
searchConditionstring–As the marketplace labels it: excellent, good, fair.
searchMinPrice / searchMaxPricenumber–Price band in USD.
searchListedWithinDaysinteger 1–365–Only recently listed inventory — ideal for fresh-deal alerts.
searchFetchFullDetailsbooleanfalseMerge full details into the same row.

Grading labels — tag listings, never filter them

FieldTypeDefaultNotes
labelLuxuryBrandsbooleanfalseTag each listing with the marketplace's luxury brand grouping. Nothing is removed.
labelMinDiscountPercentinteger 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

FieldTypeDefaultNotes
maxItemsinteger 0–1,000,0000Total 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 1000
Narrowing 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

  1. Leave all narrowing filters empty — they are for deal hunting, not for bulk comps.
  2. Give one or two broad keywords (jacket, jeans, sweater) and set Max results per keyword to the number you want.
  3. 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

FieldTypeNotes
listingIdsstring listThredUp item numbers, e.g. 1500810136.
listingUrlsstring listFull product URLs.

Seller shops (shared by three features)

FieldTypeDefaultNotes
sellerIdsstring list–Seller shop IDs. Shared by Seller Shop Listings, Seller Profile, and Sold Comps.
sellerMaxListingsinteger 1–20030Live listings per shop.
soldMaxItemsinteger 1–20030Completed listings per shop.

Scrape By URL

FieldTypeNotes
scrapeUrlsstring listAny ThredUp URL. URLs from other sites are rejected with a clear error.

Supported URL shapes

https://www.thredup.com/women?search_text=carhartt%20detroit%20jacket
https://www.thredup.com/women/carhartt
https://www.thredup.com/product/women-carhartt-jacket/1500810136
https://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

FieldTypeDefaultNotes
webhookUrlstring–Every record is POSTed here right after it is saved. Delivery never slows collection.
webhookFormatjson | slackjsonjson = full record; slack = ready-to-read message.
proxyConfigurationobjectApify ResidentialResidential 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)

FieldDescription
featureTypeWhich feature produced this row.
scrapedAtISO-8601 write time.
urlSource URL.
item_idThredUp item number — reuse it with Listing Details.
item_urlDirect product link.
title, descriptionListing copy.
main_image_url, additional_image_urlsFull gallery.
statusavailable, sold, …
current_priceLive selling price (USD).
original_priceThe listing's stated original price.
retail_priceList price / MSRP.
savings_amountRetail minus current price.
discount_percentagePercent off retail as published. Negative means priced above retail.
currencyUSD.
brand, brand_idBrand identity.
size, size_scaleSize plus the size system (ALPHA, NUMERIC).
condition, quality_code, quality_type, condition_descriptionCondition as published.
category, category_tags, department, department_tagsTaxonomy.
color, colors, style_tags, materialAttributes.
likes_countShopper saves — a live demand signal.
is_soldCompletion flag.
seller_id, seller_type, seller_on_vacation, seller_covered_shipping, seller_shipping_costSeller identity when the listing belongs to a marketplace shop.
shipping_cost, free_shippingShipping economics.
detailsFetchedtrue once full details were merged in.

Feature-specific fields

Field groupFieldsWhere
Search contextsearch_keyword, position, total_results_availablelisting_search
Detail enrichmentfabric, care_instructions, features, pattern, measurements, measurements_display, mpn, mpn_title, size_detailed, photo_countlisting_details, and enriched search rows
Shop inventoryseller_shop_id, shop_total_listingscloset_listings
Shop profileseller_type, shop_total_listings, shop_total_sold, average_rating, ratings_count, is_marketplace_shopseller_profile
Compssold_price, discount_from_retail_percentage, shop_total_soldsold_history
URL runspageType, item_idscrape_by_url
Grading labelsbrand_tier, is_luxury_brand, meets_discount_targetlisting_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: Good
https://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 block mode 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 install
cp .env.example .env # add your proxy settings
cp local.input.example.json local.input.json
npm run start:local # streams to output/local_results.jsonl
npm run typecheck
npm run build

License

ISC.