# 🏠 Rental Listings Scraper — Canada, UK & USA | Geo Enriched (`soroush98/rental-listings-scraper`) Actor

Apartments, houses, flats & rooms for rent from Kijiji + RentFaster (Canada), OpenRent (UK) and Zumper (USA) in one normalized schema: price, beds, coordinates, currency. Filters: max rent, bedrooms, keywords, near an address or subway/grocery/school. Cross-source dedup + map. Agent/MCP-ready.

- **URL**: https://apify.com/soroush98/rental-listings-scraper.md
- **Developed by:** [Soroosh Esmaeilian](https://apify.com/soroush98) (community)
- **Categories:** Real estate, Travel, Automation
- **Stats:** 3 total users, 3 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 listings

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js/docs.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python/docs.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## Rental Listings Scraper — Canada 🇨🇦 + UK 🇬🇧 + US 🇺🇸 + Geo Data 🏠

One **normalized, deduplicated** feed of rental listings — **Kijiji** and
**RentFaster.ca** for Canada, **OpenRent** for the UK, **Zumper** for the US —
with optional **nearest-amenity distances** (transit, grocery, school, …)
attached to every listing.

Most rental scrapers give you one site in that site's own ad-hoc shape. This
Actor gives you **every source in a single schema across countries**, collapses
the same unit posted to multiple sites into one row, and can tell you *how far
each place is from the subway* — a signal no other rental scraper on Apify ships.

### Why this is different

| | Typical single-site scraper | This Actor |
|---|---|---|
| Sources | One | Kijiji + RentFaster (CA), OpenRent (UK), Zumper (US) |
| Schema | Per-site, ad-hoc | One unified schema across sources & countries |
| Duplicates | You dedupe yourself | Cross-source dedup built in (`also_on`) |
| Location intelligence | Lat/lng only | Nearest-amenity distances, **included free** |

### Input

**Scope**

| Field | Type | Default | Notes |
|---|---|---|---|
| `country` | string | `"CA"` | `CA` (Kijiji + RentFaster), `GB` (OpenRent), or `US` (Zumper) |
| `sources` | array | all for the country | Sites to scrape; other countries' sources are ignored |
| `cities` | array | `[]` (all) | City names, e.g. `["Toronto","Calgary"]`, `["London"]`, `["Austin","Seattle"]` |
| `maxPerCity` | int | `100` | Cap per city, per source |
| `dedupe` | bool | `true` | Merge cross-source duplicates |

**Filters** (all optional — applied cheapest-first)

| Field | Type | Notes |
|---|---|---|
| `minRent` / `maxRent` | int | Monthly rent bounds, e.g. `maxRent: 2500` |
| `minBedrooms` / `maxBedrooms` | int | For exactly 2-bed, set both to `2`. `0` = include bachelor |
| `keywords` | array | Title/description must contain **ALL** (case-insensitive). e.g. `["female"]`, `["parking","balcony"]` |
| `excludeKeywords` | array | Drop if title/description contains **ANY**. e.g. `["no pets"]` |
| `nearAmenities` | array | Keep listings within `maxAmenityDistanceM` of **each** type: `subway, train, bus_stop, grocery, cafe, pharmacy, park, school, university, library, gym, hospital`. Auto-enables enrichment |
| `maxAmenityDistanceM` | int | Default `800` (≈10-min walk) |
| `nearAddress` | string | A specific place to anchor on, e.g. `"200 Bay St, Toronto"`. Geocoded, then listings kept within `nearAddressRadiusM`. **Use this for "near my workplace"** |
| `nearAddressRadiusM` | int | Default `2000` |

**Enrichment & infra**

| Field | Type | Default | Notes |
|---|---|---|---|
| `enrichAmenities` | bool | `false` | Attach `amenity_distances_m` even without a `nearAmenities` filter (fast, free) |
| `enrichRadiusM` | int | `1500` | Amenity search radius |
| `maxEnrich` | int | `200` | Cap on listings enriched per run |
| `proxyConfiguration` | object | Apify Proxy on | **Recommended** — see below |

Examples:

```json
// minimal
{ "sources": ["kijiji", "rentfaster"], "cities": ["Toronto"], "maxPerCity": 50 }

// "2-bed under $2500 in Toronto, near a subway, female-only"
{
  "cities": ["Toronto"], "minBedrooms": 2, "maxBedrooms": 2,
  "maxRent": 2500, "nearAmenities": ["subway"], "maxAmenityDistanceM": 800,
  "keywords": ["female"]
}

// "1-bed within 2 km of my office at 200 Bay St"
{ "cities": ["Toronto"], "maxBedrooms": 1, "nearAddress": "200 Bay St, Toronto", "nearAddressRadiusM": 2000 }
````

#### "Near X" — two different mechanisms

- **Near a *type* of place** (subway, grocery, university…) → `nearAmenities`. Backed by OpenStreetMap; great coverage in Canadian cities.
- **Near a *specific* place** (your office, a named landmark) → `nearAddress`. Geocoded to one point. Amenity categories can't find a specific employer (OSM may not have "Company X"), so use `nearAddress` for that.

#### Description questions (e.g. "female only", "no pets")

The full `description` is always in the output. For **literal** phrases, use `keywords` / `excludeKeywords` (deterministic, server-side). For **nuanced** interpretation, let an LLM read the returned descriptions — e.g. via [Apify's MCP server](https://docs.apify.com/platform/integrations/mcp), Claude can run this Actor and reason over the results in chat. Note: keyword matching is literal, so it depends on how the lister phrased it, and gender-restricted *whole-unit* ads may run into provincial human-rights rules (shared/roommate situations are typically exempt) — that's on the data, not the filter.

### Output

Each dataset item (empty fields omitted):

```json
{
  "source": "kijiji",
  "also_on": ["rentfaster"],
  "source_id": "1700123456",
  "url": "https://www.kijiji.ca/v-apartments-condos/...",
  "title": "Bright 2BR near subway",
  "address": "123 King St W, Toronto, ON",
  "city": "Toronto",
  "province": "ON",
  "postal_code": "M5V 1J5",
  "lat": 43.6453,
  "lng": -79.3806,
  "country": "CA",
  "monthly_rent": 2450,
  "currency": "CAD",
  "bedrooms": 2.0,
  "bathrooms": 1.0,
  "sqft": 720,
  "property_type": "apartment",
  "furnished": false,
  "pet_friendly": true,
  "utilities_included": ["heat", "water"],
  "available_from": "2026-07-01",
  "description": "…",
  "amenity_distances_m": { "subway": 320, "grocery": 150, "park": 410 },
  "scraped_at": "2026-06-27T12:00:00+00:00"
}
```

`bedrooms: 0.5` means bachelor/studio. `also_on` lists other sites the same
unit was found on (only when `dedupe` is enabled). `monthly_rent` is always a
per-calendar-month figure in `currency` (CAD or GBP) — weekly UK quotes are
normalized for you.

**UK specifics** (`country: "GB"`, OpenRent): `province` holds the nation code
(`ENG`/`SCT`/`WLS`/`NIR`), `postal_code` is the outward code only (`"WC2N"` —
UK listings don't publish full postcodes before enquiry), and `description` is
the search snippet, not the full ad text.

**US specifics** (`country: "US"`, Zumper): `province` holds the state code,
apartment complexes yield one row per advertised floorplan (same address,
distinct `source_id`), `monthly_rent` is that floorplan's "from" price, and
`description` is a short summary.

### Use it from Claude (MCP) 🤖

This Actor is built to be driven by an AI agent, not just a form. Connect
**[Apify's MCP server](https://docs.apify.com/platform/integrations/mcp)** to
Claude (Desktop, Claude Code, or any MCP client) and Claude can run it from a
plain-English request, then reason over the results — no code.

1. Add the hosted MCP server `https://mcp.apify.com` (OAuth), or run
   `@apify/actors-mcp-server` locally with your `APIFY_TOKEN`.
2. In Claude, just ask:
   > *"Run the Canada rentals actor for Toronto — 2-bed under $2500 near a subway — and recommend the best 3."*

Claude maps that straight onto the inputs —
`{cities:["Toronto"], minBedrooms:2, maxBedrooms:2, maxRent:2500, nearAmenities:["subway"]}` —
runs the Actor, reads the dataset, and answers in chat. Because the filters
(`maxRent`, `minBedrooms`, `keywords`, `nearAmenities`, `nearAddress`) are
pushed *into* the Actor, Claude isn't post-filtering a huge blob — it gets a
short, correct set back. The `amenity_distances_m` field is what lets it answer
*"near a subway / grocery / school"* precisely instead of guessing from text.

### Proxy — please read

- **RentFaster.ca**, **OpenRent**, and **Zumper** sit behind bot challenges that
  fingerprint the TLS handshake, so browser-like *headers* alone get blocked.
  The Actor forges a real Chrome TLS/HTTP2 fingerprint (via `curl_cffi`) on a
  sticky IP to clear them; a **residential proxy** is still recommended for a
  clean IP.
- **Kijiji** rate-limits and blocks datacenter IPs aggressively.

Use **Apify Proxy** (residential group) for production runs, and set
`apifyProxyCountry` to match your `country` input (`CA`, `GB`, or `US`). The
default input already enables Apify Proxy.

### Amenity enrichment

Nearest-amenity distances come from **bundled offline POI indexes** — ~225k
Canadian, ~529k UK, and ~1.14M US POIs (OpenStreetMap, via Geofabrik) shipped
inside the Actor and queried in-process. No external API, no rate limits: enriching
hundreds of listings takes well under a second, and it's **included free** (no
per-listing enrichment charge). Stations, groceries, cafes, pharmacies, schools,
universities, libraries, gyms and hospitals carry their **OSM name** too — the
map labels them "Charing Cross — subway · 240m", not just a dot. The snapshots
are refreshed periodically; POIs are static infrastructure so they don't need
to be live.

