Search Zillow Homes for Sale

Site zillow.comTask search-zillow-property-listingsVersion v25Updated Nov 5, 2026Category real-estate

Search Zillow for-sale listings by location with price, bedroom, bathroom, property-type, construction, and amenity filters encoded in searchQueryState, verify Zillow retained the location and filters, and extract deduplicated property rows across result pages as structured JSON. This skill was captured from a live agent session on zillow.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.

Use Zillow search results to retrieve for-sale listings for a supplied city, neighborhood, or ZIP code, filtered by price, beds, baths, property type, construction status, pool, or garage, then validate that Zillow kept the requested location and filters before extracting structured rows.

Use Cases

Use this when you need Zillow listings for a location with address, price, home type, beds, baths, living area, lot size, status, and detail URL fields. Apply filters when the caller requires a price range, minimum bedrooms or bathrooms, a property type such as single-family, condo, townhouse, or land, new construction, a lot-size range, or homes with a pool or garage. Use the validation output to report which requested constraints Zillow actually applied.

Automation Flow

  1. Open the session with a US residential proxy: proxy: { proxy: "residential", proxyCountry: "us", proxySticky: true }. Zillow challenges unproxied sessions.

  2. Build the results URL directly; no homepage visit or search-box interaction is needed: https://www.zillow.com/homes/?category=SEMANTIC&searchQueryState={encodeURIComponent(JSON.stringify({filterState:{...},usersSearchTerm:"{location}"}))}

    usersSearchTerm is the location, e.g. Raleigh, NC 27606. Omit any filterState key the caller did not request.

  3. Add optional filterState keys before navigation:

    • price:{min:{min-price},max:{max-price}} for a price range.
    • beds:{min:{n}} and baths:{min:{n}} for minimum bedrooms and bathrooms.
    • isSingleFamily:{value:true}, isCondo:{value:true}, isTownhouse:{value:true}, or isLotLand:{value:true} to restrict the property type.
    • isNewConstruction:{value:true} for new construction.
    • hasPool:{value:true|false} and hasGarage:{value:true} for amenities.
    • lotSize:{min:{sqft},max:{sqft}} for lot size in square feet (1 acre = 43560).
    • sortSelection:{value:"pricea"} for lowest price first or sortSelection:{value:"days"} for newest listings.
  4. In one call, goto the constructed URL with waitUntil: domcontentloaded, then run this evaluator on the loaded results page:

