Filter RentFaster.ca Rentals by Preferences
Purpose
Return RentFaster.ca rental listings that match a renter's preferences — pets, bedrooms,
budget, in-suite laundry and move-in timing — by reading the site's own JSON data feed instead
of driving the browser. A filtered search completes in a few seconds. Every preference a listing
leaves blank is reported as "unknown" rather than guessed. Each result carries a canonical
link, price, size (sq_feet), bed/bath counts, cat/dog rules, laundry type, availability and
lat/lon coordinates. The skill can additionally rank results by walking distance to a place the
renter cares about (using the returned coordinates) and dedupe against previously-seen listings.
Read-only — it never contacts a landlord, applies, or books a tour.
When to Use
- "Find me 2-bedroom Calgary rentals under $1,500 that allow cats and have in-suite laundry."
- Daily/weekly monitoring where you only want listings you haven't seen before (dedupe by
id). - Ranking candidate rentals by proximity to an office, a park, a river, or a transit stop.
- Any flow that would otherwise scrape RentFaster HTML — the JSON feed is faster, cheaper, and not Cloudflare-blocked, whereas the HTML pages are.
Workflow
RentFaster.ca's UI is a thin client over a public JSON feed at
https://www.rentfaster.ca/api/search.json. The HTML pages (/, /properties/...) sit behind
Cloudflare's WAF and return HTTP 403 to any plain fetch, but the JSON feed returns 200.
Lead with the feed; only fall back to the browser to read a single listing's floor-plan page.
1. Build the feed URL with the filters the API actually honors
GET https://www.rentfaster.ca/api/search.json
?proximity_type=location-city
&novalidate=true
&cities[]=Calgary
&price_range_adv[from]=1000
&price_range_adv[to]=1500
&beds=2
&cur_page=1
URL-encode the brackets (cities%5B%5D=, price_range_adv%5Bfrom%5D=). Fetch it:
browse cloud fetch "https://www.rentfaster.ca/api/search.json?proximity_type=location-city&novalidate=true&cities%5B%5D=Calgary&price_range_adv%5Bfrom%5D=1000&price_range_adv%5Bto%5D=1500&beds=2&cur_page=1" --proxies
Server-side filters the API honors (confirm acceptance via the echoed query object in the
response): cities[] (repeatable), price_range_adv[from]/price_range_adv[to], beds
(studio = 0), baths, type[] (e.g. Apartment, House, Condo), cur_page.
2. Read { listings, total, query } and paginate
The response is {"listings":[...48 per page...], "total":N, "query":{...}}. Read total and
increment cur_page until covered (or cap at ~3 pages for a quick preview). query echoes
exactly which params the server accepted — anything missing from it was silently dropped.
3. Filter CLIENT-SIDE for the preferences the API ignores
These params are accepted syntactically but do not filter the result set, so apply them yourself against each listing object:
- Pets — use the per-listing booleans
catsanddogs. Keep a listing only if the relevant boolean istrue. If the field isnull/absent, emit"unknown"and do not assume the pet is allowed. - In-suite laundry — inspect
features[]. Laundry values observed:"Laundry - In Suite","Laundry - Shared","Laundry - Coin/Card". Iffeaturesis empty/missing, laundry is"unknown". Do not usekeywords=for laundry (see Gotchas). - Move-in date —
availability(e.g."Immediate") plusavdate(free text, may list several dates for multi-unit buildings, e.g."Immediate, November 01"). Parse leniently;"unknown"when blank.
4. (Optional) Enrich: distance ranking and floor plans
- Walking distance — every listing has
latitude/longitude. Rank by distance to a target place: straight-line (haversine) is instant from the feed alone; for true walking distance, pass the coordinate pairs to an external routing service — the feed itself only provides coordinates, not routes. - Room sizes / floor plans —
sq_feetis a CSV of unit sizes (e.g."346, 522, 432"). When it is empty, say plainly "no room sizes given"; optionally open the listing'swebsite(building site) or its/properties/...page via the browser fallback to look for floor plans. Never fabricate a size.
5. (Optional) Dedupe across repeat searches
Persist the set of ids you've already surfaced. On the next run, drop listings whose id is in
that set so the renter only sees new inventory.
6. Emit compact JSON
Build the output object (see Expected Output). Cap listings at ~10 and report the full
count in total_matched — emitting every field of hundreds of listings as prose will blow the
response budget.
Browser fallback (only when you must render a listing page)
The feed covers the search/filter task completely. The only reason to touch the browser is to
read a single listing's rendered floor-plan/photos when sq_feet is blank:
sid=$(browse cloud sessions create --keep-alive --verified --proxies | node -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>process.stdout.write(JSON.parse(s).id))")
browse open "https://www.rentfaster.ca/properties/807-mcneill-road-northeast-calgary-301522" --remote --session "$sid"
browse wait load --remote --session "$sid"
browse get markdown body --remote --session "$sid"
browse cloud sessions update "$sid" --status REQUEST_RELEASE
Both --verified and --proxies are required for the HTML path — a bare session gets
Cloudflare 403 / challenge.
Site-Specific Gotchas
- HTML is Cloudflare-WAF'd; the JSON feed is not. A plain
browse cloud fetchof/or any/properties/...page returns 403 (cloudflare,cloudflare-waf).search.jsonandmap.jsonreturn 200 (theirCf-Cache-StatusisDYNAMIC, not challenged). - Proxies are optional for the feed, mandatory for the browser fallback.
search.jsonreturns 200 both with and without--proxiesfrom a datacenter IP (verified: identical totals). Keep--proxieson for resilience against rate-limiting and any geo edge cases, but the feed does not require residential egress. The browser/HTML path, by contrast, needs--verified --proxiesor Cloudflare blocks it. cats=1/dogs=1query params are silently ignored. Passing them returns the identical result set and distribution as omitting them (verified: 47 cats-true / 1 cats-false in the first page either way). Pets must be filtered client-side on thecats/dogsbooleans.keywords=mangles input.keywords=in-suite laundryis echoed back as"-suite laundry"— the parser strips/splits tokens. Don't rely on it for laundry or any structured filter; filter onfeatures[]instead.- Flag, don't guess, missing fields.
cats/dogsare usually booleans but can be absent;featuresis frequently empty (≈40 of 48 listings on a sample page had no laundry feature listed);sq_feetis occasionally blank. Each of these must surface as"unknown"/ "no room sizes given" — never defaulted to a favourable value. - Bracket encoding is required. Unencoded
cities[]=can be eaten by the shell/CLI and produce an empty response. Always send%5B%5D. - 48 results per page, fixed.
per_pageis ignored; paginate withcur_page.totalis the full server-side count (e.g. Calgary ≈ 8,700 all-types; 2-bed $1000–$1500 ≈ 3,250). avdateis free text and may be multi-valued. Multi-unit buildings list several dates ("Immediate, November 01"); don't parse it as a single ISO date.- Listing links are relative. The
linkfield is/properties/<slug>-<id>; prefixhttps://www.rentfaster.cato get a clickable URL. - ID-scoped refresh:
proximity_type=location-id&id[]=<id>returns a single listing (plus a few related) — handy for re-checking one listing's current price/availability cheaply. map.jsonis a lighter sibling (https://www.rentfaster.ca/api/map.json, up to 500 listings/page, ~31,900 nationwide) but omitsfeatures,cats/dogs,sq_feet. Usesearch.jsonwhen you need the preference fields.- No site-specific CAPTCHA observed on the JSON feed across the iterations tested — the only anti-bot surface is Cloudflare on the HTML pages.
Expected Output
{
"success": true,
"city": "Calgary",
"total_matched": 15,
"filters": {
"beds": 2,
"price_from": 1000,
"price_to": 1500,
"cats": true,
"laundry": "in-suite",
"move_in": "asap"
},
"listings": [
{
"id": 301522,
"url": "https://www.rentfaster.ca/properties/807-mcneill-road-northeast-calgary-301522",
"title": "NEWLY RENOVATED 2 BDRM, Split Level in Mayland Heights",
"price": "1500",
"sq_feet": "950",
"beds": "2",
"baths": "1",
"cats": true,
"dogs": true,
"laundry": "in-suite",
"availability": "Immediate",
"avdate": "Immediate",
"latitude": 51.058088,
"longitude": -114.016964
}
],
"error_reasoning": null
}
A listing with fields the feed omitted (honest "unknown" reporting):
{
"id": 784644,
"url": "https://www.rentfaster.ca/properties/some-building-calgary-784644",
"title": "2 Bedroom Apartment",
"price": "1425",
"sq_feet": "no room sizes given",
"beds": "2",
"baths": "1",
"cats": true,
"dogs": "unknown",
"laundry": "unknown",
"availability": "Immediate",
"avdate": "unknown",
"latitude": 51.0441,
"longitude": -114.0631
}
No matches after filtering:
{
"success": true,
"city": "Calgary",
"total_matched": 0,
"filters": { "beds": 3, "price_from": 800, "price_to": 1000, "cats": true, "laundry": "in-suite" },
"listings": [],
"error_reasoning": null
}
Feed unreachable / blocked:
{
"success": false,
"city": "Calgary",
"total_matched": 0,
"listings": [],
"error_reasoning": "search.json returned HTTP 403 (Cloudflare) — retry with --proxies or back off and retry."
}