Replacing getAddress.io? Free drop-in replacement →
← Back to Blog
Tutorials Feb 17, 2026 · 12 min read min read

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:

  1. Comparable sales — recent transactions for similar properties nearby. The most important signal.
  2. Property attributes — bedrooms, floor area (m²), property type (detached/semi/terrace/flat), EPC rating.
  3. 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 →