### Legal & fair use

Scrapes only **publicly visible** listing data — no logins, no private data.
You are responsible for your use of the output. Kijiji and RentFaster each have
Terms of Use that restrict automated access; review them and your jurisdiction's
rules, run politely (low concurrency, sensible caps), and don't redistribute in
ways those terms prohibit. This Actor is provided for research and personal use.

### Roadmap

- **Australia.** Every major AU portal (Domain, realestate.com.au, Gumtree AU,
  Flatmates, rent.com.au) hard-blocks automation today; support waits on a
  headless-browser approach being worth it.
- More UK/US sources for cross-source dedup (OpenRent and Zumper are
  single-source per country today).
- Facebook Marketplace + rentals.ca sources (best-effort; both are anti-bot).
- Listing-level change tracking (price drops, relistings).

### Development

This repo **is** the Actor: `src/` is the code that runs on Apify
(`python -m src`), `.actor/` holds the actor/input/dataset schemas, and the
`Dockerfile` builds the image. `src/data/pois_<cc>.npz` are the bundled offline
POI indexes, one per country (rebuild from Geofabrik extracts with
`tools/build_poi_index.py --country CA` — needs `pip install osmium` — then
push). Adding a source or country starts at the registry in
`src/sources/__init__.py`.

```bash
python3.12 -m venv .venv
.venv/bin/pip install -r requirements.txt

.venv/bin/python test_smoke.py    # offline: parsing, dedup, filters (also runs in CI)
.venv/bin/python live_check.py    # hits the real sites, no Apify SDK needed
.venv/bin/python harness_e2e.py   # full pipeline against live sites, Apify calls stubbed

apify push                        # deploy (manual — no CI deploy)
```

