# OnTheMarket Scraper - UK Property Data, Price & Tenure (`neverempty/onthemarket-listings`) Actor

OnTheMarket.com listings for sale or to rent as clean JSON: price as a number with its meaning (sale or monthly rent, weekly rent from the page), tenure with lease years, bedrooms, bathrooms, features, coordinates, the agency and added/reduced status. Filters cost no extra requests. Unofficial.

- **URL**: https://apify.com/neverempty/onthemarket-listings.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Real estate, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.45 / 1,000 property returneds

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?

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.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — 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

## OnTheMarket Scraper - UK Property Data, Price & Tenure

Pull property listings from **OnTheMarket.com** — the UK's third-largest property portal — as clean JSON: the price as a number and what it means, tenure with the lease years remaining, bedrooms, bathrooms, features, coordinates and the agency, for sale or to rent, anywhere in the UK.

*Unofficial. This Actor is not affiliated with, endorsed by, or sponsored by OnTheMarket. All product names are trademarks of their respective owners.*

No API key. Reads the same public pages a visitor sees.

***

### Run it tomorrow and you only get what is new

Schedule this Actor with `monitoringMode: true` and each run returns **only the listings it has not returned before**. Not "here is everything again, please de-duplicate it yourself" — the Actor remembers what it already gave you, so a daily feed of a 4,000-listing search costs you the handful of new homes, not 4,000 rows every morning. **You are never charged for the same property twice.**

Listings your filters removed are remembered too, so widening a filter later does not resurrect them as false "new" listings. Listings that did not fit under your limit are **not** remembered, so they are still waiting on the next run instead of being silently lost.

***

### Why this one is different

#### The price is a number that means what it says

OnTheMarket publishes the price as text — `£550,000` on a sale, `£1,550 pcm (£358 pw)` on a rental. This Actor reads it into `price` and labels it: `priceType: "sale"` (median in the 86 sales measured: **£650,000**) or `priceType: "monthly-rent"` (median in the 28 rentals: **£1,410**), with `rentPerWeek` taken from OnTheMarket's own bracket rather than calculated. A listing whose price text carries no figure comes back as `price: null` with `isPriceOnApplication: true` — never a zero. OnTheMarket's qualifier (`Guide price`, `Offers over`, `Shared ownership`) is kept in `priceQualifier`.

#### Tenure is a value, and the lease length is another

OnTheMarket only states the tenure inside the feature bullets — `Tenure: Leasehold (999 years remaining)`. This Actor lifts it out into `tenure` (`FREEHOLD`, `LEASEHOLD`, `SHARE_OF_FREEHOLD`) and `leaseYearsRemaining` (a number, on 35 of the 114 listings measured), and removes the bullet from `features` so it is not sold twice. The shortest lease in the sample was 59 years.

#### Advertising slots are not sold as properties

Up to two of the thirty cards on a list page are advertising slots (`ad?: true`), not listings. They are dropped before counting, so they are never delivered or charged.

#### No branch phone numbers

OnTheMarket's card data includes the marketing branch's telephone on **every one of the 114 listings measured**. This Actor returns the agency name and its branch page and leaves the phone out; UK phone numbers written into the address or features are removed too. Property data, not a contact list.

#### An unknown place is a 404, and it is reported as one

OnTheMarket answers a place it does not know with an honest 404. This Actor returns a `no-such-search` row that says how OnTheMarket spells its places — distinct from `no-results`, which is its own "zero listings" answer. OnTheMarket serves at most 34 pages per search and redirects anything beyond to its front page; that is treated as "no more pages", never as an empty search. A page that fails for any other reason is reported as `unreadable`, with a note saying which pages were delivered before it. All of these rows are free.

#### Filters run here, because OnTheMarket's robots.txt says so

