Search Etsy consumer listings for any caller-provided product query and optional filters, returning structured listing cards with listing ID, title, shop, canonical URL, current and original price, rating, review count, badges, shipping indicators, image, and sponsored-ad status. Optionally collect multiple result pages, deduplicate them, enforce strict numeric price bounds, apply semantic title filters, and verify destination shipping or explicitly displayed item condition on listing pages. This is read-only.
Use Cases
Use for requests to find Etsy products by keyword, including price-bounded searches, color or category facets, shipping-country filters, sorting, rating constraints, semantic material/title filters, and multi-page collection. For fixed-count requests, continue through page={n} URLs until enough qualifying unique listings are collected or pages are exhausted. Use listing-detail verification when the caller requires a property that search cards do not reliably expose, especially destination-specific shipping or condition. Never add items to carts, favorite listings, sign in, or initiate purchases.
Automation Flow
- Run the complete flow in one
browserless_agentsession with a US residential proxy so DataDome clearance persists:
{"proxy":{"proxy":"residential","proxyCountry":"us"},"commands":[...]}- Warm up and clear DataDome in the same session:
{"method":"goto","params":{"url":"https://www.etsy.com/","waitUntil":"load","timeout":45000}},
{"method":"solve","params":{"type":"dataDome"}},
{"method":"waitForTimeout","params":{"time":7000}}Dismiss a consent dialog only when present, commonly button[aria-label="Ok"].
3. Navigate directly to the filtered search URL in the same session:
https://www.etsy.com/search?q={url-encoded-query}
Append only requested parameters. Supported modifiers include order=most_relevant|most_recent|price_asc|price_desc|highest_reviews, min={min-dollars}, max={max-dollars}, is_handmade=true, is_vintage=true, is_supply=true, instant_download=true|false, free_shipping=true, ship_to={ISO-country-code}, is_discounted=true, customizable=true, and is_personalizable=true. For “under” constraints use max={limit} but enforce strict < limit in the evaluator.
4. For color or other dynamic facets, inspect the loaded facet links and reuse their complete href parameters; never guess opaque IDs. The observed purple facet used explicit=1&attr_1=8, but this is only a current-site example and must be revalidated from the live facet link. Add the resulting parameters to the direct search URL. ship_to=IS is the Iceland country filter.
5. For multiple pages, navigate to the same filtered URL with &page={page-number} and combine outputs in memory, deduplicating by listing_id. Do not assume one page contains all qualifying listings.
6. Set minPrice, maxPrice, inclusiveMax, minRating, and the title filters from the request; leave bounds and filters null when not requested. Run this evaluator once on each loaded results page:
(() => {
const minPrice=null,maxPrice=null,inclusiveMax=false,minRating=null;
const titleInclude=null,titleMaterial=null,titleExclude=null;
const clean=x=>x?.replace(/\s+/g,' ').trim()||null;
const money=t=>{if(!t)return null;const m=t.replace(/\s/g,'').match(/([$€£])?([\d.,]+)/);if(!m)return null;let n=m[2];if(n.includes(',')&&n.includes('.'))n=n.replace(/,/g,'');else if(n.includes(','))n=n.replace(',','.');const value=Number(n);return Number.isFinite(value)?{currency:m[1]||null,value}:null};
const seen=new Set(),listings=[];
for(const root of document.querySelectorAll('[data-listing-id],li,article,[role="listitem"]')){
const id=root.getAttribute('data-listing-id')||root.querySelector('[data-listing-id]')?.getAttribute('data-listing-id');
if(!id||seen.has(id))continue;
const link=root.querySelector('a[href*="/listing/"]');if(!link)continue;
const title=link.getAttribute('aria-label')||clean(root.querySelector('h3,h2,[data-listing-title]')?.textContent)||clean(link.textContent)||'';
if(titleInclude&&!titleInclude.test(title))continue;if(titleMaterial&&!titleMaterial.test(title))continue;if(titleExclude&&titleExclude.test(title))continue;
const node=root.querySelector('[data-price],.currency-value,p[class*="price" i],[aria-label*="price" i]');
const priceText=clean(node?.getAttribute('aria-label')||node?.textContent)||clean(root.innerText)?.match(/[$€£]\s*[\d.,]+/)?.[0]||null;const parsed=money(priceText);
if(minPrice!=null&&(!parsed||parsed.value<minPrice))continue;
if(maxPrice!=null&&(!parsed||(inclusiveMax?parsed.value>maxPrice:parsed.value>=maxPrice)))continue;
const star=root.querySelector('clg-static-review-stars[rating]');const label=[...root.querySelectorAll('[aria-label]')].map(x=>x.getAttribute('aria-label')).find(x=>/star rating/i.test(x))||null;
const rating=star?.getAttribute('rating')?Number(star.getAttribute('rating')):label?Number((label.match(/([0-9]+(?:\.[0-9]+)?)/)||[])[1]):null;
if(minRating!=null&&(rating==null||rating<minRating))continue;
seen.add(id);const original=root.querySelector('[class*="original" i],[aria-label*="original price" i]');
listings.push({listing_id:id,title,shop_name:clean(root.querySelector('[data-shop-name],.v2-listing-card__shop,[class*="shop-name"]')?.textContent),listing_url:link.href.split('?')[0],price_formatted:priceText,price_raw:parsed?.value??null,currency:parsed?.currency??null,original_price_formatted:clean(original?.textContent||original?.getAttribute('aria-label')),rating,review_count:star?.getAttribute('review-count-text')||(label?.match(/with ([\d.,kK]+) reviews/i)||[])[1]||null,badges:['Bestseller',"Etsy's Pick",'Star Seller'].filter(b=>root.textContent.includes(b)),is_ad:/Ad from shop/i.test(root.textContent),free_shipping:/Free shipping/i.test(root.textContent),image_url:root.querySelector('img')?.src||null});
}
const u=new URL(location.href);return {success:!(/captcha-delivery\.com/i.test(document.body.innerHTML)||document.title==='etsy.com'),query:u.searchParams.get('q'),search_url:location.href,page:Number(u.searchParams.get('page')||1),listing_count:listings.length,listings};
})()- Combine page outputs, deduplicate by
listing_id, and return only listings actually visible on loaded pages. Missing fields remainnull; do not infer condition, material, color, or shipping eligibility from absent card text. - When destination shipping must be confirmed, open each remaining candidate’s canonical
/listing/{id}/{slug}URL and use the visibleDeliver toestimator. Openbutton[aria-controls="estimated-shipping-form-fields"], select the option whose text matches{country}fromselect#estimated-shipping-country(resolve its current value from the option rather than hardcoding it), click#estimated-shipping-submit-button, wait for the result, and extract the resulting shipping text. A searchship_to={ISO-country-code}filter is useful for discovery but is not by itself proof that every card ships to the destination. - When condition is required, inspect the listing detail page’s expanded
Item detailssection and visible body text. Reportnewonly when the page explicitly displays it; otherwise returncondition:null. Do not treat “vintage,” “custom,” or a missing condition field as proof of new condition.
Possible Friction Points
- Etsy may interpose a DataDome challenge. Use stealth, a US residential proxy, and
solvewithtype: "dataDome"; never extract from the challenge shell. - A cold direct
/searchrequest is more likely to receive a hard CAPTCHA. Warm up on the homepage, solve DataDome, and keep warm-up, navigation, filtering, pagination, and extraction in one session. - If the page title remains
etsy.comor the DOM contains acaptcha-delivery.comiframe, start a genuinely fresh browser session with a new proxy IP rather than repeatedly reloading the poisoned session. - Search pages are large; avoid
snapshotand per-card calls. Parse all cards with one in-pageevaluateper page. data-listing-idrepeats in nested markup; deduplicate by ID.data-indexis unreliable around sponsored slots.max={number}can denote a page-size or price-ceiling parameter depending on Etsy’s current search behavior. Always enforce the numeric bound in the evaluator.- “Under $X” means strictly less than X; do not include a listing priced exactly at the ceiling.
- An unfiltered search keeps cards without a parseable price and returns
price_raw: null. For a caller's “under $35” request, setmaxPrice=35and leaveinclusiveMax=false; priced cards at $35 or above and cards without a parseable price are then excluded. - Color, material, occasion, recipient, style, holiday, room, and category facets can use opaque IDs that vary by query/category. Read and reuse the complete live facet
href. - Search-card results do not reliably expose product condition. Destination shipping and condition require detail-page verification when explicitly required.
- Etsy’s shipping estimator uses
#estimated-shipping-form-fields,#estimated-shipping-country,#estimated-shipping-zip-code, and#estimated-shipping-submit-button; country option values are opaque/currently variable, so resolve them by visible country label. - Semantic title matching is heuristic and cannot prove material, color, personalization status, or condition. Never click Add to Cart, Buy it Now, Favorite, or Sign In.