Local `live_check`/`harness_e2e` runs may see Kijiji 403s from home/datacenter
IPs — that means "use a residential proxy", not "the code is broken"; the real
Kijiji test is a cloud run with Apify Proxy (residential group).

Before touching scrapers or dependency pins, read
[docs/PROJECT\_NOTES.md](docs/PROJECT_NOTES.md) — it records which sites are
viable, the exact Cloudflare workarounds, and the load-bearing version pins.

# Actor input Schema

## `country` (type: `string`):

Which country's rental market to scrape. Sources are per-country: Canada = Kijiji + RentFaster, UK = OpenRent, US = Zumper. Australia is on the roadmap. Match the proxy country below to this.

## `sources` (type: `array`):

Which rental sites to scrape. Leave empty for all sources in the selected country; sources from other countries are ignored.

## `cities` (type: `array`):

City names to cover (e.g. Toronto, Calgary — or London, Manchester for the UK). Leave empty for all supported cities.

## `maxPerCity` (type: `integer`):

Upper bound on listings collected from each city, for each source.

## `maxResults` (type: `integer`):

Hard cap on how many listings the run returns (after filtering + dedup). Raise it for bulk dataset pulls.

## `minRent` (type: `integer`):

Keep only listings at or above this monthly rent.

## `maxRent` (type: `integer`):