OnTheMarket's `robots.txt` disallows the price, bedroom, type and radius query parameters and the `/flat/`, `/apartment/` and `*-bed-` paths. This Actor requests only `/for-sale/property/<place>/` or `/to-rent/property/<place>/` plus `?page=N`, and every filter — price, bedrooms, bathrooms, type, tenure, shared ownership, developments, recently added, keywords, agency — runs on the rows already fetched. Nothing extra is requested and nothing extra is charged. A property is never dropped for a value OnTheMarket did not publish.

***

### Input

Either paste a search URL, or build one from the fields below.

| Field | Type | Default | Description |
|---|---|---|---|
| `searchUrl` | string | — | e.g. `https://www.onthemarket.com/for-sale/property/london/`. Query parameters and type paths are dropped |
| `channel` | string | `sale` | `sale` or `rent` |
| `location` | string | `london` | A town (`london`, `manchester`, `milton-keynes`) or a postcode area (`sw1a`, `m1`) |
| `maxListings` | integer | 20 | 1-950. One property = one row = one charged event. OnTheMarket serves at most 34 pages (about 950 properties) per search |
| `maxPages` | integer | 0 | Hard cap on requests; 0 = decide from `maxListings`. 34 at most |
| `useProxy` | boolean | `false` | Off by default — OnTheMarket answers Apify's network directly. Switches on by itself if blocked |
| `keywords` / `keywordMatch` / `excludeKeywords` | array / string / array | — | Match on title, address, type, features, labels, price qualifier, tenure, added/reduced text and agency |
| `agencyNames` | array | — | Keep only these agencies (partial match) |
| `minPrice` / `maxPrice` | integer | 0 | GBP. Sale total or rent per calendar month. **A property with no price is kept** |
| `requirePrice` | boolean | `false` | Drop listings with no published price |
| `minBedrooms` / `maxBedrooms` | integer | -1 | -1 = off. **An unpublished count is kept** |
| `minBathrooms` | integer | -1 | -1 = off |
| `propertyTypes` | array | — | Partial match on OnTheMarket's words: `Apartment`, `Flat`, `Terraced house`, `Detached house`, `Bungalow`, `Studio` … |
| `tenureTypes` | array | — | `FREEHOLD`, `LEASEHOLD`, `SHARE_OF_FREEHOLD`. **An unpublished tenure is kept** |
| `excludeSharedOwnership` | boolean | `false` | Drop shared-ownership listings |
| `excludeDevelopments` | boolean | `false` | Drop developer listings that advertise a range of homes |
| `recentlyAddedOnly` | boolean | `false` | Keep only listings OnTheMarket flags as recently added |
| `monitoringMode` | boolean | `false` | Return only properties not returned on a previous run |
| `resetMonitoringState` | boolean | `false` | One-shot: forget what was already returned |

```json
{ "channel": "sale", "location": "manchester", "maxListings": 100,
  "minBedrooms": 3, "maxPrice": 400000, "tenureTypes": ["FREEHOLD"], "excludeSharedOwnership": true, "excludeDevelopments": true }
```

#### Monitoring mode turns this into a daily feed

OnTheMarket lists in its own "recommended" order, so to catch new stock anywhere in the results set `maxListings` high (up to 950): every page is read, but only the listings not returned before are delivered and charged.

Schedule it and set `monitoringMode: true`: each run returns **only the properties it has not returned before**. Properties removed by your filters are remembered too, so changing a filter later does not resurrect them as false "new" listings — and properties that simply **did not fit under `maxListings`, or were cut off by the run's charge limit, are not remembered**, so they are still waiting for you on the next run rather than being silently lost.

Measured on 2026-09-05: **50,000+** properties for sale in London (OnTheMarket caps the count at 50,000); **2,919** to rent in Manchester; **53** for sale in SW1A.

***

### Output

One row per property:

