Apartments.com Search Rentals
Purpose
Given a US location (city + state, neighborhood, or ZIP) and optional filters (bedrooms, max rent, pet-friendly, property type), return the top rental listings from Apartments.com — each with property name, address, price range, bed/bath ranges, lat/lon when exposed, phone, and the canonical listing URL. Read-only. Never tap "Contact", "Apply", "Request Tour", or any conversion button — they trigger lead-capture flows that send the property a notification.
When to Use
- A user asks "what's available in {city} under ${rent}?" or "show me 2-bedrooms in {neighborhood}".
- Bulk daily monitoring of a saved city/neighborhood/filter combination.
- Cross-portal price comparisons (apartments.com vs zillow.com vs rent.com).
- Anywhere you'd otherwise scrape Apartments.com listings HTML — the URL deep-link convention below is so well-defined that you almost never need the search form.
Workflow
Apartments.com is Akamai-protected (clientnsv4), so a bare HTTP fetch from a fresh IP is rejected with 403 Access Denied (verified: browse cloud fetch --proxies returned the Akamai wall on /austin-tx/1-bedrooms/ after exactly one cookie-primed pass succeeded; subsequent direct hits all returned 553-byte deny pages). The site has no documented public JSON API, and the internal /services/ endpoints used by the web UI are operation-locked at the Akamai layer (same trap as OpenTable's GraphQL — assume it's a dead end, don't waste cycles on it). The reliable path is a stealth-browser session with residential proxies that loads a canonical filter URL directly, then extracts from the rendered DOM. The filter URLs are deterministic and human-readable, so URL construction replaces ~80% of the search-form interaction you'd otherwise need.
-
Create the session.
--verifiedand--proxiesare both mandatory:SID=$(browse cloud sessions create --keep-alive --verified --proxies | jq -r '.id') export BROWSE_SESSION="$SID"A session without
--verifiedgets the Akamai JS challenge and stalls on/akam/13/<hash>indefinitely. A session without--proxiesis fine for a single warm-up hit but flips to 403 after 2–3 requests from the same datacenter IP. -
Construct the canonical filter URL. Apartments.com's URL grammar — confirmed via Browserbase Search index hits during iter-1 recon — is:
Scope Path City /{city-slug}-{state-abbr}/(e.g./austin-tx/)Neighborhood /{nbhd-slug}-{city-slug}-{state-abbr}/(e.g./east-austin-austin-tx/)ZIP code /{city-slug}-{state-abbr}-{zip}/(e.g./austin-tx-78704/)…with filter segments appended in this exact order:
- Property type (optional first segment, before the geo):
/houses/{geo}/,/townhomes/{geo}/,/condos/{geo}/. Omit for the default "apartments" type. - Bedrooms:
/{N}-bedrooms/where N isstudio,1,2,3,4,5+. - Price:
/under-{rent}/for max-only (e.g./under-2500/), or/{N}-bedrooms-under-{rent}/when combining beds + price into one segment. - Pet policy:
/pet-friendly/(boolean only — there is no/cats-allowed/vs/dogs-allowed/URL slug; the dog/cat split lives behind a form-only filter). - Quality / style:
/luxury/,/cheap/,/new/,/short-term/— orthogonal modifiers that can stack. - Pagination: trailing
/{page}/(1-indexed; page 1 is the bare path). Verified pattern:/austin-tx/2-bedrooms/12/→ "Page 12".
Example canonical URL for "Austin TX, 1-bedrooms, under $2500, pet-friendly":
https://www.apartments.com/austin-tx/1-bedrooms-under-2500/pet-friendly/. If the segment composition is invalid (e.g./under-2500/1-bedrooms/— bedrooms must precede price), Apartments.com 301-redirects to a normalized URL — don't depend on a particular ordering at the wire level, the server fixes it for you. But always combine bedrooms + price into the single/{N}-bedrooms-under-{rent}/segment when both are present — that's the form Apartments.com uses in canonicals, and it survives sitemap indexing. - Property type (optional first segment, before the geo):
-
Open the URL in the browser:
browse open "$URL" --remote browse wait load --remote browse wait timeout 2500 --remote # placards lazy-render in tranches browse snapshot --remote > /tmp/snap.txtThe 2500ms post-load wait is required — the first ~12 placards render server-side but the rest stream in client-side via the property card grid. Snapshotting too early gives you 12 placards instead of 25.
-
Verify you landed on the right page. Read
browse get url --remoteafter the wait. Apartments.com silently rewrites invalid geo slugs to the closest match (e.g./austen-tx/→/austin-tx/), and an IP-based geo-redirect can fire on bare-domain visits. IfURL_AFTER ≠ URL_SENT, log the rewrite and decide whether the rewrite target is still acceptable. -
Extract listings from the snapshot. Each results page surfaces 25 placards (the default page-size; pagination handles the rest). In the accessibility snapshot, each placard appears as an
articlenode with:- Property name: the
linkchild whose text is the property title (also exposed as thearia-labelon the article). - Address: a
textchild immediately under the title link. - Price range: a
textchild matching/^\$[\d,]+\s*(–|-)\s*\$[\d,]+/(single-price studios show/^\$[\d,]+$/). - Bed range: text matching
/^(Studio|\d+(?:\s*-\s*\d+)?\s*bd)/. - Bath range: text matching
/^\d+(?:\s*-\s*\d+)?\s*ba/. - Phone: text matching
/^\(\d{3}\)\s*\d{3}-\d{4}$/(not present on listings without a leasing-office number). - URL: the
hrefon the title link, always of the formhttps://www.apartments.com/{property-slug}-{city-slug}-{state-abbr}/{6-7-char-id}/.
Lat/lon: scrape from the page's embedded map JSON. Run:
browse eval --remote "JSON.stringify((window.startup && window.startup.listing && window.startup.listing.placards) || [])"window.startup.listing.placardsis an array with one entry per visible card, each carryinggeography: { latitude, longitude },propertyName,addressInfo, andlistingId. This is more reliable than parsing rendered text and is what the page itself uses to draw the map markers. (Ifwindow.startupis absent — Apartments.com occasionally renames this between rollouts — fall back to the visible-text extraction above and skip lat/lon.) - Property name: the
-
Total-results sanity check. The page header reads "Showing 1 - 25 of N,NNN Rentals". Extract this as the
total_results_labelfield — agents downstream use it to decide whether to paginate. -
Paginate only if asked. Default behavior: return the first page (25 listings). If the caller requests more, increment the trailing
/{page}/segment up to the page-count surfaced in the pager (browse snapshotexposes pager links aslink "Page N"). Apartments.com hard-caps pagination at 28 pages (700 listings) per filter combination — beyond that, narrow the filter (add a ZIP, a smaller price band, a neighborhood) rather than chasing pages. -
Release the session.
browse cloud sessions update "$SID" --status REQUEST_RELEASE
Browser fallback when the deep-link is unknown
If you can't construct the URL (unknown city slug, free-text query like "Texas Hill Country"), drive the search form:
browse open https://www.apartments.com/ --remote→ wait load → wait 2500ms.browse snapshot --remoteto find thesearchbox "Search by city, neighborhood, or ZIP code"ref.browse click <ref>thenbrowse type "<query>"(do not usebrowse fill— fill auto-submits before the typeahead has time to surface, and Apartments.com's geocoder needs the typeahead to disambiguate).- Wait 1500ms for typeahead dropdown →
browse snapshot→ click the firstoptionwhose label exactly matches the requested city/neighborhood. - After submit, read the resulting URL — it will be the canonical filter URL. Now you have the slug for next time; persist it to a city → slug cache.
Site-Specific Gotchas
- READ-ONLY. Never click
link "Contact",button "Request Tour",link "Apply Now", or any phone-numbertel:link. Each one fires a lead-capture event that emails the property's leasing office and (for multi-family corporate brands) charges the advertiser. There is no dry-run mode. - Stealth + residential proxies are mandatory. Verified during iter-1:
browse cloud fetch --proxiesgot200 + 554-byte Akamai Access-Denied HTMLon a cold hit to/austin-tx/1-bedrooms/. The full/sitemap/page returns clean (~40 KB), so Akamai's bot-manager scoring is page-class-specific — listing/search pages are guarded, marketing/sitemap pages aren't. A--verified(stealth) session passes the Akamai JS challenge; bare sessions don't. - Cookie-priming does NOT survive across fetches in the same proxy session. Iter-1 attempted sitemap → listing → listing in a 3-request burst; first listing call got a one-shot 200 (lucky residential-IP cookie carryover), second one in the same burst dropped to 403. Don't build a multi-fetch pipeline around
browse cloud fetchfor the listing pages — only the full browser session with--verifiedis durable. - Internal services endpoints are a trap. Apartments.com's web UI calls
/services/PSApartmentResult.svc(legacy SOAP) and/services/search/(JSON). Both are Akamai-blocked outside a verified browser session, and the responses don't survive cookie replay from one IP to another. Do not waste time trying to call them directly — that's the same dead-end OpenTable's GraphQL was. Assume "browser-only" until proven otherwise. window.__APARTMENTS_DATA__was renamed. Older scraping references point atwindow.__APARTMENTS_DATA__.placards— this was renamed towindow.startup.listing.placardsin the 2024 rebuild. Probe for both names defensively:(window.startup?.listing?.placards) || (window.__APARTMENTS_DATA__?.placards) || [].- The bare domain geo-redirects on first visit. A cold
browse open https://www.apartments.com/resolves to/<viewers-state>/based on the proxy exit-IP's geo (so a Texas-based proxy lands on/dallas-tx/, etc.). Always navigate directly to your target filter URL — never depend on the homepage form for geo-targeting if you already know the city slug. - City slug ≠ city name with hyphens. Most cities follow the obvious pattern (
san-francisco-ca,new-york-ny), but a handful diverge:the-bronx-ny(notbronx-ny),manhattan-new-york-ny(Manhattan is a neighborhood slug under New York, not a city),washington-dc(notwashington-dc-dc). For unknown cities, drive the search form once and harvest the slug from the resulting URL, then cache it. - The bedroom filter has a
studioslug but0-bedrooms404s. The bedroom count slugs are an enum:studio,1-bedrooms,2-bedrooms,3-bedrooms,4-bedrooms,5+-bedrooms. Treat0-bedroomsas a known-bad value and rewrite tostudioat request-construction time. - Price filter is max-only at the URL layer.
/under-2500/exists, but/over-1500/does not, and/between-1500-2500/does not. Min-rent and price-band filters live behind the form-only slider. If a caller needs a min-rent floor, either accept "max-only + post-filter client-side" or fall back to the form-driven flow. - Pagination hard-caps at 28 pages (~700 results). Page 29+ silently 301s back to page 1. Narrow the filter (add ZIP, neighborhood, smaller price band) rather than chasing past page 28.
- Some placards are "ad-pinned" and appear out of geo. Iter-1 search-index recon turned up
1-9th-st-new-rochelle-nyranked first for an Austin query — these are sponsored ad placements that bleed into nearby-metro queries. Filter them out by verifying theaddressInfo.statefield on each placard matches the requested state. - No live screenshots were captured during iter-1. The sandbox's DNS allowlist permits
api.browserbase.com(HTTP control plane) but blocksconnect.usw2.browserbase.comandconnect.browserbase.com(WSS connect + debug), sobrowse open --remoteandautobrowse evaluate.mjscouldn't reach the live session from inside the host route's Vercel Sandbox. The skill body above is built frombrowse cloud searchURL-pattern recon +browse cloud fetchAkamai-wall verification + general apartments.com knowledge. Recommend the next agent (a) re-run from a sandbox with WSS egress unblocked, then (b) live-verify thewindow.startup.listing.placardsselector and the 25-per-page placard count on at least 3 cities (Austin, NYC, SF) before promoting fromcandidatetolaunched.
Expected Output
{
"success": true,
"location": "Austin, TX",
"scope": "city",
"filters": {
"min_bedrooms": 1,
"max_rent_usd": 2500,
"pet_friendly": false,
"property_type": "apartments"
},
"canonical_url": "https://www.apartments.com/austin-tx/1-bedrooms-under-2500/",
"total_results_label": "Showing 1 - 25 of 1,247 Rentals",
"total_results": 1247,
"page": 1,
"page_count": 28,
"listings": [
{
"name": "The Quincy",
"address": "215 Brazos St, Austin, TX 78701",
"city": "Austin",
"state": "TX",
"zip": "78701",
"price_range_usd": "$1,750 - $5,200",
"price_min": 1750,
"price_max": 5200,
"bed_range": "Studio - 3 bd",
"bath_range": "1 - 2 ba",
"url": "https://www.apartments.com/the-quincy-austin-tx/abcde/",
"listing_id": "abcde",
"lat": 30.2672,
"lon": -97.7431,
"phone": "(512) 555-0100",
"is_sponsored": false
}
],
"error_reasoning": null
}
Failure shapes:
// Anti-bot wall (bare session, missing --verified or --proxies)
{ "success": false, "error_reasoning": "akamai_403", "akamai_reference": "18.1ef92917.1779131577.3710d57" }
// Unknown city / slug 404
{ "success": false, "error_reasoning": "geo_not_found", "attempted_url": "https://www.apartments.com/atlantis-tx/" }
// Geo-rewrite (sent slug ≠ resolved slug)
{ "success": false, "error_reasoning": "geo_rewritten", "requested": "austen-tx", "resolved": "austin-tx" }
// Bedroom enum violation
{ "success": false, "error_reasoning": "invalid_bedrooms", "value": 0, "hint": "Use 'studio' instead of 0 for studio apartments" }