Find city rental listings

Site apartments.comTask find-city-rentalsVersion v13Updated Sep 16, 2026Category real-estate

Retrieve an Apartments.com city's rental result count and complete visible property-card details with canonical URLs, with optional rating and amenity filtering. This skill was captured from a live agent session on apartments.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.

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

  1. 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.
  2. Goto the exact URL with waitUntil: "domContentLoaded"; wait up to 8 seconds for article.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.
  3. Run this evaluate function on the loaded results page, then retain records with rating >= {rating-min} and advertises_pool === false when 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

ParamWhat it doesExample value
cityCity slug in the landing-page pathsan-francisco
state-abbrLowercase state abbreviationca
priceMaximum or minimum rent filter1000
nExact bedroom-count filter0
min / maxBedroom range filter1 / 3
pageOptional result-page number inserted as a path segment4
rating-minOptional post-extraction minimum displayed rating4
exclude-amenityOptional post-extraction advertised amenity to excludepool
furnished / pets / laundry / utilities / wheelchair-accessibleRequested amenity filters; URL syntax is not yet verifiedtrue

Possible Friction Points

TriggerAction
The misspelled appartments.com host redirects to roommates.com or another unrelated siteUse 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 listingsReport 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 elementsStart 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 blockStop 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 denialStop 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 sessionsReport retrieval blocked; do not continue homepage retries or treat the city as having zero listings.
The challenge solver times out after the access wall appearsStop solver retries and report retrieval blocked.
Access Denied persists across three fresh US residential sessionsStop retrying and report that Apartments.com blocked retrieval; do not report zero results.
Browser reload is unavailableRevisit the exact results URL instead of calling reload.
DOMContentLoaded returns no placards because cards load asynchronouslyWait up to 8 seconds for article.placard, then run the extractor.
A malformed low-price path such as /under-1/ returns 404Do 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 maximumReturn the rent range exactly as displayed; do not clamp it to the URL price.
Rating or amenity text is absent or ambiguous on a cardDo 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 cardReturn 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 exitStop retries and report retrieval blocked rather than claiming that the city has no listings.

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=apartments.com&task=find-city-rentals