Keep only listings at or below this monthly rent. e.g. 2500.

## `minBedrooms` (type: `integer`):

Minimum bedroom count. Use 0 to include bachelor/studio (stored as 0.5).

## `maxBedrooms` (type: `integer`):

Maximum bedroom count. For exactly 2-bed, set min and max both to 2.

## `keywords` (type: `array`):

Case-insensitive substring match over title + description. ALL must be present. e.g. \["female"] for women-only, or \["parking","balcony"].

## `excludeKeywords` (type: `array`):

Drop a listing if its title/description contains ANY of these. e.g. \["no pets","students only"].

## `nearAmenities` (type: `array`):

Keep only listings within the distance below of EACH selected amenity type (uses OpenStreetMap). Turns on amenity enrichment automatically.

## `maxAmenityDistanceM` (type: `integer`):

Used with 'Near these amenity types'. 800 m ≈ a 10-minute walk.

## `nearAddress` (type: `string`):

Free-text address or place to anchor on, e.g. 'University of Toronto' or '200 Bay St, Toronto'. Geocoded, then listings are kept within the radius below. Use this for 'near my workplace' (amenity categories can't find a specific employer).

## `nearAddressRadiusM` (type: `integer`):

Used with 'Near a specific address'.

## `dedupe` (type: `boolean`):

Merge the same unit posted to multiple sites into one row (records the extra site in `also_on`).

## `enrichAmenities` (type: `boolean`):

Add `amenity_distances_m` (nearest subway/grocery/school/etc. in metres) from a bundled offline POI index — fast, free, no external API. Off by default. See README.

## `enrichRadiusM` (type: `integer`):

Only used when amenity enrichment is on.

## `maxEnrich` (type: `integer`):

Cap on how many listings are enriched per run. Only used when enrichment is on.

## `maxConcurrency` (type: `integer`):

Per-source request concurrency. Keep low to stay polite and avoid blocks.

## `minDelayMs` (type: `integer`):

Lower bound of the random delay inserted before each request (politeness).

## `maxDelayMs` (type: `integer`):

Upper bound of the random delay inserted before each request (politeness).

## `userAgent` (type: `string`):

Optional. Set a real contact string in production.

## `proxyConfiguration` (type: `object`):

All supported sites block datacenter IPs (Cloudflare), so this defaults to RESIDENTIAL IPs, rotated per request. Set apifyProxyCountry to match your Country input (CA for Canada, GB for the UK). Residential proxy is metered (~$8/GB) and draws from your plan credit. Switch the group to datacenter to run free, but expect intermittent blocks.

## Actor input object example

```json
{
  "country": "CA",
  "sources": [],
  "cities": [
    "Toronto",
    "Vancouver",
    "Calgary"
  ],
  "maxPerCity": 100,
  "maxResults": 50,
  "keywords": [],
  "excludeKeywords": [],
  "nearAmenities": [],
  "maxAmenityDistanceM": 800,
  "nearAddressRadiusM": 2000,
  "dedupe": true,
  "enrichAmenities": false,
  "enrichRadiusM": 1500,
  "maxEnrich": 200,
  "maxConcurrency": 3,
  "minDelayMs": 500,
  "maxDelayMs": 1500,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "CA"
  }
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "sources": [],
    "cities": [
        "Toronto",
        "Vancouver",
        "Calgary"
    ],
    "maxResults": 50,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "CA"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("soroush98/rental-listings-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "sources": [],
    "cities": [
        "Toronto",
        "Vancouver",
        "Calgary",
    ],
    "maxResults": 50,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "CA",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("soroush98/rental-listings-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "sources": [],
  "cities": [
    "Toronto",
    "Vancouver",
    "Calgary"
  ],
  "maxResults": 50,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "CA"
  }
}' |
apify call soroush98/rental-listings-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=soroush98/rental-listings-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "🏠 Rental Listings Scraper — Canada, UK & USA | Geo Enriched",
        "description": "Apartments, houses, flats & rooms for rent from Kijiji + RentFaster (Canada), OpenRent (UK) and Zumper (USA) in one normalized schema: price, beds, coordinates, currency. Filters: max rent, bedrooms, keywords, near an address or subway/grocery/school. Cross-source dedup + map. Agent/MCP-ready.",
        "version": "0.1",
        "x-build-id": "SLFceel2OPt184RLT"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/soroush98~rental-listings-scraper/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-soroush98-rental-listings-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/soroush98~rental-listings-scraper/runs": {
            "post": {
                "operationId": "runs-sync-soroush98-rental-listings-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/soroush98~rental-listings-scraper/run-sync": {
            "post": {
                "operationId": "run-sync-soroush98-rental-listings-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "sources"
                ],
                "properties": {
                    "country": {
                        "title": "Country",
                        "enum": [
                            "CA",
                            "GB",
                            "US"
                        ],
                        "type": "string",
                        "description": "Which country's rental market to scrape. Sources are per-country: Canada = Kijiji + RentFaster, UK = OpenRent, US = Zumper. Australia is on the roadmap. Match the proxy country below to this.",
                        "default": "CA"
                    },
                    "sources": {
                        "title": "Sources",
                        "type": "array",
                        "description": "Which rental sites to scrape. Leave empty for all sources in the selected country; sources from other countries are ignored.",
                        "items": {
                            "type": "string",
                            "enum": [
                                "kijiji",
                                "rentfaster",
                                "openrent",
                                "zumper"
                            ],
                            "enumTitles": [
                                "Kijiji (CA)",
                                "RentFaster.ca (CA)",
                                "OpenRent (UK)",
                                "Zumper (US)"
                            ]
                        },
                        "default": []
                    },
                    "cities": {
                        "title": "Cities",
                        "type": "array",
                        "description": "City names to cover (e.g. Toronto, Calgary — or London, Manchester for the UK). Leave empty for all supported cities.",
                        "default": [],
                        "items": {
                            "type": "string"
                        }
                    },
                    "maxPerCity": {
                        "title": "Max listings per city (per source)",
                        "minimum": 1,
                        "maximum": 5000,
                        "type": "integer",
                        "description": "Upper bound on listings collected from each city, for each source.",
                        "default": 100
                    },
                    "maxResults": {
                        "title": "Max results (total)",
                        "minimum": 1,
                        "maximum": 10000,
                        "type": "integer",
                        "description": "Hard cap on how many listings the run returns (after filtering + dedup). Raise it for bulk dataset pulls.",
                        "default": 50
                    },
                    "minRent": {
                        "title": "Min rent ($/month)",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Keep only listings at or above this monthly rent."
                    },
                    "maxRent": {
                        "title": "Max rent ($/month)",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Keep only listings at or below this monthly rent. e.g. 2500."
                    },
                    "minBedrooms": {
                        "title": "Min bedrooms",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Minimum bedroom count. Use 0 to include bachelor/studio (stored as 0.5)."
                    },
                    "maxBedrooms": {
                        "title": "Max bedrooms",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Maximum bedroom count. For exactly 2-bed, set min and max both to 2."
                    },
                    "keywords": {
                        "title": "Description must contain ALL of these",
                        "type": "array",
                        "description": "Case-insensitive substring match over title + description. ALL must be present. e.g. [\"female\"] for women-only, or [\"parking\",\"balcony\"].",
                        "default": [],
                        "items": {
                            "type": "string"
                        }
                    },
                    "excludeKeywords": {
                        "title": "Description must contain NONE of these",
                        "type": "array",
                        "description": "Drop a listing if its title/description contains ANY of these. e.g. [\"no pets\",\"students only\"].",
                        "default": [],
                        "items": {
                            "type": "string"
                        }
                    },
                    "nearAmenities": {
                        "title": "Near these amenity types",
                        "type": "array",
                        "description": "Keep only listings within the distance below of EACH selected amenity type (uses OpenStreetMap). Turns on amenity enrichment automatically.",
                        "items": {
                            "type": "string",
                            "enum": [
                                "subway",
                                "train",
                                "bus_stop",
                                "grocery",
                                "cafe",
                                "pharmacy",
                                "park",
                                "school",
                                "university",
                                "library",
                                "gym",
                                "hospital"
                            ]
                        },
                        "default": []
                    },
                    "maxAmenityDistanceM": {
                        "title": "Max distance to amenity (m)",
                        "minimum": 100,
                        "maximum": 5000,
                        "type": "integer",
                        "description": "Used with 'Near these amenity types'. 800 m ≈ a 10-minute walk.",
                        "default": 800
                    },
                    "nearAddress": {
                        "title": "Near a specific address / place",
                        "type": "string",
                        "description": "Free-text address or place to anchor on, e.g. 'University of Toronto' or '200 Bay St, Toronto'. Geocoded, then listings are kept within the radius below. Use this for 'near my workplace' (amenity categories can't find a specific employer)."
                    },
                    "nearAddressRadiusM": {
                        "title": "Radius around that address (m)",
                        "minimum": 100,
                        "maximum": 20000,
                        "type": "integer",
                        "description": "Used with 'Near a specific address'.",
                        "default": 2000
                    },
                    "dedupe": {
                        "title": "Collapse cross-source duplicates",
                        "type": "boolean",
                        "description": "Merge the same unit posted to multiple sites into one row (records the extra site in `also_on`).",
                        "default": true
                    },
                    "enrichAmenities": {
                        "title": "Attach nearest-amenity distances",
                        "type": "boolean",
                        "description": "Add `amenity_distances_m` (nearest subway/grocery/school/etc. in metres) from a bundled offline POI index — fast, free, no external API. Off by default. See README.",
                        "default": false
                    },
                    "enrichRadiusM": {
                        "title": "Amenity search radius (m)",
                        "minimum": 250,
                        "maximum": 5000,
                        "type": "integer",
                        "description": "Only used when amenity enrichment is on.",
                        "default": 1500
                    },
                    "maxEnrich": {
                        "title": "Max listings to enrich",
                        "minimum": 1,
                        "maximum": 5000,
                        "type": "integer",
                        "description": "Cap on how many listings are enriched per run. Only used when enrichment is on.",
                        "default": 200
                    },
                    "maxConcurrency": {
                        "title": "Max concurrent requests",
                        "minimum": 1,
                        "maximum": 10,
                        "type": "integer",
                        "description": "Per-source request concurrency. Keep low to stay polite and avoid blocks.",
                        "default": 3
                    },
                    "minDelayMs": {
                        "title": "Min per-request delay (ms)",
                        "type": "integer",
                        "description": "Lower bound of the random delay inserted before each request (politeness).",
                        "default": 500
                    },
                    "maxDelayMs": {
                        "title": "Max per-request delay (ms)",
                        "type": "integer",
                        "description": "Upper bound of the random delay inserted before each request (politeness).",
                        "default": 1500
                    },
                    "userAgent": {
                        "title": "User-Agent override",
                        "type": "string",
                        "description": "Optional. Set a real contact string in production."
                    },
                    "proxyConfiguration": {
                        "title": "Proxy",
                        "type": "object",
                        "description": "All supported sites block datacenter IPs (Cloudflare), so this defaults to RESIDENTIAL IPs, rotated per request. Set apifyProxyCountry to match your Country input (CA for Canada, GB for the UK). Residential proxy is metered (~$8/GB) and draws from your plan credit. Switch the group to datacenter to run free, but expect intermittent blocks.",
                        "default": {
                            "useApifyProxy": true,
                            "apifyProxyGroups": [
                                "RESIDENTIAL"
                            ],
                            "apifyProxyCountry": "CA"
                        }
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
