Skip to main content
29M UK properties, one API call. Get an API key →
Back to Blog
Developer & API Mar 3, 2026 · 10 min read min read

UK house price growth by postcode: tracking capital appreciation via API

How to pull 1Y, 3Y, 5Y, and 10Y capital appreciation for any outcode in England and Wales, with Python code to rank postcodes by appreciation, time-adjust historic valuations, and calculate LTV movement for mortgage clients.

Homedata Team · Updated

Why postcode-level price growth matters

National house price indices, Halifax, Nationwide, ONS, tell you that UK house prices rose X% last year. They don't tell you whether your postcode did better or worse. A flat in Bristol city centre and a terrace in a pit-village commuter belt have almost nothing in common as investments, even in the same region.

Outcode-level price growth data bridges that gap. An outcode is the first part of a UK postcode: SW1A, LS1, M4. It covers a tight geographic cluster, typically a few streets to a few square kilometres, which tracks micro-market trends that national indices smooth out completely.

For investors, mortgage brokers, and proptech developers, this matters:

  • Investors use capital appreciation data to compare locations before committing, 5Y growth rate is more relevant than last month's index.
  • Mortgage advisors use 1Y and 3Y growth to help clients understand loan-to-value movement since purchase.
  • Lenders use price growth trends to assess collateral risk on existing mortgage books.
  • Proptech platforms surface growth charts on property listings to increase time-on-page and conversion.

The /price-growth/{outcode}/ endpoint

Homedata's price growth endpoint returns capital appreciation at four time horizons for any UK outcode:

curl https://api.homedata.co.uk/price-growth/SW1A/ \
  -H "Authorization: Api-Key YOUR_API_KEY"

Response:

{
  "outcode": "SW1A",
  "median_price": 875000,
  "transaction_count": 142,
  "growth_1y": 3.2,
  "growth_3y": 11.4,
  "growth_5y": 22.8,
  "growth_10y": 61.3,
  "period": "2026-Q1"
}

All growth figures are percentage change over the period. growth_5y: 22.8 means the median sale price in this outcode is 22.8% higher than it was five years ago. The calculation uses median sold prices from HM Land Registry Price Paid Data, actual completed transactions, not asking prices.

Fetching price growth for a single property

The simplest use case: you have a property's postcode and want to show how the local market has performed:

import requests

API_KEY = "your_api_key_here"

def get_price_growth(postcode: str) -> dict:
    """Fetch price growth data for a postcode's outcode."""
    outcode = postcode.strip().upper().split()[0]  # 'SW1A 2AA' → 'SW1A'
    resp = requests.get(
        f"https://api.homedata.co.uk/price-growth/{outcode}/",
        headers={"Authorization": f"Api-Key {API_KEY}"},
        timeout=10
    )
    resp.raise_for_status()
    return resp.json()

data = get_price_growth("SW1A 2AA")
print(f"1Y:  {data['growth_1y']:+.1f}%")
print(f"3Y:  {data['growth_3y']:+.1f}%")
print(f"5Y:  {data['growth_5y']:+.1f}%")
print(f"10Y: {data['growth_10y']:+.1f}%")
print(f"Median price: £{data['median_price']:,}")

Ranking postcodes by 5-year appreciation

The real power comes when you query multiple outcodes and compare. This is useful for investment screening: finding areas with the strongest long-term appreciation trajectory:

import requests
from concurrent.futures import ThreadPoolExecutor

API_KEY = "your_api_key_here"
BASE = "https://api.homedata.co.uk/api"

# A shortlist of outcodes to compare
OUTCODES = ["E1", "E2", "E3", "N1", "N4", "N7", "N16", "SE1", "SE15", "SW9"]

def fetch_growth(outcode: str) -> dict | None:
    """Fetch 5Y growth for one outcode. Returns None on error."""
    try:
        r = requests.get(
            f"{BASE}/price-growth/{outcode}/",
            headers={"Authorization": f"Api-Key {API_KEY}"},
            timeout=10
        )
        if r.status_code == 200:
            return r.json()
    except requests.RequestException:
        pass
    return None

