# Rightmove, Zoopla & OnTheMarket UK Property Listings + Planning (`yappman/uk-property-listings`) Actor

UK property listings for sale and to rent from Rightmove, Zoopla and OnTheMarket on one schema, plus HM Land Registry sold house prices (England and Wales) and optional nearby planning applications. Data-quality figures: data.yappman.com/data-quality

- **URL**: https://apify.com/yappman/uk-property-listings.md
- **Developed by:** [Russell Yapp](https://apify.com/yappman) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 listings

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Rightmove, Zoopla & OnTheMarket UK Property Listings + Planning

Get UK for-sale and to-rent listings from Rightmove, Zoopla and OnTheMarket as clean, consistent JSON, one row per listing, with optional planning data for each.

One real row from a default run (shortened to its core fields):

```json
{
  "portal": "rightmove",
  "listingId": "93988728",
  "url": "https://www.rightmove.co.uk/properties/93988728",
  "listingType": "for-sale",
  "price": { "value": 2495000, "currency": "GBP", "qualifier": null, "period": null },
  "address": "Parklands Drive, London, N3",
  "outcode": "N3",
  "latitude": 51.590223,
  "longitude": -0.207787,
  "propertyType": "Detached",
  "beds": 6,
  "baths": 5,
  "agent": { "name": "Goldschmidt & Howland", "branch": "Temple Fortune - Sales" },
  "publishedAt": "2026-10-06T15:34:47Z"
}
```

A default run (40 London listings from Rightmove and Zoopla) costs $0.125 on the free plan: $0.003 a listing plus $0.005 to start. Paid plans pay less.

**Run it with the default input**

[![Demo: the Homes for sale in Leeds example task in the Apify Console, set to Rightmove and Zoopla](https://data.yappman.com/assets/leeds-property-demo.gif)](https://youtu.be/XumDImDl4kE)

16-second demo on YouTube: https://youtu.be/XumDImDl4kE

### What you get

- **For-sale and to-rent listings from Rightmove and Zoopla, plus Zoopla new homes**, sourced from search results or direct listing pages, on one schema — see "Portal differences" below for what varies by site. OnTheMarket (for sale, to rent and new homes) is opt-in; see "OnTheMarket" below.
- **Optional detail fields** — EPC rating, floor area, tenure, council tax band, UPRN, price history, description, features, floor plans — fetched from each listing's detail page. Not every field is populated on every portal; see "Portal differences".
- **Optional planning constraints** for each listing's coordinates, sourced from the UK government's planning data service (England only). Works the same on every portal.
- **Optional nearby planning applications** for each listing — reference, description, decision state and a link to the council's page — from a nightly copy of PlanIt's UK planning data. See "Planning applications" below.
- **Sold prices (Rightmove only)** — HM Land Registry sale history for an area, one row per sold property at postcode level (no street address, listing link or Rightmove id) — see "Sold prices (Rightmove)" below.

| Group | Fields |
| --- | --- |
| Core (every row) | `portal`, `listingId`, `url`, `listingType`, `status`, `price` (value, currency, qualifier, period), `address`, `outcode`, `latitude`/`longitude`, `coordinatesSource`, `propertyType`, `beds`, `baths`, `receptions`, `newHome`, `chainFree`, `retirement`, `sharedOwnership`, `agent` (name, branch, phone, url), `publishedAt`, `scrapedAt` |
| Detail (with `includeDetails`, or listing mode) | `postcode`, `floorAreaSqft`, `tenure`, `leaseholdYears`, `councilTaxBand`, `epcRating`, `uprn`, `priceHistory`, `description`, `features`, `images`, `floorPlans` |
| Planning constraints (add-on) | `planning.constraints` (`conservationArea`, `listedBuilding`, `floodRiskZone`, `greenBelt`, `article4`, `treePreservationZone`), `planning.coverage`, `planning.checkedAt` |
| Planning applications (add-on) | `planning.applications` (each with `reference`, `authority`, `address`, `description`, `state`, `type`, `receivedAt`, `decidedAt`, `distanceMetres`, `url`, `sourceUpdatedAt`), `planning.coverage.applicationsCoverageFrom`, `planning.coverage.applicationsTruncated`, `planning.coverage.applicationsNote` (what the applications data is and is not) |

### Input

There are three ways to run the Actor.

**1. Paste a search URL.** Filter and search on rightmove.co.uk or zoopla.co.uk, then paste the address-bar URL — the Actor routes each one to its own portal by host. For onthemarket.com, paste an unfiltered area search (see "OnTheMarket"):

```json
{
  "mode": "search",
  "searchUrls": [
    "https://www.rightmove.co.uk/property-for-sale/find.html?locationIdentifier=REGION%5E87490",
    "https://www.zoopla.co.uk/for-sale/property/london/"
  ]
}
```

Accepted Rightmove search URLs: `/property-for-sale/find.html?…` and `/property-to-rent/find.html?…`, plus the plain area pages such as `/property-for-sale/London.html`. Rightmove listing URLs are `/properties/<id>`.

Accepted OnTheMarket search URLs: `/for-sale/property/<place>/`, `/to-rent/property/<place>/` and `/new-homes/property/<place>/`, unfiltered. OnTheMarket listing URLs are `/details/<id>/`.

**2. `locations` with filters and `portals`.** Give place names or postcodes; `portals` decides which site(s) they run on:

```json
{
  "mode": "search",
  "locations": ["Leeds"],
  "portals": ["zoopla"],
  "listingType": "to-rent",
  "minBeds": 2,
  "maxItems": 100
}
```

**3. `listingUrls` in listing mode.** Fetch specific listings directly, on any portal — again routed by host.

Points to know before you run:

- **`portals` defaults to `["rightmove", "zoopla"]`** and controls which site(s) `locations` and the default search run on. Add `"onthemarket"` to include OnTheMarket; it is not in the default run. Pasted `searchUrls`/`listingUrls` ignore `portals` and go to their own portal by host, as above.
- **With nothing entered, one small London search runs on each selected portal.** Clear the example `searchUrls` if you only want `locations`, or narrow `portals` to one site.
- **1,000-listing ceiling per run, shared across portals.** `maxItems` defaults to 40; each selected portal gets an equal share, and a portal that runs out of results passes the rest of its share to the others. `0` means "up to the ceiling". Rightmove and Zoopla cap out around 1,000 results per search (Rightmove's own limit; Zoopla shows at most 40 pages of about 25) and OnTheMarket at 1,020 (34 pages of 30), which may stop a large run earlier than the ceiling.
- **`listingType: "new-homes"` is for Zoopla and OnTheMarket only.** On Zoopla it works with a pasted `/new-homes/` search URL, but not with `locations` — Zoopla's own location search falls back to for-sale for that type. OnTheMarket runs it with both. Rightmove skips `locations` entirely when `new-homes` is selected (pasted Rightmove URLs are unaffected).
- **In listing mode, detail pages are always fetched and charged.** `includeDetails` is ignored there; you get the full detail-page fields on every URL you pass.
- **Filters (`minPrice`, `maxPrice`, `minBeds`, `maxBeds`, `propertyType`, `radiusMiles`, `addedSinceDays`) apply only when `locations` is used, on every portal.** All six property types (detached, semi-detached, terraced, flat, bungalow, land) are mapped on Rightmove and Zoopla. On OnTheMarket the Actor applies the price, bedroom and property-type filters itself, ignores `radiusMiles` with a note, and skips a location when `addedSinceDays` is set or `propertyType` is `land`, with a note (see "OnTheMarket"). A pasted `searchUrls` entry carries whatever filters you set when you copied it (OnTheMarket URLs are accepted only unfiltered). `addedSinceDays` uses each portal's own windows: Zoopla's last 24 hours or last 7 days, Rightmove's 1, 3, 7 or 14 days. On Rightmove, `radiusMiles` uses its own steps (0.5, 1, 3, 5 or 10 miles). Any other value is rounded **down** to the nearest window or step, with a note in the log and in RUN_SUMMARY (`filterNotes`, per portal, for scheduled runs to check), so a search is only ever narrower than asked, never wider: for example, 5 days is the last 24 hours on Zoopla and the last 3 days on Rightmove, and 30 days is 7 days on Zoopla and 14 days on Rightmove. A pasted Rightmove search URL keeps its own filters. Four of them, `tenureTypes`, `mustHave`, `dontShow` and `keywords`, are passed through to Rightmove as-is, exactly as they appear in your URL, and are not verified by this Actor. Any other parameter it cannot keep is named in the log and in RUN_SUMMARY (`ignoredUrlParams`).

### Output

One row per listing. Sample below is abridged and real (a live platform run), with the `planning` block substituted from a different listing in the same run to show its shape. This example is a Zoopla row; Rightmove and OnTheMarket rows use the same schema — see "Portal differences" and "OnTheMarket" below for what's always null and where detail fields come from:

```json
{
  "portal": "zoopla",
  "listingId": "74318505",
  "url": "https://www.zoopla.co.uk/for-sale/details/74318505/",
  "listingType": "for-sale",
  "status": "available",
  "price": { "value": 108750, "currency": "GBP", "qualifier": null, "period": null },
  "address": "Dunedin Road, London E10",
  "postcode": "E10 5NL",
  "outcode": "E10",
  "latitude": 51.557804,
  "longitude": -0.012582,
  "coordinatesSource": "listing",
  "propertyType": "flat",
  "beds": 2,
  "baths": 2,
  "floorAreaSqft": 797,
  "tenure": "leasehold",
  "leaseholdYears": 119,
  "councilTaxBand": "C",
  "epcRating": "B",
  "uprn": "10093560110",
  "priceHistory": [],
  "sharedOwnership": true,
  "agent": {
    "name": "Newlon Living",
    "branch": "Resales",
    "phone": "020 3463 1588",
    "url": "https://www.zoopla.co.uk/new-homes/developers/branch/newlon-living-resales-hale-village-105696/"
  },
  "scrapedAt": "2026-09-24T13:13:54Z",
  "planning": {
    "applications": null,
    "constraints": { "conservationArea": null, "listedBuilding": null, "floodRiskZone": "2", "greenBelt": false, "article4": false, "treePreservationZone": false },
    "coverage": {
      "applications": "not-requested",
      "constraints": "planning.data.gov.uk",
      "constraintsCoverageNote": "England only; official datasets are incomplete, so absence is not proof of no constraint; listedBuilding means a listed building or structure within 25 m, not that this property is listed"
    },
    "checkedAt": "2026-09-24T14:22:00Z"
  }
}
```

Notes on the data:

- **Rentals** carry the price and period as Zoopla displays them (typically `pcm`; some listings show no period at all — Zoopla's own "fixed price" rentals).
- **`priceHistory`** is a list of change events with dates only (`"price-drop"`, `"price-rise"`) — Zoopla does not show the amounts, so `price` is always `null` in each entry.
- **`agent`** is the marketing branch handling the listing: the agency's business details (some agents trade under a person's name).

### Portal differences

Zoopla and Rightmove return the same schema; a handful of fields behave differently by source (OnTheMarket's differences are in its own section below):

| Field | Zoopla | Rightmove |
| --- | --- | --- |
| `uprn` | From the detail page | Always `null` — not published on any Rightmove page |
| `priceHistory` | Change events, dates only | Always `[]` — Rightmove shows no price history |
| `epcRating` | A letter, from the detail page | Always `null` — Rightmove only shows the EPC as an image, never as text |
| `status` | `available` / `under-offer` / `sold-stc` / `let-agreed`, from search results | Always `null` — no reliable status signal on Rightmove pages |
| `newHome` | Set where Zoopla shows it | Set for Rightmove new-homes developments |
| Full postcode, tenure, lease years, floor area, council tax band, floor plans, images, description, key features, agent (brand, branch, business phone) | From the detail page with `includeDetails` | Same — all come from the Rightmove detail page with `includeDetails` |

`agent` is a business on every portal (brand/company name, branch, phone) — never a person.

### OnTheMarket

OnTheMarket is opt-in: add `"onthemarket"` to `portals`. The default stays `["rightmove", "zoopla"]`, so existing inputs run as before.

**What works**

- For-sale, to-rent and new-homes listings, on the same schema.
- Pasted search URLs: an unfiltered area search such as `https://www.onthemarket.com/for-sale/property/leeds/` (also `/to-rent/property/<place>/` and `/new-homes/property/<place>/`).
- Typed `locations`: a place name or an outcode such as `SW18`, resolved with OnTheMarket's own place lookup. When a name matches several places, the Actor uses the one whose OnTheMarket id is the plain name, if there is one (for "Leeds", `leeds`). If none is, that location is skipped, with a note in the log and in RUN_SUMMARY (`filterNotes`) listing the choices (as many as fit), so you can paste the search URL for the right place. A name that matches nothing fails that location.
- Listing URLs: `https://www.onthemarket.com/details/<id>/`.

**Filters**

- `minPrice`, `maxPrice`, `minBeds`, `maxBeds` and `propertyType` are applied by the Actor to the rows it fetches, not by OnTheMarket: its robots.txt disallows its own filter parameters, so the Actor never sends them.
- OnTheMarket shows at most 1,020 results (34 pages of 30) per search, and the filters apply after that cap. A narrow filter over a large area therefore returns fewer rows than on the other portals; a smaller area, such as an outcode, is less likely to reach the cap.
- These filters apply to `locations` only. A pasted OnTheMarket URL is always unfiltered, and they are not applied to it.
- With `propertyType` set, a row is kept only when OnTheMarket's own type maps to it. Types that map to none of the six (seen: "House", "House share", "Cottage") are never kept. `land` has no OnTheMarket type the Actor can match, so an OnTheMarket location with `propertyType: land` is skipped, with a note in the log and in `filterNotes`. With a bedroom filter set, a row with no bedroom count is dropped (a studio counts as 0).
- A filtered search stops after 3 pages in a row with no matching rows, with a note in the log and in `filterNotes`. It does not page on through all 34 for nothing, so a filter that matches only rows far down the results can be missed.
- RUN_SUMMARY `filterNotes` records that the filters were applied by the Actor, to at most 1,020 rows. When a search is read to its end, the run log also says how many of the fetched rows were kept.
- `radiusMiles` is not supported: it is ignored, with a note in `filterNotes`. `addedSinceDays` is not supported either, and ignoring it would search wider than you asked (and charge for every row), so an OnTheMarket location with `addedSinceDays` set is skipped, with a note in the log and in `filterNotes`, not searched wider. Other portals in the same run are unaffected.
- A pasted OnTheMarket search URL that carries OnTheMarket's own filter parameters (any query parameter other than `page`) is not accepted. It is reported as unrecognised (`unrecognisedUrls` in RUN_SUMMARY) and not fetched.

**Field differences**

- Always `null`: `epcRating` (OnTheMarket shows the EPC only as an image), `status`, `uprn`, `chainFree`, `retirement`, `publishedAt` and `receptions`. `priceHistory` is always `[]`.
- `tenure`, `leaseholdYears`, `councilTaxBand`, `floorAreaSqft`, `description`, `floorPlans` and `newHome` come only with `includeDetails` (always fetched in listing mode); search results carry none of them. `councilTaxBand` is set only when the page shows a band.
- `postcode` comes from the listing page, only when the page confirms it is the same listing and gives a valid full postcode; otherwise it is `null`. `outcode` is taken from it, so both are `null` without `includeDetails`.
- `propertyType` is one of the Actor's six types where OnTheMarket's own label maps to one, and OnTheMarket's own text otherwise (for example "House" or "Cottage"). This differs from Rightmove and Zoopla, which pass their own text through.

**Pricing and limits.** The same events and prices as the other portals. `maxItems` is shared across all selected portals, each getting an equal share.

### Planning constraints

The add-on looks up official constraints at each listing's coordinates from the UK government's planning data service, which covers **England only**.

- **`conservationArea`** — name(s) of any conservation area covering the point.
- **`listedBuilding`** — the nearest listed building or structure **within 25 metres** of the listing's coordinates, with its name, grade, and list reference. This does **not** mean the property itself is listed: the official dataset holds listed buildings as points, not building outlines, so the nearest point within range can be anything nearby — for example, a listed lamp post rather than the house itself.
- **`floodRiskZone`**, **`greenBelt`**, **`article4`**, **`treePreservationZone`** — whether the point falls in a flood risk zone, the green belt, an Article 4 direction area, or a tree preservation zone.

The official datasets are known to be incomplete, so **a missing constraint is not proof that none exists**. Outside England, `planning.coverage.constraints` is set to `"not-covered"` and nothing is charged for that listing.

#### Planning applications

Set `includePlanningApplications` to add the planning applications near each listing: those within `planningRadiusMetres` (150 m by default, 25–500) received in the last `planningYears` (1 by default, up to 3). For each one you get the reference, council, address, description, decision state, type, received and decided dates, distance from the listing, and a link to the council's own page. Up to the 100 nearest are returned, nearest first; `planning.coverage.applicationsTruncated` says when there were more. Descriptions are the council's text, lightly redacted (a name after a title such as Mr, Mrs or Dr is removed), and the applicant and agent name fields are never kept.

The applications come from a nightly copy of [PlanIt](https://www.planit.org.uk)'s UK planning data. At launch it holds about the last 12 months, and it deepens every night. **Applications received before the copy's start date are never included, whatever `planningYears` is set to, and nor are applications with no received date** (they cannot be shown to fall inside the window): every answered row carries that date as `planning.coverage.applicationsCoverageFrom`, and `sourceUpdatedAt` on each application says when that record was last copied. The newest applications arrive a few days late: the copy refreshes recent weeks in a rolling cycle, and if its newest complete day is more than 5 days old the add-on counts the copy as out of date. Decisions on applications received more than two months ago can lag by up to a few weeks; `sourceUpdatedAt` shows when each record was last copied.

**"None nearby" is charged, because it is an answer.** Where the roughly 1 km grid cells around a listing hold applications from a council PlanIt collects, a listing with none within the radius costs the same as one with many. It is never charged when the copy is out of date or cannot be read, or when those grid cells hold no applications from a council that counts (county councils and other non-local planning bodies do not) (see the next paragraph): those listings show `planning.coverage.applications: "unavailable"` or `"not-covered"` and are free.

**Areas PlanIt does not collect are free.** The copy keeps a list of the councils PlanIt is currently collecting (a council counts when PlanIt's latest date for it is within the last 90 days). The councils of the applications held in the roughly 1 km grid cells around the listing decide, all of them, not only those within the radius; county councils and other non-local planning bodies don't count as the local council. If any of those councils is currently collected, the listing is answered (and charged) as above. If none is, the listing shows `planning.coverage.applications: "not-covered"`, with no applications, and is not charged. It means PlanIt does not currently collect that area, not that nothing has been applied for there. When those cells hold no applications from a council that counts (for example a council PlanIt lists but has nothing for), the listing is never charged: the 10 km grid square around it is checked, and if it has no applications from a council PlanIt is currently collecting the listing is `"not-covered"`, and otherwise `"unavailable"`. Until the copy has finished indexing every area (and whenever the council list cannot be read, is out of date or looks wrong), an older rule applies instead, and no listing is answered "not-covered": the listing is charged only when the copy holds at least one application in the grid cells around it, even outside the radius; if it holds none, the listing shows `"unavailable"` and is free. Near a council boundary this check can't tell councils apart within about 1 km: if a covered neighbour's applications are in the grid cells around the listing (each about 1 km across), the listing counts as covered and is answered (and charged) even when its own council isn't one PlanIt collects.

### Sold prices (Rightmove)

Set `mode: "sold-prices"` for HM Land Registry sale history instead of live listings — Rightmove only. One row per sold property, at **postcode level**: no street address, no Rightmove listing link, no Rightmove property id, and no coordinates taken from Rightmove.

This is a licensing choice, not a technical limit. The sale prices and dates are HM Land Registry's Price Paid Data, open under the Open Government Licence, but the addresses attached to them come from Royal Mail's address file via Ordnance Survey, and that licence permits the address data only for personal/non-commercial use or for display in a residential price-information service; any other use, such as handing addresses out in a dataset, needs Royal Mail's permission. So a row carries only the postcode, and its coordinates are the postcode's own centroid (open data), never the property's. A postcode averages about 15 addresses, though a few cover only one — treat a row as postcode-level data, not as anonymous.

Each row carries:

- `propertyId` — an id for the property (a keyed hash of Rightmove's own id, never that id itself or the listing link it would rebuild). It stays the same from run to run for as long as Rightmove keeps its own id for the property.
- `postcode`, `outcode`.
- `latitude`/`longitude`, `coordinatesSource: "postcode-centroid"` — the postcode's centroid from postcodes.io; both null when the postcode can't be placed (the row still ships, since its price data is present).
- `propertyType` (`detached`, `semi-detached`, `terraced`, `flat`, `other`), `beds`, `baths`.
- `lastSoldPrice`, `lastSoldDate` — the most recent sale.
- `sales` — every recorded sale since 1995, newest first: `price`, `currency` (always `GBP`), `date`, `tenure`, `newBuild`.
- `area` — the area searched, `{ name, type }`.
- `landRegistryUpdatedAt` — when HM Land Registry's data was last loaded into the source.
- `scrapedAt`, `portal: "rightmove"`.

Sample row (illustrative — `propertyId` shown is a placeholder, not a real hash):

```json
{
  "portal": "rightmove",
  "propertyId": "3f9a1c0e7b2d4a68",
  "postcode": "LS8 4AX",
  "outcode": "LS8",
  "latitude": 53.824937,
  "longitude": -1.510024,
  "coordinatesSource": "postcode-centroid",
  "propertyType": "terraced",
  "beds": 2,
  "baths": 2,
  "lastSoldPrice": 272500,
  "lastSoldDate": "2026-07-31",
  "sales": [
    { "price": 272500, "currency": "GBP", "date": "2026-07-31", "tenure": "freehold", "newBuild": false },
    { "price": 202500, "currency": "GBP", "date": "2019-02-07", "tenure": "freehold", "newBuild": false }
  ],
  "area": { "name": "Leeds", "type": "region" },
  "landRegistryUpdatedAt": "2026-09-17",
  "scrapedAt": "2026-09-25T09:56:09Z"
}
```

**Input.** `searchUrls` and `locations` are read differently in this mode:

- `searchUrls` — paste a Rightmove `/house-prices/<area>.html` URL for a region or an outcode (for example `https://www.rightmove.co.uk/house-prices/leeds.html` or `.../ls6.html`); a street, station or postcode page is skipped, with the reason in RUN_SUMMARY. Its `soldIn` and `propertyType` filters are kept; any other parameter is dropped and named in RUN_SUMMARY (`ignoredUrlParams`).
- `locations` — a place name (resolved through Rightmove's own location search; only region-type matches are supported so far) or an outcode such as `LS6`. Full postcodes, stations and streets are not supported yet.
- With nothing entered, one small area runs by default: `https://www.rightmove.co.uk/house-prices/ls6.html`. The listing form's prefilled London `searchUrls` examples are ignored in this mode.

**Filters, for typed `locations` only** (a pasted URL carries its own):

- `soldWithinYears` — sales within the last N years. Rightmove's own windows are 1, 2, 3, 5, 10, 15, 20 and 30 years; any other value is rounded **down** to the nearest window, with a note in the log and in RUN_SUMMARY (`filterNotes`). Leave it empty for all years back to 1995.
- `propertyType` — `detached`, `semi-detached`, `terraced` or `flat`. `bungalow` and `land` have no sold-prices equivalent: a location searched with either is skipped, with the reason in RUN_SUMMARY.
- `minPrice`, `maxPrice`, `minBeds`, `maxBeds`, `radiusMiles`, `addedSinceDays`, `includeDetails` and the planning add-ons don't apply to sold prices; if set, they're named in RUN_SUMMARY (`filterNotes`) and otherwise ignored. Radius, tenure and sort order aren't offered yet.

**Limits.** Up to 1,000 rows per area (Rightmove's own cap: 40 pages of 25), and `maxItems` per run as in the other modes. Each row's `sales` list is newest first. Coverage is England and Wales only (HM Land Registry): an area whose sales are not HM Land Registry data is skipped before any row is read (nothing charged), and a row whose postcode is outside England and Wales is skipped and counted in RUN_SUMMARY (`rowsSkipped.outsideEnglandAndWales`). A record whose postcode can't be read from Rightmove's address string is skipped — not output, not charged — and counted in RUN_SUMMARY (`rowsSkipped.noPostcode`).

### Pricing

Pay per event. Platform usage is included in the price — you don't need your own proxy. The same events and prices apply to every listing row, whichever portal it came from, and the planning add-ons work the same on all of them. Sold-price rows (`sold-prices` mode) have their own event and no add-ons.

| Event | Charged when | Price |
| --- | --- | --- |
| `run-start` | Once per run, only when the first rows (listings or sold prices) reach your dataset — a run that returns nothing costs nothing | $0.005 per run |
| `listing` | Each listing row returned | $3.00 per 1,000 |
| `listing-detail` | Each detail page fetched (`includeDetails`, or always in listing mode) | $5.00 per 1,000 |
| `planning-constraints` | Each listing where the lookup succeeds for a point in England | $5.00 per 1,000 |
| `planning-applications` | Each listing where the planning-applications lookup answers, including "none nearby"; not for `not-covered` or `unavailable` (see Planning applications) | $10.00 per 1,000 |
| `sold-price` | Each sold-price row returned (`sold-prices` mode) | $3.00 per 1,000 |

Higher Apify plans get tiered discounts on these prices (up to 30% at Gold and above).

**Worked examples:**

- Default run, 40 listings, no add-ons: 40 × $0.003 + $0.005 ≈ **$0.125**.
- 200 listings with details and planning constraints: 200 × $0.013 + $0.005 ≈ **$2.61**.

Use Apify's maximum charge per run setting to cap spend on any run.

### Reliability

If a run is blocked, it says so plainly: the run status and the `RUN_SUMMARY` record in the run's key-value store report the block, and nothing is charged for it. The Actor never emits placeholder or sentinel rows to make a run look successful. For an up-to-date, independently measured success rate, see this Actor's statistics on the Apify Store page rather than any number here.

Rightmove and OnTheMarket requests are each throttled to at most one a second per site, across all of a run's sessions. Apify Proxy handles all portals — datacenter first for Rightmove and OnTheMarket, residential for Zoopla — with platform usage included in the price, so there's nothing for you to configure.

### Limits and known gaps

- Some detail fields are only shown for some listings. In particular, on Zoopla, EPC rating, council tax band, floor area and UPRN are often missing on new-homes listings, and new-homes detail pages carry no full postcode.
- Price-on-application listings have not yet been seen in practice; support for them is unconfirmed.
- On Zoopla, `status` reflects what search results show, which in practice is `available` or `under-offer`. Rightmove rows always carry `status: null` — see "Portal differences".
- Commercial listings on Rightmove are skipped.
- OnTheMarket shows at most 1,020 results per search, and the Actor's filters apply after that cap; see "OnTheMarket" above.
- Sold prices (Rightmove) are postcode-level only — no street address, listing link or Rightmove id; see "Sold prices (Rightmove)" above. Full-postcode, station and street inputs aren't supported for sold prices yet.

### Read more

- [Planning applications and constraints next to UK property listings (Rightmove, Zoopla, OnTheMarket)](https://dev.to/yappman/planning-applications-and-constraints-next-to-uk-property-listings-rightmove-zoopla-onthemarket-2kic): what the planning add-ons and sold prices return, a worked example with real output, pricing, and limits.

### Data and legal

This Actor is not affiliated with or endorsed by Rightmove, Zoopla or OnTheMarket. Searches are user-directed (you supply the search or listing URLs) and every run is capped at 1,000 rows — there is no whole-site crawl mode. The Actor respects Rightmove's robots.txt: for example, it never fetches sold-price detail pages. Every OnTheMarket request is checked against OnTheMarket's robots.txt too, which is why OnTheMarket's own filter parameters are never sent. The Actor stores nothing beyond the run itself.

On personal data: agent details are the marketing branch's business details (branch name, office phone, URL). Listing descriptions are the agent's own text and can occasionally mention a member of staff. No vendor or landlord details are collected. No name fields are taken from planning records; application descriptions are the council's own text with titled names (Mr, Mrs, Dr and so on) removed, and may occasionally still mention a person.

Planning constraints contain public sector information licensed under the Open Government Licence v3.0 (planning.data.gov.uk). Planning applications courtesy of PlanIt (planit.org.uk).

Sold prices (Rightmove) contain HM Land Registry data © Crown copyright and database right 2026. This data is licensed under the Open Government Licence v3.0. Postcode centroids: contains OS data © Crown copyright and database right 2026. Contains Royal Mail data © Royal Mail copyright and database right 2026. Source: Office for National Statistics licensed under the Open Government Licence v.3.0.

You are responsible for your own use of the data and for complying with applicable terms and law.

### FAQ

**Do I need my own proxies?** No. Apify Proxy is used as needed and included in the event pricing.

**Which portal is used?** Whichever site(s) you list in `portals` (default: Rightmove and Zoopla; add `onthemarket` to include it) — it applies to `locations` and the default search. Pasted `searchUrls` and `listingUrls` always go to their own portal, detected from the host.

**Why are some Rightmove fields empty?** Rightmove doesn't publish everything Zoopla does: `uprn` and `priceHistory` are always `null`/`[]` on Rightmove rows, and `epcRating` is always `null` because Rightmove only shows the EPC as an image, never as text. `status` is always `null` too. See "Portal differences" above.

**Why did a run charge nothing?** It returned no rows (for example, it was blocked). `run-start` is only charged once the first rows reach your dataset, so an empty run costs nothing.

**Can I monitor new listings daily?** Schedule the Actor from the Apify Console with `locations` and `addedSinceDays: 1` (a daily run) or `7` (a weekly one); each scheduled run only charges for what it finds. Other values are rounded down to each portal's nearest window (see Input). OnTheMarket cannot apply `addedSinceDays`, so with it set an OnTheMarket location is skipped, with a note, instead of being searched wider; the other portals in the run are unaffected.

**Why are some OnTheMarket fields empty?** `epcRating`, `status`, `uprn`, `chainFree`, `retirement`, `publishedAt` and `receptions` are always `null` on OnTheMarket rows, and `priceHistory` is `[]`; tenure, lease years, council tax band, floor area and postcode need `includeDetails`. See "OnTheMarket" above.

**What does `listedBuilding` mean?** The nearest listed building or structure within 25 metres of the listing — not that the listing itself is listed. See "Planning constraints" above.

### Changelog

See [CHANGELOG.md](./CHANGELOG.md) for release notes.

### Need something custom?

I build and maintain Actors for retail pricing, property listings and sites behind Akamai or Cloudflare, plus scheduled data pipelines with monitoring. Fixed-scope work through Abley-Yapp Consulting — email info@yappman.com with the site and the fields you need.

# Changelog

This Actor's version history is a separate document: https://apify.com/yappman/uk-property-listings/changelog.md

# Actor input Schema

## `mode` (type: `string`):

Chooses what the run does: search listings, fetch given listing URLs, or return sold prices (Rightmove, England and Wales, postcode level). Listing mode always fetches and charges detail pages. All modes.

## `portals` (type: `array`):

Portals that `locations` and the default search run on, 1 to 3 of rightmove, zoopla, onthemarket. Pasted URLs go to their own portal. `maxItems` is shared across portals. Search mode.

## `searchUrls` (type: `array`):

Portal search pages, up to 50, each run with its own filters. Leave empty unless the user supplied a URL; never write one. In sold-prices mode, a Rightmove house-prices area page. Search and sold-prices modes.

## `locations` (type: `array`):

Place names to search, up to 50, on each selected portal; Rightmove and OnTheMarket also take outcodes such as LS6. In sold-prices mode, a place name or outcode; full postcodes are refused. Search and sold-prices modes.

## `listingType` (type: `string`):

Whether `locations` searches look for homes for sale, to rent or new homes. Only OnTheMarket takes new-homes with `locations`; Rightmove and Zoopla skip them with a note. Search mode.

## `minPrice` (type: `integer`):

Minimum price in GBP, 0 or more, for `locations` searches. Search mode.

## `maxPrice` (type: `integer`):

Maximum price in GBP, 0 or more, for `locations` searches. Search mode.

## `minBeds` (type: `integer`):

Minimum number of bedrooms, 0 to 20, for `locations` searches. Search mode.

## `maxBeds` (type: `integer`):

Maximum number of bedrooms, 0 to 20, for `locations` searches. Search mode.

## `propertyType` (type: `string`):

Limits `locations` searches to one property type. Sold prices has no bungalow or land and OnTheMarket has no land, so those locations are skipped with a note. Search and sold-prices modes.

## `radiusMiles` (type: `number`):

Search radius in miles around each location, 0 to 40, for `locations` searches. Rightmove rounds down to 0, 0.5, 1, 3, 5 or 10; OnTheMarket ignores it, with a note. Search mode.

## `addedSinceDays` (type: `integer`):

Only listings added in the last N days, 1 to 365, for `locations`. Rightmove rounds down to 1, 3, 7 or 14, Zoopla to 1 or 7; OnTheMarket locations are skipped with a note. Search mode.

## `soldWithinYears` (type: `integer`):

Only sales in the last N years, 1 to 30, for typed `locations`; rounded down to 1, 2, 3, 5, 10, 15, 20 or 30. Empty means all years. Sold-prices mode.

## `listingUrls` (type: `array`):

Links to individual listing pages on Rightmove, Zoopla or OnTheMarket, 1 to 1,000, required in listing mode. Leave empty unless the user supplied a URL; never write one. Listing mode.

## `maxItems` (type: `integer`):

The most rows a run returns, 0 to 1,000; 0 means 1,000. Shared across portals in search mode. All modes.

## `includeDetails` (type: `boolean`):

Fetches each listing's detail page for extra fields such as EPC rating, floor area, tenure and council tax band. Charged as listing-detail; see Pricing. Listing mode always fetches them. Search mode.

## `includePlanningConstraints` (type: `boolean`):

Adds official planning constraints at each listing's coordinates, England only. Charged as planning-constraints when answered for a point in England; see Pricing. Search and listing modes.

## `includePlanningApplications` (type: `boolean`):

Adds nearby planning applications from a copy of PlanIt's data, leaving out undated ones and those before the copy's start date. Charged as planning-applications, not for not-covered areas; see Pricing. Search and listing modes.

## `planningRadiusMetres` (type: `integer`):

How far from each listing to look for planning applications, 25 to 500 metres. Used with `includePlanningApplications`. Search and listing modes.

## `planningYears` (type: `integer`):

How many years back to look for planning applications, 1 to 3; nothing before the copy's start date is included. Used with `includePlanningApplications`. Search and listing modes.

## Actor input object example

```json
{
  "mode": "search",
  "portals": [
    "rightmove"
  ],
  "searchUrls": [
    "https://www.rightmove.co.uk/property-for-sale/find.html?locationIdentifier=REGION%5E87490"
  ],
  "locations": [
    "Leeds"
  ],
  "listingType": "to-rent",
  "minPrice": 100000,
  "maxPrice": 400000,
  "minBeds": 2,
  "maxBeds": 4,
  "propertyType": "terraced",
  "radiusMiles": 3,
  "addedSinceDays": 7,
  "soldWithinYears": 5,
  "listingUrls": [],
  "maxItems": 100,
  "includeDetails": false,
  "includePlanningConstraints": false,
  "includePlanningApplications": false,
  "planningRadiusMetres": 250,
  "planningYears": 2
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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 = {
    "mode": "search",
    "portals": [
        "rightmove",
        "zoopla"
    ],
    "searchUrls": [
        "https://www.rightmove.co.uk/property-for-sale/find.html?locationIdentifier=REGION%5E87490",
        "https://www.zoopla.co.uk/for-sale/property/london/"
    ],
    "locations": [],
    "listingType": "for-sale",
    "listingUrls": [],
    "maxItems": 40
};

// Run the Actor and wait for it to finish
const run = await client.actor("yappman/uk-property-listings").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 = {
    "mode": "search",
    "portals": [
        "rightmove",
        "zoopla",
    ],
    "searchUrls": [
        "https://www.rightmove.co.uk/property-for-sale/find.html?locationIdentifier=REGION%5E87490",
        "https://www.zoopla.co.uk/for-sale/property/london/",
    ],
    "locations": [],
    "listingType": "for-sale",
    "listingUrls": [],
    "maxItems": 40,
}

# Run the Actor and wait for it to finish
run = client.actor("yappman/uk-property-listings").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "mode": "search",
  "portals": [
    "rightmove",
    "zoopla"
  ],
  "searchUrls": [
    "https://www.rightmove.co.uk/property-for-sale/find.html?locationIdentifier=REGION%5E87490",
    "https://www.zoopla.co.uk/for-sale/property/london/"
  ],
  "locations": [],
  "listingType": "for-sale",
  "listingUrls": [],
  "maxItems": 40
}' |
apify call yappman/uk-property-listings --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,yappman/uk-property-listings"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/ljdTMPGN1IN7DGtlj/builds/WT0A3iO4l4QJA2f2V/openapi.json
