How to Build an Automated Property Valuation (AVM) with the Homedata API
Build a production-ready AVM in Python or JavaScript: combine comparable sold prices, EPC floor area, and price growth trends to estimate any UK property's market value. Full code, confidence scoring, and API reference.
What is an Automated Valuation Model (AVM)?
An AVM is an algorithm that estimates a property's market value using comparable sales, property attributes, and statistical methods — without requiring a physical inspection. Banks, mortgage lenders, and proptech platforms use AVMs to make instant decisions on lending, portfolio management, and pricing.
The UK residential AVM market is dominated by a handful of proprietary providers — enterprise-only, opaque, and gated behind long contracts. If you're building a proptech product, you've probably hit this wall.
The alternative: build your own. The Homedata API gives you the raw ingredients — comparable sales, EPC floor area data, and property attributes — in a single API call. This guide shows you how to combine them into a working AVM.
The ingredients of a property valuation
Every credible AVM needs three things:
- Comparable sales — recent transactions for similar properties nearby. The most important signal.
- Property attributes — bedrooms, floor area (m²), property type (detached/semi/terrace/flat), EPC rating.
- Time adjustment — house prices change. A sale 3 years ago is worth less as a comparable than one last month.
Homedata's /api/comparables/{uprn}/ endpoint returns the nearest comparable sales ranked by geographic proximity. Combined with /api/property/{uprn}/ for the subject property's attributes, you have everything you need.
Step 1: Get the subject property
Start by looking up the property you want to value:
import requests
API_KEY = "your_api_key_here"
UPRN = "10023456789"
# Get the subject property
subject = requests.get(
f"https://api.homedata.co.uk/api/property/{UPRN}/",
headers={"Authorization": f"Api-Key {API_KEY}"}
).json()
print(f"Property: {subject['address']}")
print(f"Type: {subject['property_type']}")
print(f"Bedrooms: {subject['bedrooms']}")
print(f"Floor area: {subject.get('total_floor_area', 'Unknown')} m²")
Step 2: Pull comparable sales
The /api/comparables/{uprn}/ endpoint returns the 20 nearest sold prices, ranked by distance. Each comparable includes the sold price, date, distance in metres, property type, and bedroom count.
# Get comparable sales
comps = requests.get(
f"https://api.homedata.co.uk/api/comparables/{UPRN}/",
headers={"Authorization": f"Api-Key {API_KEY}"}
).json()
print(f"Found {len(comps['comparables'])} comparables")
for c in comps['comparables'][:5]:
print(f" £{c['price']:,} — {c['distance_m']}m away — {c['date']}")
Step 3: Filter and weight the comparables
Raw comparables aren't all equal. A 5-bed detached 800m away shouldn't influence the valuation of a 2-bed terrace. Apply filters:
from datetime import datetime, timedelta
def filter_comparables(comps, subject):
"""Filter comparables to similar properties sold recently."""
filtered = []
cutoff = datetime.now() - timedelta(days=730) # 2 years
for c in comps:
# Same property type (or close)
type_match = c['property_type'] == subject['property_type']
# Similar bedrooms (±1)
bed_match = abs(c.get('bedrooms', 0) - subject.get('bedrooms', 0)) <= 1
# Within 500m
distance_ok = c['distance_m'] <= 500
# Sold within 2 years
sale_date = datetime.strptime(c['date'], '%Y-%m-%d')
recent = sale_date >= cutoff
if type_match and bed_match and distance_ok and recent:
filtered.append(c)
return filtered
good_comps = filter_comparables(comps['comparables'], subject)
print(f"Filtered to {len(good_comps)} relevant comparables")
Step 4: Time-adjust the prices
A property sold for £250,000 two years ago is worth more today if the market has risen. Use the Homedata /api/price-growth/{outcode}/ endpoint to get the annual growth rate for the area:
# Get area price growth
outcode = subject['postcode'].split()[0]
growth = requests.get(
f"https://api.homedata.co.uk/api/price-growth/{outcode}/",
headers={"Authorization": f"Api-Key {API_KEY}"}
).json()
annual_growth = growth['growth_1y'] / 100 # e.g. 0.045 = 4.5%
def time_adjust(price, sale_date_str, annual_rate):
"""Adjust a historical price to today's value."""
sale_date = datetime.strptime(sale_date_str, '%Y-%m-%d')
years_ago = (datetime.now() - sale_date).days / 365.25
return price * (1 + annual_rate) ** years_ago
for c in good_comps:
c['adjusted_price'] = time_adjust(c['price'], c['date'], annual_growth)
Step 5: Calculate the per-sqm rate and estimate
If floor area data is available (from the EPC endpoint), price-per-square-metre is the most accurate basis for comparison:
def estimate_value(comps, subject_floor_area=None):
"""
Weighted average of comparable prices.
If floor area is available, use price/m².
Weight by inverse distance (closer = more weight).
"""
if not comps:
return None
total_weight = 0
weighted_sum = 0
for c in comps:
# Inverse distance weighting (minimum 50m to avoid division issues)
weight = 1 / max(c['distance_m'], 50)
if subject_floor_area and c.get('floor_area'):
# Price per m² method
price_psm = c['adjusted_price'] / c['floor_area']
value = price_psm * subject_floor_area
else:
# Direct comparable method
value = c['adjusted_price']
weighted_sum += value * weight
total_weight += weight
estimate = weighted_sum / total_weight
# Confidence: based on count and consistency
prices = [c['adjusted_price'] for c in comps]
spread = (max(prices) - min(prices)) / estimate if prices else 1
confidence = "high" if len(comps) >= 5 and spread < 0.3 else \
"medium" if len(comps) >= 3 else "low"
return {
'estimate': round(estimate, -3), # Round to nearest £1,000
'low': round(min(prices) * 0.95, -3),
'high': round(max(prices) * 1.05, -3),
'confidence': confidence,
'comparables_used': len(comps),
}
floor_area = subject.get('total_floor_area')
result = estimate_value(good_comps, floor_area)
print(f"\n--- AVM Result ---")
print(f"Estimated value: £{result['estimate']:,.0f}")
print(f"Range: £{result['low']:,.0f} – £{result['high']:,.0f}")
print(f"Confidence: {result['confidence']}")
print(f"Based on {result['comparables_used']} comparables")
Making it production-ready
The above is a working AVM in ~60 lines of Python. To make it production-grade, you'd add:
- Fallback radius expansion — if fewer than 3 comparables within 500m, expand to 1km, then 2km.
- Property type substitution — if no same-type comps exist, use adjacent types (semi ↔ terrace) with a discount/premium factor.
- Floor area normalisation — use EPC data from
/api/epc-checker/{uprn}/for both subject and comparables. - Caching — price growth rates change monthly, not daily. Cache them.
- Batch processing — use
/api/property-batch/for portfolio valuations (up to 100 UPRNs per call).
JavaScript version
The same logic in Node.js:
const API_KEY = 'your_api_key_here';
const UPRN = '10023456789';
const BASE = 'https://api.homedata.co.uk/api';
const headers = { Authorization: `Api-Key ${API_KEY}` };
async function valuate(uprn) {
// 1. Subject property
const subject = await fetch(`${BASE}/property/${uprn}/`, { headers }).then(r => r.json());
// 2. Comparables
const { comparables } = await fetch(`${BASE}/comparables/${uprn}/`, { headers }).then(r => r.json());
// 3. Price growth for time adjustment
const outcode = subject.postcode.split(' ')[0];
const growth = await fetch(`${BASE}/price-growth/${outcode}/`, { headers }).then(r => r.json());
const rate = growth.growth_1y / 100;
// 4. Filter: same type, ±1 bed, <500m, <2 years old
const cutoff = Date.now() - 730 * 86400000;
const filtered = comparables.filter(c =>
c.property_type === subject.property_type &&
Math.abs((c.bedrooms || 0) - (subject.bedrooms || 0)) <= 1 &&
c.distance_m <= 500 &&
new Date(c.date).getTime() >= cutoff
);
// 5. Time-adjust and weight by inverse distance
let totalWeight = 0, weightedSum = 0;
for (const c of filtered) {
const yearsAgo = (Date.now() - new Date(c.date)) / (365.25 * 86400000);
const adjusted = c.price * Math.pow(1 + rate, yearsAgo);
const weight = 1 / Math.max(c.distance_m, 50);
weightedSum += adjusted * weight;
totalWeight += weight;
}
const estimate = Math.round((weightedSum / totalWeight) / 1000) * 1000;
return { estimate, comparables_used: filtered.length };
}
valuate(UPRN).then(console.log);
API endpoints used
| Endpoint | Purpose | Plan |
|---|---|---|
/api/property/{uprn}/ |
Subject property attributes | Starter |
/api/comparables/{uprn}/ |
Nearby sold prices | Starter |
/api/price-growth/{outcode}/ |
Area growth rate for time adjustment | Starter |
/api/epc-checker/{uprn}/ |
Floor area for per-m² pricing | Starter |
/api/property-batch/ |
Bulk processing (portfolio mode) | Professional |
When to use Homedata's built-in AVM
If you don't want to build your own model, the Homedata AVM endpoint (/api/avm/{uprn}/) does all of this for you — comparable selection, time adjustment, price-per-sqm calculation, and confidence scoring — in a single call. It returns an estimated sale value, rental value, price range, and the comparables it used.
Build your own when you need custom weighting, proprietary adjustments, or want to combine property data with your own internal datasets. Use the built-in AVM when you want a production-ready answer in one API call.
Ready to start? Get a free API key — 100 calls/month, no credit card. The Starter plan includes all endpoints used in this tutorial.
Build your own AVM today
Free tier includes comparables, property data, EPC, and price growth.
Get free API key →