Retrieve the reported rental count and structured visible Apartments.com listings for a city. Return each community's name, full address, displayed rent and bedroom ranges, phone, baths, availability or sold-out indication, amenities, rating, pool-advertisement flag, and canonical listing URL.
Use Cases
- Find visible rental communities in a city.
- Return property contact, pricing, and amenity details.
- Apply bedroom or price filters through the URL.
- Filter extracted listings by minimum displayed rating or advertised amenities.
- Produce canonical Apartments.com listing URLs.
Automation Flow
- Build
https://www.apartments.com/{city}-{state-abbr}/; optionally append/under-{price}/,/min-{price}/,/{n}-bedrooms/,/{n}-bedrooms-under-{price}/, or/{min}-to-{max}-bedrooms-under-{price}/; for a later result page, insert/{page}/after the city slug or filter path. Add any verified site-specific amenity filter path when available. - Goto the exact URL with
waitUntil: "domContentLoaded"; wait up to 8 seconds forarticle.placard. Inspect the title and body for Access Denied, Akamai, challenge, or other block text before treating missing cards as zero results. If blocked or no cards appear, revisit the exact URL once; if the block persists across fresh US residential sessions, report retrieval blocked rather than zero results. - Run this evaluate function on the loaded results page, then retain records with
rating >= {rating-min}andadvertises_pool === falsewhen those optional filters are requested:
(() => {
const clean = el => el ? el.textContent.trim().replace(/\s+/g, ' ') : null;
const first = (root, selectors) => {
for (const selector of selectors) {
const el = root.querySelector(selector);
if (el && clean(el)) return clean(el);
}
return null;
};
const out = { total_results: null, result_count_on_page: 0, listings: [] };
const countNodes = [...document.querySelectorAll('.searchResults, .resultSummary, [class*=resultsCount], [class*=resultCount], h1, h2')];
const countText = countNodes.map(clean).filter(Boolean).join(' ') || document.body.innerText || document.title || '';
const countMatch = countText.match(/([\d,]+)\s+Rentals\b/i);
if (countMatch) out.total_results = Number(countMatch[1].replace(/,/g, ''));
for (const card of document.querySelectorAll('article.placard')) {
const cardText = clean(card) || '';
const community_name = first(card, ['.js-placardTitle', '.title']);
const full_address = first(card, ['.property-address']);
const link = card.querySelector('a.property-link');
const canonical_url = link ? link.href.split('#')[0].split('?')[0] : null;
const rents = [], beds = [];
for (const box of card.querySelectorAll('.bedRentBox')) {
const bed = first(box, ['.bedTextBox', '[class*=bed]']);
const rent = first(box, ['.priceTextBox', '[class*=price]']);
if (bed) beds.push(bed);
if (rent) rents.push(rent);
}
const nums = rents.flatMap(v => [...v.matchAll(/\$\s*([\d,]+(?:\.\d{2})?)/g)].map(m => Number(m[1].replace(/,/g, '')))).filter(Number.isFinite);
let rent_range = null;
if (rents.length === 1) rent_range = rents[0];
else if (nums.length > 1) rent_range = '$' + Math.min(...nums).toLocaleString('en-US') + ' - $' + Math.max(...nums).toLocaleString('en-US');
const ratingText = first(card, ['[class*=rating]', '[aria-label*="star" i]']);
const ratingMatch = (ratingText || cardText).match(/(?:rating|rated|stars?)?\s*([1-5](?:\.\d)?)\s*(?:out of 5|stars?)?/i);
const rating = ratingMatch ? Number(ratingMatch[1]) : null;
const amenities = first(card, ['.amenities', '.amenities-list', '[class*=amenit]']);
const advertises_pool = /\b(pool|swimming pool|swimming-pool)\b/i.test((amenities || '') + ' ' + cardText);
const bedroom_range = beds.length === 1 ? beds[0] : beds.length > 1 ? beds[0] + ' - ' + beds[beds.length - 1] : null;
const phone = first(card, ['.phone-link span', 'a.phone-link']);
const baths = first(card, ['.property-baths', '.bathTextBox', '[class*=bath]']);
const availability = first(card, ['.availability', '.availability-date', '[class*=avail]']);
const unavailable_or_sold_out = /\b(unavailable|not available|sold out|currently unavailable|no availability)\b/i.test(cardText);
if (community_name || canonical_url) out.listings.push({ community_name, full_address, rent_range, bedroom_range, phone, canonical_url, baths, availability, unavailable_or_sold_out, amenities, rating, advertises_pool });
}
out.result_count_on_page = out.listings.length;
return out;
})()Params
| Param | What it does | Example value |
|---|---|---|
| city | City slug in the landing-page path | san-francisco |
| state-abbr | Lowercase state abbreviation | ca |
| price | Maximum or minimum rent filter | 1000 |
| n | Exact bedroom-count filter | 0 |
| min / max | Bedroom range filter | 1 / 3 |
| page | Optional result-page number inserted as a path segment | 4 |
| rating-min | Optional post-extraction minimum displayed rating | 4 |
| exclude-amenity | Optional post-extraction advertised amenity to exclude | pool |
| furnished / pets / laundry / utilities / wheelchair-accessible | Requested amenity filters; URL syntax is not yet verified | true |
Possible Friction Points
| Trigger | Action |
|---|---|
The misspelled appartments.com host redirects to roommates.com or another unrelated site | Use the correctly spelled https://www.apartments.com/{city}-{state-abbr}/ URL. |
| A misspelled or malformed city slug returns an Akamai Access Denied page instead of correction, redirection, or listings | Report the observed block status; do not infer that the site corrected the city or has zero results. |
Apartments.com access is blocked by an Akamai challenge or the page has no article.placard elements | Start a fresh BQL stealth session with a US residential proxy, navigate to the exact results URL, wait up to 8 seconds, and revisit that exact URL once. |
| The challenge solver reports an IP/TLS-reputation block | Stop solver retries, start a fresh session with a cleaner US residential exit IP, and report retrieval blocked if the denial persists. |
| Fresh sessions and the challenge solver report IP/TLS-reputation denial | Stop bypass attempts and report retrieval blocked rather than zero results. |
| Access Denied appears on both the homepage and the requested city path across fresh US residential sessions | Report retrieval blocked; do not continue homepage retries or treat the city as having zero listings. |
| The challenge solver times out after the access wall appears | Stop solver retries and report retrieval blocked. |
| Access Denied persists across three fresh US residential sessions | Stop retrying and report that Apartments.com blocked retrieval; do not report zero results. |
| Browser reload is unavailable | Revisit the exact results URL instead of calling reload. |
DOMContentLoaded returns no placards because cards load asynchronously | Wait up to 8 seconds for article.placard, then run the extractor. |
A malformed low-price path such as /under-1/ returns 404 | Do not treat it as an empty result set; use a supported price threshold or the unfiltered bedroom path and inspect displayed rents. |
| Filtered cards display ranges extending above the requested maximum | Return the rent range exactly as displayed; do not clamp it to the URL price. |
| Rating or amenity text is absent or ambiguous on a card | Do not infer eligibility; leave rating null or advertises_pool based only on visible matching text and exclude uncertain records when strict filtering is required. |
| Explicit unavailability wording is absent from the card | Return unavailable_or_sold_out: false only when the rendered card text contains no matching wording; do not infer availability from rent or bedroom fields. |
| Denver results path returns an IP/TLS-reputation denial across fresh U.S. residential sessions, including a rotating exit | Stop retries and report retrieval blocked rather than claiming that the city has no listings. |