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.
Why postcode-level price growth matters
National house price indices — Halifax, Nationwide, ONS — tell you that UK house prices rose X% last year. What they don't tell you is 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 if they're 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 5 years ago. The calculation uses median sold prices from HM Land Registry title transfer data, so it reflects actual completed transactions — not asking prices or estimates.
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 isn't just for rankings — it's a key 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=5in Python orPromise.allin 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.
Track capital appreciation by postcode
1Y, 3Y, 5Y, and 10Y growth rates for any UK outcode. Starter plan, free tier available.