Etsy Product Search

Site etsy.comTask search-productsVersion v12Updated Sep 3, 2026Category marketplace

Search Etsy listings for a caller-provided query with optional price, color, shipping-country, ordering, pagination, semantic title, and detail verification filters while handling DataDome and returning structured read-only results. This skill was captured from a live agent session on etsy.com and is published here as a reusable recipe for agents.

NoteSelectors and URL schemes drift as sites change. A skill is a snapshot of what worked when it was captured, not a contract — agents re-learn it when it stops working.

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

  1. Run the complete flow in one browserless_agent session with a US residential proxy so DataDome clearance persists:
{"proxy":{"proxy":"residential","proxyCountry":"us"},"commands":[...]}
  1. 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};
})()
  1. Combine page outputs, deduplicate by listing_id, and return only listings actually visible on loaded pages. Missing fields remain null; do not infer condition, material, color, or shipping eligibility from absent card text.
  2. When destination shipping must be confirmed, open each remaining candidate’s canonical /listing/{id}/{slug} URL and use the visible Deliver to estimator. Open button[aria-controls="estimated-shipping-form-fields"], select the option whose text matches {country} from select#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 search ship_to={ISO-country-code} filter is useful for discovery but is not by itself proof that every card ships to the destination.
  3. When condition is required, inspect the listing detail page’s expanded Item details section and visible body text. Report new only when the page explicitly displays it; otherwise return condition: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 solve with type: "dataDome"; never extract from the challenge shell.
  • A cold direct /search request 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.com or the DOM contains a captcha-delivery.com iframe, start a genuinely fresh browser session with a new proxy IP rather than repeatedly reloading the poisoned session.
  • Search pages are large; avoid snapshot and per-card calls. Parse all cards with one in-page evaluate per page.
  • data-listing-id repeats in nested markup; deduplicate by ID. data-index is 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, set maxPrice=35 and leave inclusiveMax=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.

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=etsy.com&task=search-products