(()=>{
const clean=v=>v==null?null:String(v).replace(/\s+/g,' ').trim()||null;
const num=v=>{if(typeof v==='number')return v;const m=String(v??'').replace(/[$,]/g,'').match(/\d+(?:\.\d+)?/);return m?Number(m[0]):null};
const eq=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
const canonical=v=>{try{const u=new URL(v,location.origin);if(!/^(www\.)?zillow\.com$/.test(u.hostname)||!/^\/homedetails\/[^/]+\/\d+_zpid\/?$/.test(u.pathname))return null;return `${u.origin}${u.pathname.replace(/\/+$/,'/')}`}catch{return null}};
const body=document.body?.innerText||'';
const frames=[...document.querySelectorAll('iframe')].map(f=>`${f.title||''} ${f.getAttribute('aria-label')||''} ${f.src||''}`).join(' ');
const blocked=/captcha|perimeterx|px-captcha|access denied|robot check|press\s*&?\s*hold|human verification|consent wall/i.test(body+' '+frames);
let requested=null;try{const raw=new URL(location.href).searchParams.get('searchQueryState');requested=raw?JSON.parse(raw):null}catch{}
const base={success:false,assessment:'unknown',blocked,url:location.href,documentTitle:document.title,requestedFilters:requested?.filterState||{},appliedFilters:null,droppedFilters:[],changedFilters:[],poolConstraint:{requested:null,applied:null,representable:null,verified:false},total:null,count:0,listings:[],nextUrl:null};
if(blocked)return {...base,assessment:'blocked',error:'Zillow challenge, consent, or access-denied page'};
const script=document.getElementById('__NEXT_DATA__');
if(!script)return {...base,error:'Missing Zillow __NEXT_DATA__'};
try{
const data=JSON.parse(script.textContent||'{}'),state=data?.props?.pageProps?.searchPageState||{},query=state?.queryState||{},applied=query?.filterState||{},wanted=requested?.filterState||{};
const droppedFilters=Object.keys(wanted).filter(k=>!(k in applied)).map(k=>({key:k,requested:wanted[k],applied:null,reason:'missing'}));
const changedFilters=Object.keys(wanted).filter(k=>k in applied&&!eq(wanted[k],applied[k])).map(k=>({key:k,requested:wanted[k],applied:applied[k],reason:'changed'}));
const requestedPool=wanted?.hasPool?.value,appliedPool=applied?.hasPool?.value;
const poolConstraint={requested:requestedPool??null,applied:appliedPool??null,representable:Object.prototype.hasOwnProperty.call(applied,'hasPool'),verified:typeof requestedPool==='boolean'&&requestedPool===appliedPool};
const term=clean(query?.usersSearchTerm||state?.usersSearchTerm),requestedTerm=clean(requested?.usersSearchTerm),termMatches=!!requestedTerm&&!!term&&term.toLowerCase()===requestedTerm.toLowerCase();
const searchInput=clean(document.querySelector("input[aria-label='Search']")?.value),uiTermMatches=searchInput==null?null:!!requestedTerm&&searchInput.toLowerCase().includes(requestedTerm.toLowerCase());
const lower=String(term||'').toLowerCase(),zip=(lower.match(/\b\d{5}(?:-\d{4})?\b/)||[])[0]||null;
const rows=state?.cat1?.searchResults?.listResults||[];
const listings=rows.map(r=>{const i=r?.hdpData?.homeInfo||{},address=clean(r?.address||[r?.addressStreet,r?.addressCity,r?.addressState,r?.addressZipcode].filter(Boolean).join(', '));return {zpid:clean(r?.zpid??i?.zpid),detailUrl:canonical(r?.detailUrl||i?.detailUrl),address,price:num(r?.unformattedPrice??i?.price??r?.price),priceText:clean(r?.price),homeType:clean(r?.hdpData?.homeType??i?.homeType??r?.homeType),beds:r?.beds??i?.bedrooms??null,baths:r?.baths??i?.bathrooms??null,livingArea:r?.area??i?.livingArea??i?.livingAreaValue??null,lotSize:r?.lotSize??i?.lotSize??i?.lotAreaValue??null,status:clean(r?.statusText??r?.listingStatus??i?.homeStatus),pool:clean(r?.pool??i?.pool),garage:clean(r?.garage??i?.garage)}}).filter(x=>x.detailUrl);
const mismatches=listings.filter(x=>zip&&!String(x.address||'').toLowerCase().includes(zip)).map(x=>x.address);
const total=state?.categoryTotals?.cat1?.totalResultCount??null,next=state?.pagination?.nextUrl||state?.cat1?.searchResults?.pagination?.nextUrl||null,sortValue=clean(applied?.sortSelection?.value);
const nextUrl=next&&requested?new URL('/homes/',location.origin):null;
if(nextUrl){nextUrl.searchParams.set('category','SEMANTIC');nextUrl.searchParams.set('searchQueryState',JSON.stringify({...requested,pagination:{...requested.pagination,currentPage:(requested.pagination?.currentPage||1)+1}}));}
const confirmed=termMatches&&(uiTermMatches!==false)&&(listings.length===0||mismatches.length===0);
return {success:true,assessment:droppedFilters.length||changedFilters.length?'filters-dropped':total===0?'zero-results':total>0?'results-with-filters':'unknown',blocked:false,url:location.href,documentTitle:document.title,location:{usersSearchTerm:term,requestedTerm,termMatches,searchInput,uiTermMatches,zip,confirmed,mismatches},requestedFilters:wanted,appliedFilters:applied,droppedFilters,changedFilters,poolConstraint,total,count:listings.length,listings,nextUrl:nextUrl?.href||null,sort:{value:sortValue,lowestPriceConfirmed:sortValue==='pricea',newestConfirmed:['days','days_on_zillow'].includes(sortValue)}};
}catch(e){return {...base,error:String(e)}}
})()
  1. Accept the rows only when location.confirmed is true, the requested term was retained, no addresses mismatch, uiTermMatches is true when a search input is present, and changedFilters holds no incompatible change. Report every entry in droppedFilters and changedFilters. A zero-result response is valid only when embedded state is present and location validation passes.

  2. If the location search is empty, mismatched, or shows another market in the title or search input, discard the rows, start a fresh US residential-proxy session, and retry with the extracted {zip} as usersSearchTerm and the same filter state. If the ZIP retry is also mismatched, report a location-validation failure.

  3. To paginate, navigate to the evaluator's rebuilt nextUrl, which preserves usersSearchTerm and the complete filterState while incrementing pagination.currentPage inside searchQueryState. Do not use Zillow's bare next-page link, which may omit the requested location and filters. Rerun the evaluator and apply step 5 on every page, up to the page limit (default 3), stopping when nextUrl is null. Deduplicate stitched results by non-null zpid, keeping the first complete row.

