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
Open the session with a US residential proxy:
proxy: { proxy: "residential", proxyCountry: "us", proxySticky: true }. Zillow challenges unproxied sessions.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}"}))}usersSearchTermis the location, e.g.Raleigh, NC 27606. Omit anyfilterStatekey the caller did not request.Add optional
filterStatekeys before navigation:price:{min:{min-price},max:{max-price}}for a price range.beds:{min:{n}}andbaths:{min:{n}}for minimum bedrooms and bathrooms.isSingleFamily:{value:true},isCondo:{value:true},isTownhouse:{value:true}, orisLotLand:{value:true}to restrict the property type.isNewConstruction:{value:true}for new construction.hasPool:{value:true|false}andhasGarage:{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 orsortSelection:{value:"days"}for newest listings.
In one call,
gotothe constructed URL withwaitUntil: 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)}}
})()Accept the rows only when
location.confirmedistrue, the requested term was retained, no addresses mismatch,uiTermMatchesistruewhen a search input is present, andchangedFiltersholds no incompatible change. Report every entry indroppedFiltersandchangedFilters. A zero-result response is valid only when embedded state is present and location validation passes.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}asusersSearchTermand the same filter state. If the ZIP retry is also mismatched, report a location-validation failure.To paginate, navigate to the evaluator's rebuilt
nextUrl, which preservesusersSearchTermand the completefilterStatewhile incrementingpagination.currentPageinsidesearchQueryState. 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 whennextUrlis null. Deduplicate stitched results by non-nullzpid, keeping the first complete row.
Params
| Param | What it does | Example value |
|---|---|---|
usersSearchTerm | Location searched by Zillow | Raleigh, NC 27606 |
zipFallback | Retry term derived from the requested ZIP | 27606 |
filterState.price.min | Minimum listing price | 350000 |
filterState.price.max | Maximum listing price | 650000 |
filterState.beds.min | Minimum bedrooms | 4 |
filterState.baths.min | Minimum bathrooms | 3 |
filterState.isNewConstruction.value | Requests new construction; verify retention | true |
filterState.isSingleFamily.value | Requests single-family homes; verify retention | true |
filterState.hasGarage.value | Requests homes with garages | true |
filterState.hasPool.value | Requests pool inclusion or exclusion; verify retention | false |
filterState.sortSelection.value | Result ordering | days |
filterState.isTownhouse.value | Restricts to townhouses | true |
filterState.isCondo.value | Restricts to condos; verify retention | true |
filterState.isLotLand.value | Restricts to lots or land | true |
filterState.lotSize.min | Minimum lot size in square feet | 43560 |
filterState.lotSize.max | Maximum lot size in square feet | 435600 |
category | Zillow search page mode | SEMANTIC |
pageLimit | Number of result pages to retrieve | 3 |
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
solvewith notype, 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
usersSearchTermto 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, orhasPool. 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
searchQueryStateURL and navigate directly. nextUrlcan 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.