Skip to main content
Free UK property data API Start free →
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 UK outcode — 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 /api/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/api/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/api/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
/api/price-growth/{outcode}/ 1Y/3Y/5Y/10Y capital appreciation Starter
/api/postcode-profile/{postcode}/ Area demographics, index of multiple deprivation Starter
/api/comparables/{uprn}/ Nearby sold prices for AVM / valuation Starter
/api/property/{uprn}/ Property attributes (type, bedrooms, tenure) Starter

Rate limits and batching

If you're screening 100+ outcodes, the Starter plan's per-request limit applies to each call individually. To stay within rate limits:

  • 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), the Professional plan includes a higher rate limit and batch endpoint support.

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/api/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 a free API key — the Starter plan covers all endpoints in this guide. No credit card required.

Start building with the free API →

100 calls/month · EPC, Land Registry, council tax, schools · No credit card

Get free API key

Track capital appreciation by postcode

1Y, 3Y, 5Y, and 10Y growth rates for any UK outcode. Starter plan, free tier available.