Params

ParamWhat it doesExample value
usersSearchTermLocation searched by ZillowRaleigh, NC 27606
zipFallbackRetry term derived from the requested ZIP27606
filterState.price.minMinimum listing price350000
filterState.price.maxMaximum listing price650000
filterState.beds.minMinimum bedrooms4
filterState.baths.minMinimum bathrooms3
filterState.isNewConstruction.valueRequests new construction; verify retentiontrue
filterState.isSingleFamily.valueRequests single-family homes; verify retentiontrue
filterState.hasGarage.valueRequests homes with garagestrue
filterState.hasPool.valueRequests pool inclusion or exclusion; verify retentionfalse
filterState.sortSelection.valueResult orderingdays
filterState.isTownhouse.valueRestricts to townhousestrue
filterState.isCondo.valueRestricts to condos; verify retentiontrue
filterState.isLotLand.valueRestricts to lots or landtrue
filterState.lotSize.minMinimum lot size in square feet43560
filterState.lotSize.maxMaximum lot size in square feet435600
categoryZillow search page modeSEMANTIC
pageLimitNumber of result pages to retrieve3

Possible Friction Points

  • Zillow shows a PerimeterX "Press & Hold" page ("Access to this page has been denied") on proxied sessions too, often on the second navigation. Call solve with no type, re-read the page, and if it persists start a fresh US residential-proxy session and retry the direct URL. Do not click through the challenge.
  • An HTTP 400 on the direct encoded URL is not zero results. Retry the exact URL once in a fresh proxied session, then the ZIP-only URL; if both return 400, report that Zillow search state could not be loaded.
  • Zillow can route usersSearchTerm to an unrelated market (observed: Rancho Cordova, CA; Killeen, TX; Roseville, MI). Trust rows only when the evaluator's location checks pass.
  • Zillow may silently drop isCondo, isNewConstruction, isSingleFamily, isLotLand, or hasPool. Do not claim a constraint the evaluator lists as dropped or changed, and do not infer pool status from returned rows.
  • A missing __NEXT_DATA__ script means the page did not load search state, not zero results. Restart with a fresh proxy and retry; if a restrictive price filter is present, retry once without it, then report filter-state failure.
  • Do not use the advanced-filter controls on an empty or mismatched page; rebuild the full searchQueryState URL and navigate directly.
  • nextUrl can disappear before the requested page limit; report the pages retrieved and a pagination failure.
  • The same property can appear on more than one page; deduplicate by zpid.

Call it

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