```json
{
  "source": "onthemarket.com",
  "status": "ok",
  "listingId": "20268229",
  "title": "3 bedroom chalet for sale",
  "address": "David Drive, Harold Park",
  "url": "https://www.onthemarket.com/details/20268229/",
  "searchedLocation": "Property & houses for sale in London",
  "transactionType": "sale",
  "propertyType": "Chalet",
  "bedrooms": 3,
  "bathrooms": 2,
  "price": 550000,
  "priceCurrency": "GBP",
  "priceType": "sale",
  "priceDisplay": "£550,000",
  "priceQualifier": null,
  "isPriceOnApplication": false,
  "rentPerMonth": null,
  "rentPerWeek": null,
  "isSharedOwnership": false,
  "tenure": "FREEHOLD",
  "leaseYearsRemaining": null,
  "features": ["Three Bedrooms", "Semi Detached", "Great Condition"],
  "labels": [],
  "latitude": 51.600282905437,
  "longitude": 0.243168901765,
  "addedOrReducedText": "Added yesterday",
  "mainLabel": "Added yesterday",
  "isRecentlyAdded": true,
  "isReduced": false,
  "isPremium": false,
  "isExclusive": true,
  "isSpotlight": false,
  "hasVirtualTour": false,
  "agencyName": "haart Estate Agents - Harold Wood",
  "agencyUrl": "https://www.onthemarket.com/agents/branch/haart-estate-agents-harold-wood/",
  "isDevelopment": false,
  "developmentFeatures": [],
  "mainImageUrl": "https://media.onthemarket.com/properties/20268229/1641899749/image-0-480x320.jpg",
  "scrapedAt": "2026-09-05T08:36:33.126Z"
}
```

Measured across 114 live listings (113 unique) on 2026-09-05: id, title, address, url, type, price, coordinates, agency and added/reduced text on **114 of 114**; bedrooms on **110**; bathrooms on **107**; tenure on **85**; a lease length on **35**; a price qualifier on **51**. OnTheMarket does not publish a listing date on the list page — only the text `Added today` / `Reduced yesterday` / `Added > 14 days` and a recently-added flag, both passed through.

#### Rows that are never charged

| `status` | when |
|---|---|
| `no-results` | OnTheMarket reports zero properties for this search. That is its answer, not a failure |
| `no-such-search` | OnTheMarket has no page at that address (HTTP 404) - a place spelled differently from OnTheMarket's addresses. **Not** a claim that there is nothing there |
| `no-filter-match` | Listings were read, but your filters removed all of them. The row says how many were read |
| `no-new-listings` | Monitoring mode: nothing new since the previous run |
| `unreadable` | The page could not be read, its embedded data was missing, or it claimed a total above zero while yielding nothing |

***

### Pricing

Pay per property returned. Rows that report an empty search, an unknown place, a filter that matched nothing, or a page that could not be read are **not** charged. A property that appears twice across pages is dropped before delivery, so it is never charged twice.

***

### Notes

- `robots.txt` was read in full on 2026-09-05. The `User-agent: *` section disallows agent contact pages, `*/flat/`, `*/apartment/`, `*-bed-` paths (except a few named allowances), student and overseas bedroom paths, and the query parameters `max-price`, `min-price`, `min-bedrooms`, `max-bedrooms`, `prop-types`, `radius`, `sort-field`, `let-agreed`, `under-offer`, `new-home-flag=T`, `view=map…`, `direction`, `modal`, `more-like-this`, `bounding-box`, `online-viewings-first`, `exclusive-first`. This Actor requests only the plain listing path with `?page=N`.
- Only public pages are read. No login, no API key.

# Actor input Schema

## `searchUrl` (type: `string`):

Paste an OnTheMarket listing page, for example https://www.onthemarket.com/for-sale/property/london/ or https://www.onthemarket.com/to-rent/property/manchester/, and the two fields below are ignored. Query parameters and property-type paths are dropped: OnTheMarket's robots.txt forbids them, so every filter runs here on the rows already fetched.

## `channel` (type: `string`):

sale = for-sale, rent = to-rent.

## `location` (type: `string`):

A town (london, manchester, milton-keynes) or a postcode area (sw1a, m1), as it appears in OnTheMarket's own addresses. A place OnTheMarket does not know comes back as a no-such-search row (OnTheMarket answers 404), not as an empty result.

## `maxListings` (type: `integer`):

