Etsy Scraper — listings, prices, shops, ratings
Pricing
Pay per event
Etsy Scraper — listings, prices, shops, ratings
Scrape Etsy listings past DataDome: price, shop, rating, review count, stock, materials and images. Search by keyword or scrape listing IDs directly. No browser. Powered by xhr.dev.
Pricing
Pay per event
Rating
0.0
(0)
Developer
xhrdev
Maintained by CommunityActor stats
0
Bookmarked
3
Total users
2
Monthly active users
3 days ago
Last modified
Categories
Share
Scrape Etsy listings past DataDome — price, shop, rating, review count, stock on hand, shipping origin, materials, category and the full image set. Search by keyword, pass listing IDs you already hold, or hand it listing and category URLs.
No browser is launched. The DataDome challenge is solved as plain HTTP by xhr.dev, so a page costs a few hundred milliseconds of compute instead of the several seconds and gigabyte of RAM a stealth browser needs.
Quickstart
Paste this into the input and press Run.
{"queries": ["leather wallet"],"scrapeDetails": true,"maxItems": 20}
Search is thin — this is Etsy, not the Actor
Etsy server-renders only about a dozen results per page and loads the rest in the browser as you scroll. There is no fix for that from an HTTP-only scraper, and any Actor claiming hundreds of results per search fetch is either driving a browser or counting differently.
So treat search as a way to discover listings, and raise maxPagesPerQuery to go wider rather than expecting depth from one page. scrapeDetails is on by default precisely because the listing page is where Etsy's good data lives.
Input
| Field | Type | Default | Description |
|---|---|---|---|
queries | string[] | ["leather wallet"] | Keywords to search. See the note on search depth below. |
itemIds | string[] | [] | Etsy listing IDs to fetch directly — the number in the URL, e.g. 1214703903. |
startUrls | array | [] | /listing/ URLs are read as products; /search, /c/ and /market/ URLs are read as listings. |
scrapeDetails | boolean | true | On, every listing found is opened for its full record. Off, you get only what search results show — cheaper, much thinner. See below. |
maxPagesPerQuery | integer | 1 | How deep to page each search, 1–250. |
maxItems | integer | 50 | Stop once this many listings have been scraped. |
proxyConfiguration | object | Apify RESIDENTIAL | Which proxy to route through. Leave it on residential — see Proxies. |
maxRetries | integer | 3 | Attempts per page before giving up on it, 1–5. Each attempt takes a fresh proxy session. |
timeoutSecs | integer | 120 | How long one page — challenge, solve and all — may take before the attempt fails. 10–300. |
maxConcurrency | integer | 4 | Pages worked on at once, 1–10. |
Nothing is required — the defaults run a real search out of the box.
Output — with scrapeDetails: true (the default)
One dataset item per listing, from the listing page itself.
| Field | Type | Description |
|---|---|---|
id | string | Etsy listing ID. |
title | string | Listing title. |
url | string | Canonical listing URL, tracking parameters stripped. |
shop | string | Shop name. |
price | number | Current price. |
currency | string | ISO currency code, e.g. USD. |
availability | string | InStock, OutOfStock, etc. |
quantityAvailable | number | Units the shop has on hand. |
shipsFrom | string | Country code the item ships from. |
rating | number | Average rating out of 5. |
reviewCount | number | Number of reviews. |
category | string | Full category path, e.g. Bags & Purses < Wallets & Money Clips < Wallets. |
material | string | Primary material where the shop set one. |
description | string | Full listing description. |
images | string[] | Full-size image URLs. |
Output — with scrapeDetails: false
One item per search result card. No listing page is fetched, so this is billed at the cheaper listing rate.
| Field | Type | Description |
|---|---|---|
id | string | Etsy listing ID. |
title | string | Listing title. |
url | string | Canonical listing URL. |
price | number | Price as shown on the card. |
currency | string | Currency symbol as rendered, e.g. $ — the card does not carry an ISO code. |
image | string | Thumbnail URL (255px), not the full-size image. |
shopId | string | Numeric shop ID. The shop name is only on the listing page. |
rating | number | Average rating, when the card shows one. |
reviewCount | number | Review count, when the card shows one. |
Example record
Taken verbatim from a live run. Long text and image lists are trimmed here for readability; the real record carries them in full.
{"id": "4461240474","title": "Personalized Leather Cash Wallet for Men, Full Grain Slim Front Pocket Card Holder, Handmade Minimalist Wallet, Gift for him","url": "https://www.etsy.com/listing/4461240474/leather-cash-wallet-slim-front-pocket","shop": "AmericanLeatherGift","price": 37.79,"currency": "USD","availability": "InStock","quantityAvailable": 359,"shipsFrom": "US","rating": 4.8,"reviewCount": 56,"category": "Bags & Purses < Wallets & Money Clips < Wallets","material": "Leather","description": "Personalized Leather Cash Wallet for Men Upgrade everyday carry with this personalized wallet crafted from premium full grain leather. Designed for modern simplicity, this leather cash wallet offers a refined alternative to bulky traditional wallets. Compact … (truncated here for readability)","images": ["https://i.etsystatic.com/41443453/r/il/b11a3f/7994642756/il_fullxfull.7994642756_ld98.jpg","https://i.etsystatic.com/41443453/r/il/deb604/7782209464/il_fullxfull.7782209464_1lsp.jpg","https://i.etsystatic.com/41443453/r/il/3ccf3a/7830135935/il_fullxfull.7830135935_4qh6.jpg","… 17 more"]}
A row from the same search with scrapeDetails: false
{"id": "4544756667","title": "Green Alligator Wallet - Handmade Luxury Bifold Wallet with Red Python Leather Interior - Exotic Leather Wallet for Men - Personalized Gift","url": "https://www.etsy.com/listing/4544756667/green-alligator-wallet-handmade-luxury","price": 570,"currency": "$","image": "https://i.etsystatic.com/13350861/r/il/97feff/8302896792/il_255x319.8302896792_4bxf.jpg","shopId": "13350861","rating": 4.8,"reviewCount": 470}
Pricing
Two rates, and which one you pay depends on scrapeDetails:
| Event | Price | When |
|---|---|---|
product-detail | $15.00 / 1,000 ($0.015 each) | A listing page was fetched and parsed — scrapeDetails: true. |
listing-item | $3.00 / 1,000 ($0.003 each) | A search-result row only — scrapeDetails: false. |
Charged on delivered rows only; failed pages cost nothing. The five-fold difference is real work: detail mode fetches one page per listing, listing mode gets ~12 rows from a single fetch.
Use listing mode to survey, detail mode to collect. A price-tracking job that already holds the IDs should pass itemIds and skip search entirely.
Proxies: read this before changing anything
Set this to Apify RESIDENTIAL and leave it there. It is not a style preference, and it is the single most common reason a run comes back empty.
DataDome decides which challenge to serve based on the exit IP. We measured this from Apify against twelve DataDome-protected sites, same code, minutes apart:
| Exit IP | Challenge served | Result |
|---|---|---|
| Datacenter (Apify default, or no proxy) | The hard captcha | 3 of 12 sites passed |
| Residential | Interstitial, or no challenge at all | 12 of 12 sites passed, 2–5s each |
A datacenter address does not make Etsy slower. It changes the problem into a different, much harder one. Every attempt already pins a fresh residential session automatically, so a burnt exit node gets a genuinely new IP rather than a retry down the same dead pipe.
If a run fails wholesale, check the proxy group before anything else.
Run it from the API or CLI
Replace <TOKEN> with your Apify API token.
Run and get the results back in one call:
curl -X POST "https://api.apify.com/v2/acts/xhrdev~etsy-scraper/run-sync-get-dataset-items?token=<TOKEN>" \-H "Content-Type: application/json" \-d '{"queries":["leather wallet"],"scrapeDetails":true,"maxItems":20}'
Start a run without waiting:
curl -X POST "https://api.apify.com/v2/acts/xhrdev~etsy-scraper/runs?token=<TOKEN>" \-H "Content-Type: application/json" \-d '{"queries":["leather wallet"],"scrapeDetails":true,"maxItems":20}'
With the Apify CLI:
$apify call xhrdev/etsy-scraper --input '{"queries":["leather wallet"],"scrapeDetails":true,"maxItems":20}'
Fetching results
Every run writes to a dataset. Change format for JSON, CSV, or Excel:
# JSONcurl "https://api.apify.com/v2/datasets/<DATASET_ID>/items?token=<TOKEN>&clean=true&format=json"# CSVcurl "https://api.apify.com/v2/datasets/<DATASET_ID>/items?token=<TOKEN>&clean=true&format=csv"# Page through a large datasetcurl "https://api.apify.com/v2/datasets/<DATASET_ID>/items?token=<TOKEN>&offset=1000&limit=1000"
<DATASET_ID> comes back as defaultDatasetId on the run object.
Limits and failure behaviour
100 pages per run, hard. Not configurable. It is a guard against a typo in maxPagesPerQuery turning into a bill and a hammering of Etsy. For more than that, split the work across runs.
Failures never enter the dataset. A page that could not be fetched is not written as a half-empty row — that would corrupt the clean table you are paying for. Instead every run writes a RUN_SUMMARY record to the key-value store:
{"site": "etsy","itemsScraped": 12,"pagesFetched": 13,"pagesFailed": 0,"failures": [],"solverHost": "https://trial.xhr.dev"}
Read it at https://api.apify.com/v2/key-value-stores/<STORE_ID>/records/RUN_SUMMARY. failures holds up to 50 entries, each with the URL and why it failed.
A run that scrapes nothing and failed at least one page exits as failed, rather than reporting success over an empty dataset. If everything failed, the message points at the proxy group first, because that is nearly always the cause.
Pagination is never speculative. Page 2 is queued only after page 1 comes back and the site confirms how many pages exist, so you are not billed for fetching past the end of a short result set.
Spend cap. Set maxTotalChargeUsd on the run. The Actor stops fetching once it is reached, mid-run, rather than overshooting.
Questions
A run came back empty. Why?
Check the proxy group first — a datacenter exit is the cause the overwhelming majority of the time. Then read RUN_SUMMARY in the key-value store for the per-page reasons.
Is this affiliated with Etsy? No. This is an independent tool with no affiliation with, endorsement by, or connection to Etsy or any bot-protection vendor. Names and trademarks belong to their owners.
How does it get past the challenge without a browser? It is solved as HTTP, by xhr.dev. No Chrome is launched, which is why a page costs a few hundred milliseconds of compute instead of the several seconds and ~1 GB of RAM a stealth browser needs.
Can I use the solver directly, on a site that isn't Etsy? Yes — that is DataDome Unblocker, which takes any URL and hands back the HTML and the clearance cookies. There is also Akamai Unblocker for Akamai Bot Manager.
Can I run this on my own infrastructure? Yes. The solver these Actors call is a self-hosted Docker container, sold on a flat fee with no per-request pricing. See xhr.dev.
Why is currency a $ symbol in listing mode but USD in detail mode?
Because that is what each page provides. The search card renders a symbol; the listing page carries a proper ISO code in its structured data. Nothing is normalised or guessed.
Related Actors
- DataDome Unblocker — any URL behind DataDome, returns HTML plus clearance cookies.
- Akamai Unblocker — the same, for Akamai Bot Manager.
- leboncoin Scraper — French classified ads.
- Anthropologie Scraper — apparel and homeware.
- Grainger Scraper — industrial supply lookups.
Built by xhr.dev. Independent tool, not affiliated with Etsy or any bot-protection vendor. Scrape only what you are permitted to access, and check the site's terms before you run anything at scale.