# Fetch all outcodes in parallel (polite: 5 workers)
with ThreadPoolExecutor(max_workers=5) as pool:
    results = list(pool.map(fetch_growth, OUTCODES))

# Filter failures and sort by 5Y growth descending
ranked = sorted(
    [r for r in results if r],
    key=lambda x: x.get("growth_5y", 0),
    reverse=True
)

print(f"\n{'Outcode':<10} {'5Y Growth':>10} {'3Y Growth':>10} {'1Y Growth':>10} {'Median £':>12}")
print("-" * 55)
for r in ranked:
    print(
        f"{r['outcode']:<10}"
        f"{r['growth_5y']:>+9.1f}%"
        f"{r['growth_3y']:>+9.1f}%"
        f"{r['growth_1y']:>+9.1f}%"
        f"£{r['median_price']:>10,}"
    )

Example output:

Outcode    5Y Growth  3Y Growth  1Y Growth      Median £
-------------------------------------------------------
E3         +31.2%     +14.1%      +5.2%       £ 425,000
N16        +28.7%     +12.8%      +4.9%       £ 510,000
SE15       +26.4%     +11.3%      +3.8%       £ 395,000
N4         +24.1%     +10.2%      +3.1%       £ 445,000
E2         +23.8%      +9.7%      +2.9%       £ 590,000
N7         +21.5%      +8.4%      +2.7%       £ 475,000
E1         +19.2%      +7.6%      +2.4%       £ 640,000
SW9        +18.3%      +7.1%      +2.1%       £ 520,000
SE1        +15.1%      +5.2%      +1.6%       £ 780,000
N1         +14.8%      +4.9%      +1.3%       £ 820,000

A pattern emerges quickly: strong 5Y growth often correlates with lower median prices. These are the areas that gentrified. SE1 and N1 are already expensive; the big moves happened 10+ years ago. E3 and SE15 show the current frontier.

Use case: time-adjusting a valuation

Price growth data matters beyond rankings. It's a core input for AVM models. If you have a property that last sold for £380,000 two years ago, you can project its current value using the outcode's growth rate:

from datetime import datetime

def time_adjust_price(historic_price: float, postcode: str, sale_date: str) -> dict:
    """Project a historic sale price forward using local price growth."""
    data = get_price_growth(postcode)

    sale_dt = datetime.strptime(sale_date, "%Y-%m-%d")
    years_ago = (datetime.now() - sale_dt).days / 365.25

    # Use the most appropriate growth rate for the elapsed time
    if years_ago <= 1.5:
        annual_rate = data["growth_1y"] / 100
    elif years_ago <= 4:
        annual_rate = (data["growth_3y"] / 100) / 3
    elif years_ago <= 7.5:
        annual_rate = (data["growth_5y"] / 100) / 5
    else:
        annual_rate = (data["growth_10y"] / 100) / 10

    projected = historic_price * (1 + annual_rate) ** years_ago

    return {
        "original_price": historic_price,
        "projected_price": round(projected, -3),
        "years_elapsed": round(years_ago, 1),
        "annual_rate_used": round(annual_rate * 100, 2),
        "outcode": data["outcode"],
    }

result = time_adjust_price(380000, "E3 4AA", "2023-09-15")
print(f"Original:  £{result['original_price']:,}")
print(f"Projected: £{result['projected_price']:,}")
print(f"Annual rate used: {result['annual_rate_used']}% ({result['outcode']})")

Use case: mortgage LTV recalculation

Mortgage advisors use this pattern to help clients understand how their LTV has changed since they bought, useful for remortgage conversations:

def calculate_current_ltv(
    original_price: float,
    purchase_date: str,
    postcode: str,
    outstanding_mortgage: float
) -> dict:
    """Estimate current LTV based on price growth since purchase."""
    adj = time_adjust_price(original_price, postcode, purchase_date)
    current_value = adj["projected_price"]
    ltv = (outstanding_mortgage / current_value) * 100

    return {
        "purchase_price":       original_price,
        "estimated_value":      current_value,
        "outstanding_mortgage": outstanding_mortgage,
        "estimated_ltv":        round(ltv, 1),
        "equity":               round(current_value - outstanding_mortgage, -3),
    }

