# UK Land Registry Sold Prices (`spookyweb/uk-land-registry-sold-prices`) Actor

Sold house prices from HM Land Registry Price Paid Data, every residential sale in England and Wales since 1995. Filter by postcode, street, town, district, county, date window and price window.

- **URL**: https://apify.com/spookyweb/uk-land-registry-sold-prices.md
- **Developed by:** [丂卩ㄖㄖҜㄚ](https://apify.com/spookyweb) (community)
- **Categories:** Real estate, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.60 / 1,000 sales

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

- **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

## UK Land Registry Sold Prices

Sold house prices for England and Wales, straight from HM Land Registry Price Paid Data. Every residential sale registered since January 1995, roughly 30 million transactions, with the address, the price paid, the date, the property type and the tenure. No API key, no account, no scraping of a listings site.

### 🔍 What does UK Land Registry Sold Prices do?

It queries the **Land Registry's own open data API** and returns every matching sale as a clean row, ready for a spreadsheet, a valuation model or your own product. Search a **postcode**, a **street**, a **town**, a **council district** or a whole **county**, then narrow it by date window and price window.

The quickest way to try it is to leave the prefilled input alone and press Start: it returns the most recent sales in Bath since the start of 2024. Swap `town` for your own area, or drop a full postcode into `postcode`, and you have the sold price history for that patch.

This is the register itself, not a portal's copy of it. Every row carries a `landRegistryUrl` pointing back at the official record for that transaction, so anything you publish can be traced to source.

### 📊 What data can I extract from Land Registry Price Paid Data?

One row per registered sale:

| Field | Description |
|---|---|
| `transactionId` | Land Registry unique identifier for the sale |
| `transactionDate` | Date of the sale, as a plain calendar date in `YYYY-MM-DD` |
| `pricePaid` | Sale price in whole pounds sterling |
| `address` | Full address on one line, assembled from the address parts below |
| `saon` | Secondary addressable object name, the flat or unit within a building |
| `paon` | Primary addressable object name, the house number or building name |
| `street` | Street name |
| `locality` | Locality or village within the posting town |
| `town` | Posting town or city |
| `district` | Local authority district the property sits in |
| `county` | County on the address |
| `postcode` | Full UK postcode recorded at the time of sale |
| `propertyType` | `detached`, `semi-detached`, `terraced`, `flat-maisonette` or `otherpropertytype` |
| `estateType` | Tenure of the sale, `freehold` or `leasehold` |
| `newBuild` | True when the property was newly built at the point of sale |
| `transactionCategory` | `standardpricepaidtransaction` for an ordinary sale, `additionalpricepaidtransaction` otherwise |
| `recordStatus` | State of the record in the register, `add`, `change` or `delete` |
| `landRegistryUrl` | The official Land Registry page for this transaction |
| `retrievedAt` | When the row was collected, ISO 8601 |

The address is split the way the Land Registry splits it, so `saon` and `paon` are separate from `street`. That matters when you are matching against another address dataset, because "Flat 5, Grosvenor Court, 8" is one building name and one unit rather than free text. The joined `address` field is there for the cases where you just want a label.

### 💡 Why pull sold prices from the Land Registry?

**Valuation and comparables.** Every sale on a street, with dates and prices, so a comparable is evidence rather than an asking price. Asking prices are what a seller hoped for. Price Paid Data is what somebody actually paid.

**Area price history.** Pull a whole town or district across a date window and you have the raw material for a price index, a time series, or a chart of how a postcode has moved since 1995.

**New build tracking.** Set `newBuildOnly` and you get the first sale of every plot in an area, which is how you find which developments completed, when they completed, and what the units went for.

**Property data products.** Portals, mortgage tools, insurance quoting and conveyancing all want a sold price history keyed on an address. This is the source they are all ultimately reading.

**Homeowner and agent research.** The last sale price of a specific address, found by postcode and house number, in one call.

**Academic and journalistic analysis.** The dataset is open government data, so an analysis built on it can be published and reproduced by anyone.

### 🚀 How do I use UK Land Registry Sold Prices?

1. Click **Try for free**.
2. Set an area. A full `postcode` for one street corner, `street` plus `town` for one road, `town` or `district` for a whole area, `county` for a region.
3. Set a date window with `soldAfter` and `soldBefore`, both `YYYY-MM-DD`, and a price window with `minPrice` and `maxPrice` if you want one.
4. Narrow further with `propertyTypes`, `estateType`, `newBuildOnly` or `transactionCategory`.
5. Set `maxResults` to cap what the run returns and therefore what it costs.
6. Click **Start**, then download the results as JSON, CSV or Excel, or read them straight from the API.

### ⬇️ Input

Every filter is optional and they all combine. A run with no filters at all is allowed but returns an arbitrary slice of the whole register, so it logs a warning and it is almost never what you want.

```json
{
  "town": "BATH",
  "soldAfter": "2024-01-01",
  "minPrice": 300000,
  "propertyTypes": ["detached"],
  "sortBy": "newest",
  "maxResults": 500
}
```

| Field | Type | Default | What it does |
|---|---|---|---|
| `postcode` | string | | A full UK postcode such as `BA1 2XY` |
| `street` | string | | Street name without the house number |
| `town` | string | | Posting town or city |
| `district` | string | | Local authority district |
| `county` | string | | County on the address |
| `soldAfter` | string | | Earliest sale date, `YYYY-MM-DD`, inclusive |
| `soldBefore` | string | | Latest sale date, `YYYY-MM-DD`, inclusive |
| `minPrice` | integer | | Lowest price in whole pounds, inclusive |
| `maxPrice` | integer | | Highest price in whole pounds, inclusive |
| `propertyTypes` | array | all | Any of detached, semi-detached, terraced, flat or maisonette, other |
| `estateType` | string | `any` | `freehold` or `leasehold` |
| `newBuildOnly` | boolean | `false` | Keep only new builds |
| `transactionCategory` | string | `any` | `standard` or `additional` |
| `sortBy` | string | `newest` | `newest`, `oldest`, `highest-price`, `lowest-price` or `fastest` |
| `maxResults` | integer | `500` | Hard cap on rows returned, and on cost. Maximum 10000 |
| `maxRecordsScanned` | integer | `20000` | Safety stop when the property filters are narrow |

Case and spacing do not matter. `bath` becomes `BATH` and `ba1 2xy` becomes `BA1 2XY` before the request goes out, because the register stores everything in upper case and matches exactly.

### ⬆️ Output

#### Table view

Results arrive as a **Sales** table you can sort and filter in the Console, with the date, price, address, type and tenure lined up for scanning. There is a second view, **Address parts**, which splits the address into the Land Registry's own fields for anyone matching against another dataset.

#### JSON

A typical row:

```json
{
  "transactionId": "7663DF8A-3554-4438-96E4-6483101AE00A",
  "transactionDate": "1996-05-17",
  "pricePaid": 54000,
  "address": "FLAT 5, GROSVENOR COURT, 8, PARSONAGE ROAD, STOCKPORT, STOCKPORT, SK4 4JZ",
  "saon": "FLAT 5",
  "paon": "GROSVENOR COURT, 8",
  "street": "PARSONAGE ROAD",
  "locality": "STOCKPORT",
  "town": "STOCKPORT",
  "district": "STOCKPORT",
  "county": "GREATER MANCHESTER",
  "postcode": "SK4 4JZ",
  "propertyType": "flat-maisonette",
  "estateType": "leasehold",
  "newBuild": false,
  "transactionCategory": "standardpricepaidtransaction",
  "recordStatus": "add",
  "landRegistryUrl": "https://landregistry.data.gov.uk/data/ppi/transaction/7663DF8A-3554-4438-96E4-6483101AE00A/current",
  "retrievedAt": "2026-08-16T09:41:02.118Z"
}
```

`newBuild` is `true`, `false` or `null`. It is null when the register published no value for that sale, because a missing flag is not the same statement as a sale that was not a new build.

Download it from the run as JSON, CSV or Excel, or read it straight from the API.

Every run also writes a `RUN_SUMMARY` record to the key-value store with the search that ran, how many records were read, how many were saved and why the run stopped: `complete` when the result set was exhausted, `max-results-reached`, `scan-limit-reached`, `time-limit-reached`, `charge-limit-reached` or `error`. That is how you tell a search that found everything from one that was capped part way.

### Coverage and currency

Price Paid Data covers **England and Wales only**. Scotland is held by Registers of Scotland and Northern Ireland by Land and Property Services, and neither appears here.

The record starts on **1 January 1995** and runs to the most recent monthly publication. A sale enters the register some weeks after completion, so expect roughly a **two month lag** at the leading edge. The newest few weeks of any area will look thin, and they fill in later. That is the publication cycle rather than a gap in this Actor.

Sales are registered transactions of residential property sold for value. Transfers that were not sales, sales that were never registered, and commercial property outside the residential definition are not in the dataset at all.

### How the filters behave

The address, date and price filters are applied by the Land Registry query service, so narrowing on those makes the run faster as well as cheaper. Two details are worth knowing because both are silent when you get them wrong.

**Address matching is exact and upper case.** The register stores `BATH`, not `Bath`, and a lower case request is accepted and returns nothing. This Actor upper cases every address field for you, so it is only a problem if you go to the API directly.

**Only whole postcodes match.** The index is on full postcodes, so `BA1` on its own selects nothing rather than selecting all of BA1. Give a full postcode, or use `town`, `district` or `county` for an area. A partial postcode is rejected with an explanation instead of running and returning an empty dataset.

**Dates must be `YYYY-MM-DD`.** Any other format is accepted by the API and matches zero rows, which is indistinguishable from a street that never sold. The Actor validates the format before the request goes out and stops with the reason.

`propertyTypes`, `estateType`, `newBuildOnly` and `transactionCategory` are applied to the rows as they arrive rather than by the API. The query service times out when those are combined with an address filter, so doing it this way is the difference between an answer and a ninety second failure. It costs nothing extra, but a very narrow choice means reading more records to fill the run, which is what `maxRecordsScanned` bounds.

### Sorting and speed

`sortBy` is handled by the query service and adds several seconds per page. `newest` is the default because a sold price history is almost always wanted newest first. If you are pulling a whole area and intend to sort it yourself afterwards, set `sortBy` to `fastest` and the run drops to a fraction of the time.

An unsorted page of 200 sales comes back in about a second. The same page sorted takes anything from two seconds to the ninety second point where the query service gives up, depending on how wide the area is. That is why `maxResults` tops out at 10000: it is what fits inside the 300 second default run timeout. A run that runs out of time stops on its own, keeps every row it collected and records `time-limit-reached` in the run summary, so it never dies mid page with nothing to show. For more than that in one go, raise the run timeout in the Console, split the search by date window, or switch to `fastest`.

### Data source

HM Land Registry Price Paid Data, published at [landregistry.data.gov.uk](https://landregistry.data.gov.uk/) under the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/). This Actor reads the public [Linked Data API](https://landregistry.data.gov.uk/app/root/doc/ppd), which needs no key and no account.

Contains HM Land Registry data © Crown copyright and database right. Reuse is permitted under the Open Government Licence, and the licence asks you to carry that attribution wherever the data is published.

### 💰 How much does it cost?

You pay per sale returned. A run that matches nothing returns nothing and costs nothing beyond the standard Actor start.

`maxResults` is the cap on both rows and cost, so setting it is how you bound a run in advance. Narrowing with `soldAfter`, `minPrice` and a tight area also keeps you from paying for records you were only going to discard afterwards.

Paid Apify plans get a lower rate per sale. The current price and the plan discounts are shown on this Actor's pricing tab.

### 🔌 Integrations

Send results straight to Google Sheets, Slack, Airtable, Zapier, Make or your own webhook using [Apify integrations](https://docs.apify.com/platform/integrations). Runs can be scheduled, so a monthly job that pulls the newest sales in your patch and appends them to a sheet takes about a minute to set up. You can also trigger a run from another tool, which is the usual way to attach a sold price history to a new record in a CRM.

### 🔗 Using UK Land Registry Sold Prices with the Apify API

```bash
curl -X POST "https://api.apify.com/v2/acts/spookyweb~uk-land-registry-sold-prices/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"town": "BATH", "soldAfter": "2024-01-01", "minPrice": 300000, "maxResults": 500}'
```

Or with the Apify client:

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

const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('spookyweb/uk-land-registry-sold-prices').call({
  town: 'BATH',
  soldAfter: '2024-01-01',
  minPrice: 300000,
  propertyTypes: ['detached'],
  maxResults: 500,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Full detail is in the [Apify API reference](https://docs.apify.com/api/v2), and every run is also callable from the [Python](https://docs.apify.com/api/client/python) and [JavaScript](https://docs.apify.com/api/client/js) clients.

### ❓ FAQ

#### How far back does the sold price data go?

To 1 January 1995. Every residential sale in England and Wales registered since then is in the dataset, which is roughly 30 million transactions. Anything before 1995 is not held in Price Paid Data at all.

#### Does it cover Scotland or Northern Ireland?

No. HM Land Registry covers England and Wales. Scottish sold prices are held by Registers of Scotland and Northern Irish sales by Land and Property Services, and neither is part of this dataset.

#### How recent are the newest sales?

There is roughly a two month lag between a sale completing and the transaction appearing in the register, because the data is published on a monthly cycle after registration. The last few weeks of any area will look sparse and will fill in over the following months.

#### Can I get the sold price history for one address?

Yes. Set `postcode` to the full postcode and leave everything else open, then pick out the rows whose `paon` matches the house number or building name. A postcode usually covers a handful of properties, so the whole history comes back in one small run.

#### What is the difference between a standard and an additional transaction?

Standard covers ordinary sales at full market value between private individuals. Additional covers everything that does not meet those criteria: repossessions, buy to let purchases, transfers under a power of sale, and sales to or from a company. Both are real prices, but if you are building comparables you usually want `transactionCategory` set to `standard`, because an additional sale is often below market value.

#### Why does my search return nothing?

Four usual causes. The area is spelled differently in the register than in everyday use, most often the posting town rather than the council. The postcode is partial, and only whole postcodes match. The date window is inside the two month publication lag. Or the property type and tenure filters between them exclude everything in the area. The run log names which of these applied.

#### What do saon and paon mean?

They are the British Standard address terms the Land Registry uses. `paon` is the primary addressable object, meaning the house number or the building name. `saon` is the secondary one, meaning the flat or unit inside that building. Most houses have a `paon` and no `saon`, and flats have both.

#### Do I need a Land Registry API key?

No. Price Paid Data is open government data, published without quota. The only practical limits are the ones this Actor works around, which are documented above under how the filters behave.

### ⚖️ Is it legal to use Land Registry sold price data?

Yes. HM Land Registry publishes Price Paid Data deliberately, as open government data, so that the property market is transparent and anyone can see what a property actually sold for. It is transaction and address information about property, published by the registry itself.

The data is released under the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/), which permits commercial use, adaptation and redistribution provided you carry the attribution: contains HM Land Registry data © Crown copyright and database right, licensed under the Open Government Licence v3.0. Apify's [ethical scraping guide](https://blog.apify.com/is-web-scraping-legal/) covers the wider picture.

### 👍 Your feedback

Found a bug, or want a field that is not here yet? Open an issue on the Actor's Issues tab. Requests that make the data more useful get built, and problems get fixed quickly.

### 🔎 You might also like

| Actor | What it does |
|---|---|
| [UK Planning Applications](https://apify.com/spookyweb/uk-planning-applications) | Planning applications from every UK council, tagged by the type of work proposed |
| [UK Food Hygiene Ratings](https://apify.com/spookyweb/uk-food-hygiene-ratings) | Official FSA hygiene ratings for every UK food business, searchable by council or postcode |
| [Website Contact Scraper](https://apify.com/spookyweb/website-contact-scraper) | Company websites in, one contact record per domain out, with emails and social links |

# Actor input Schema

## `postcode` (type: `string`):

A full UK postcode such as BA1 2XY. Case and spacing are normalised for you. Price Paid Data is indexed on whole postcodes only, so an outward code on its own such as BA1 will not match. Use town, district or county for a wider area.

## `street` (type: `string`):

A street name on its own, without the house number, for example MILSOM STREET. Pair it with a town to keep a common street name from returning the same road in forty different places.

## `town` (type: `string`):

The posting town on the address, for example BATH, MANCHESTER or NEWCASTLE UPON TYNE. This is the postal town rather than the council, so a Manchester suburb is often filed under MANCHESTER.

## `district` (type: `string`):

The local authority district, for example TAMESIDE or BATH AND NORTH EAST SOMERSET. This is the right field for pulling every sale in one council area, which the postal town does not line up with.

## `county` (type: `string`):

The county on the address, for example GREATER MANCHESTER or GLOUCESTERSHIRE. The widest of the address filters, useful for regional analysis when paired with a date window.

## `soldAfter` (type: `string`):

Earliest sale date to include, as YYYY-MM-DD. The bound is inclusive. Price Paid Data starts on 1 January 1995, so anything earlier simply widens the window.

## `soldBefore` (type: `string`):

Latest sale date to include, as YYYY-MM-DD. The bound is inclusive. Newly registered sales appear roughly two months after completion, so the most recent weeks are always thin.

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

Lowest sale price to include, in whole pounds with no commas or currency symbol. The bound is inclusive, so 250000 keeps a sale at exactly 250,000.

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

Highest sale price to include, in whole pounds with no commas or currency symbol. The bound is inclusive. Useful for stripping out the bulk portfolio transfers that sit at the top of every area.

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

Keep only these kinds of property. Leave it empty to keep all of them. Other covers everything that is not a home in the usual sense, which is where most commercial and land transactions land.

## `estateType` (type: `string`):

Keep only freehold or only leasehold sales. Flats are almost always leasehold and houses are usually freehold, so this doubles as a rough filter for flats against houses.

## `newBuildOnly` (type: `boolean`):

Keep only properties that were newly built when they sold. This is the first sale of a plot, so it is how you find which developments completed in an area and what the units went for.

## `transactionCategory` (type: `string`):

Standard covers ordinary sales at market value between private individuals. Additional covers repossessions, buy to let purchases, transfers under a power of sale and other sales that do not meet the standard criteria.

## `sortBy` (type: `string`):

How the Land Registry should order the sales before paging. Sorting is done in the query service and adds several seconds per page, so pick Fastest when the order genuinely does not matter and you want the whole set.

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

Hard cap on the rows saved, which is also the cap on what the run can cost. Each sale returned is one billable result. The ceiling is 10000 because that is what fits inside the 300 second default run timeout, and a run that runs out of time stops cleanly and keeps everything it collected.

## `maxRecordsScanned` (type: `integer`):

Safety stop on how many sales the run reads before giving up on filling the result cap. It only matters when the property, tenure, new build or category filters are narrow, because those are applied after the records arrive. Set it below the maximum sales to return and it wins, capping the run there and saying so in the log.

## Actor input object example

```json
{
  "town": "BATH",
  "soldAfter": "2024-01-01",
  "estateType": "any",
  "newBuildOnly": false,
  "transactionCategory": "any",
  "sortBy": "newest",
  "maxResults": 100,
  "maxRecordsScanned": 20000
}
```

# Actor output Schema

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

Every matching sale, one row per transaction, with the address, the price paid and the property details.

## `runSummary` (type: `string`):

The search that ran, how many records were read, how many were saved and why the run stopped.

# 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 = {
    "town": "BATH",
    "soldAfter": "2024-01-01",
    "maxResults": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("spookyweb/uk-land-registry-sold-prices").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 = {
    "town": "BATH",
    "soldAfter": "2024-01-01",
    "maxResults": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("spookyweb/uk-land-registry-sold-prices").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 '{
  "town": "BATH",
  "soldAfter": "2024-01-01",
  "maxResults": 100
}' |
apify call spookyweb/uk-land-registry-sold-prices --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,spookyweb/uk-land-registry-sold-prices"
        }
    }
}

```

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/chVnUUxO70K7tKXJ7/builds/JJBwuzVvy8JVNg3Hr/openapi.json