How many properties to return. You are charged for the rows you actually receive. OnTheMarket serves at most 34 pages (about 950 properties) per search; narrow the place to see more.

## `maxPages` (type: `integer`):

A hard stop on how many pages are requested, whatever the filters do. 0 lets the run work it out. OnTheMarket has 34 pages at most.

## `useProxy` (type: `boolean`):

Off by default because OnTheMarket answers Apify's own network directly. If OnTheMarket ever starts blocking, the run switches to a proxy on its own.

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

Keep only properties whose title, address, type, features, labels, price qualifier or agency contain these words. No extra requests are made.

## `keywordMatch` (type: `string`):

any: at least one keyword. all: every keyword.

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

Drop properties containing any of these words.

## `agencyNames` (type: `array`):

Keep only properties marketed by these agencies. Partial names work.

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

A sale price is a total; a rent is per calendar month, the way OnTheMarket shows it. 0 means no minimum.

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

0 means no maximum. A property with no published price is kept.

## `requirePrice` (type: `boolean`):

Off by default: a property with no price is not treated as a cheap one, it is simply kept.

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

-1 turns the filter off. A property without a bedroom count is kept.

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

-1 turns the filter off.

## `minBathrooms` (type: `integer`):

-1 turns the filter off. A property without a bathroom count is kept.

## `propertyTypes` (type: `array`):

OnTheMarket's words, partial match: Apartment, Flat, Terraced house, Semi-detached house, Detached house, Bungalow, Maisonette, Studio, Chalet, Land …

## `tenureTypes` (type: `array`):

FREEHOLD, LEASEHOLD, SHARE\_OF\_FREEHOLD. A property whose tenure is not published in its features is kept.

## `excludeSharedOwnership` (type: `boolean`):

Drop listings whose price qualifier is 'Shared ownership'.

## `excludeDevelopments` (type: `boolean`):

Drop developer listings that advertise a range of homes rather than one property.

## `recentlyAddedOnly` (type: `boolean`):

Keep only listings OnTheMarket flags as recently added.

## `monitoringMode` (type: `boolean`):

Turn this on and schedule the Actor: each run returns only the properties it has never returned before, so you are charged for new stock rather than for the same pages again. Properties that did not fit under the limit are still waiting for you on the next run.

## `resetMonitoringState` (type: `boolean`):

Clears the memory for this search, so the next monitoring run starts from scratch. Works with or without monitoring mode on.

## Actor input object example

```json
{
  "channel": "sale",
  "location": "london",
  "maxListings": 20,
  "maxPages": 0,
  "useProxy": false,
  "keywords": [],
  "keywordMatch": "any",
  "excludeKeywords": [],
  "agencyNames": [],
  "minPrice": 0,
  "maxPrice": 0,
  "requirePrice": false,
  "minBedrooms": -1,
  "maxBedrooms": -1,
  "minBathrooms": -1,
  "propertyTypes": [],
  "tenureTypes": [],
  "excludeSharedOwnership": false,
  "excludeDevelopments": false,
  "recentlyAddedOnly": false,
  "monitoringMode": false,
  "resetMonitoringState": false
}
```

# Actor output Schema

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

One row per property: title and address, the price as a number with what it means (a sale total or a monthly rent, with the weekly rent as OnTheMarket shows it) and OnTheMarket's own price text and qualifier, tenure normalised to freehold / leasehold / share of freehold with the lease years remaining in their own column, bedrooms, bathrooms, features, labels, coordinates, added-or-reduced status, shared-ownership and new-development flags, the agency and branch page, and the main image. Every row also names the search OnTheMarket actually ran. Searches with no results, places OnTheMarket does not know, filters that matched nothing, and pages that could not be read come back as their own rows and are not charged. Branch phone numbers are not collected.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/onthemarket-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/onthemarket-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 '{}' |
apify call neverempty/onthemarket-listings --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/onthemarket-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/zbRs5Yy17bsEFwyeQ/builds/ZFztiG90GS8zOoqI2/openapi.json