ltv = calculate_current_ltv(
    original_price=350000,
    purchase_date="2021-03-01",
    postcode="N16 8AA",
    outstanding_mortgage=252000
)
print(f"Purchase price:   £{ltv['purchase_price']:,}")
print(f"Estimated value:  £{ltv['estimated_value']:,}")
print(f"Current LTV:      {ltv['estimated_ltv']}%")
print(f"Estimated equity: £{ltv['equity']:,}")

JavaScript version

The same postcode ranking in Node.js:

const API_KEY = 'your_api_key_here';
const BASE = 'https://api.homedata.co.uk/api';
const headers = { Authorization: `Api-Key ${API_KEY}` };

const OUTCODES = ['E1', 'E2', 'E3', 'N1', 'N4', 'N7', 'N16', 'SE1', 'SE15', 'SW9'];

async function fetchGrowth(outcode) {
  const res = await fetch(`${BASE}/price-growth/${outcode}/`, { headers });
  if (!res.ok) return null;
  return res.json();
}

async function rankByAppreciation(outcodes) {
  const results = await Promise.all(outcodes.map(fetchGrowth));
  return results
    .filter(Boolean)
    .sort((a, b) => b.growth_5y - a.growth_5y);
}

rankByAppreciation(OUTCODES).then(ranked => {
  ranked.forEach(r => {
    console.log(`${r.outcode.padEnd(8)} 5Y: ${r.growth_5y.toFixed(1)}%  Median: £${r.median_price.toLocaleString()}`);
  });
});

API endpoints used

Endpoint Purpose Plan
/price-growth/{outcode}/ 1Y/3Y/5Y/10Y capital appreciation Included
/postcode-profile/{postcode}/ Area demographics, index of multiple deprivation Included
/comparables/{uprn}/ Nearby sold prices for AVM / valuation Arranged per customer
/property/{uprn}/ Property attributes (type, bedrooms, tenure) Included

Batching

If you're screening 100+ outcodes, each call spends a token from your balance. To keep runs efficient:

  • Use the parallel fetch pattern above with max_workers=5 in Python or Promise.all in JS, don't fire 100 requests simultaneously.
  • Cache results: price growth data updates quarterly. Store it in Redis or a simple dict with a TTL of 7 days and you'll eliminate 95% of repeat calls.
  • For bulk screening (500+ postcodes), a higher-allowance plan gives you the monthly headroom to run it.

Combining growth with yield for total return

Capital appreciation is only half the picture for investors. The total return on a buy-to-let is appreciation plus rental yield. You can combine the price growth endpoint with Homedata's rental data for a complete picture:

def total_return_estimate(postcode: str, uprn: str) -> dict:
    """Estimate total annual return = gross yield + annual price growth."""
    growth = get_price_growth(postcode)

    # Pull gross yield from the property endpoint
    prop = requests.get(
        f"https://api.homedata.co.uk/property/{uprn}/",
        headers={"Authorization": f"Api-Key {API_KEY}"},
        timeout=10
    ).json()

    gross_yield = prop.get("gross_yield_estimate", 0)  # e.g. 4.5
    appreciation = growth["growth_1y"]                 # e.g. 3.2

    return {
        "outcode": growth["outcode"],
        "gross_yield":   gross_yield,
        "appreciation":  appreciation,
        "total_return":  round(gross_yield + appreciation, 1),
    }

Ready to start screening? Get an API key: pay as you go, and every endpoint in this guide is included.

Start building with a free API key →

Pay as you go · EPC, Land Registry, council tax, schools · First top-up matched 100%

Get an API key

Track capital appreciation by postcode

1Y, 3Y, 5Y, and 10Y growth rates for any UK outcode. Pay as you go: 100 tokens = £1.