Address Lookup Tutorial: Typeahead → Resolve → Enriched Property in Under 50 Lines
Build an address autocomplete with property enrichment using the Homedata API. From typeahead to bedrooms, EPC score, and comparable sales — in under 50 lines of Python or JavaScript.
What you're building
An address autocomplete that doesn't just return a formatted string. It returns a property. Type a street name, pick an address, and your form fills with bedrooms, property type, EPC score, floor area, and the last sold price. No manual lookups, no secondary API calls. One resolved UPRN gets you there.
The OS Open UPRN dataset (the canonical source for UPRN-to-address mapping) refreshes quarterly. The April 2026 release is the current dataset, see the Ordnance Survey OS Open UPRN page for the latest publication dates.
This tutorial covers three steps:
- Find: instant typeahead across 36 million UK addresses (2 tokens)
- Retrieve: resolve a UPRN to enriched property data (API key required)
- Chain: use the UPRN to pull EPC data and comparable sales
The full working implementation is under 50 lines of Python. The JavaScript widget is about the same. Both are below.
You'll need:
- A Homedata API key: pay as you go, 100 tokens = £1
- Python 3.8+ (for the server-side examples)
pip install requests
Step 1: Find: address typeahead (2 tokens)
The Find endpoint is the entry point. It queries a 36-million-address Elasticsearch index and returns suggestions in under 50ms. It takes the same API key as every other REST endpoint. (The browser autocomplete lane is different: that one takes a public widget key.)
Pass a q parameter with at least 2 characters. It handles postcodes, street names, building numbers, all three query modes auto-detect from the input.
curl "https://api.homedata.co.uk/address/find/?q=10+downing+street" \
-H "Authorization: Api-Key YOUR_API_KEY"
{
"suggestions": [
{
"uprn": 100023336956,
"address": "10 DOWNING STREET, LONDON, SW1A 2AA",
"postcode": "SW1A 2AA",
"town": "London"
},
{
"uprn": 10008315461,
"address": "10A DOWNING STREET, LONDON, SW1A 2AB",
"postcode": "SW1A 2AB",
"town": "London"
}
],
"count": 2
}
The key field is uprn. That's the Unique Property Reference Number: your handle to everything that follows. Store it. Pass it to Retrieve.
You can also filter results to a specific postcode with ?postcode=SW1A, or increase the result count up to 15 with ?limit=10.
Alternative: list all addresses at a postcode
If you already know the postcode, batch processing a dataset, or building a postcode-first lookup flow, use the Postcode endpoint instead. It returns every address at that postcode with full building identifiers, and takes the same API key as every other endpoint.
curl "https://api.homedata.co.uk/address/postcode/SW1A2AA/" \
-H "Authorization: Api-Key YOUR_API_KEY"
# Response
{
"postcode": "SW1A 2AA",
"count": 3,
"addresses": [
{
"uprn": 100023336956,
"address": "10 DOWNING STREET, LONDON, SW1A 2AA",
"building_name": "",
"building_number": "10",
"sub_building": "",
"street": "Downing Street",
"town": "London"
}
]
}
Each address includes building_number and building_name: useful if you need to chain to an endpoint that requires a building identifier rather than a UPRN.
Step 2: Retrieve: resolve a UPRN to a full property record
Retrieve is where the value is. Pass a UPRN at the tier you need: each tier is a path segment, and a strict superset of the one below it:
/address/(5 tokens): formatted address + coordinates/base/(10 tokens): adds bedrooms, property type, EPC score, floor area, last sold price/core/(25 tokens): adds valuation estimate, construction details, structural data, area stats, 55+ fields total
For most form-filling and qualification use cases, /base/ is the right call. It's the balance point between data richness and response speed.
curl "https://api.homedata.co.uk/property/100023336956/base/" \
-H "Authorization: Api-Key YOUR_API_KEY"
{
"uprn": 100023336956,
"full_address": "10 DOWNING STREET, LONDON, SW1A 2AA",
"address_line_1": "10 Downing Street",
"address_line_2": "",
"postcode": "SW1A 2AA",
"latitude": 51.5034,
"longitude": -0.1276,
"property_type": "Terraced",
"bedrooms": 4,
"bathrooms": 3,
"epc_floor_area": 248,
"current_energy_efficiency": 71,
"potential_energy_efficiency": 83,
"last_epc_date": "2019-06-14",
"last_sold_date": "2010-01-12",
"last_sold_price_gbp": 825000
}
With one API call you know: the property is a 4-bed terraced house, EPC Band C (score 71), 248m² floor area, last sold for £825k in 2010. That's a mortgage application pre-filled. Or a lettings listing scaffolded. Or a valuation context built.
The current_energy_efficiency score maps to letter bands: 1–20 = G, 21–38 = F, 39–54 = E, 55–68 = D, 69–80 = C, 81–91 = B, 92–100 = A. You don't need to call the EPC endpoint separately if you just want the band. Retrieve gives you the score and you derive the band yourself.
Step 3: Chain: EPC details and comparable sales
The UPRN from Find is the key to every other endpoint. Here's where the real power shows up: once you have it, a single lookup triggers a cascade of property intelligence.
For an EPC deep-dive, construction age, floor area from the official certificate, EPC id, call the EPC endpoint separately. The code below chains all three: Find → Retrieve → Comparables → EPC, with the same UPRN threading through each step.
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://api.homedata.co.uk"
HEADERS = {"Authorization": f"Api-Key {API_KEY}"}
# Step 1: Find the address
suggestions = requests.get(
f"{BASE}/address/find/",
params={"q": "42 acacia avenue, leeds", "limit": 5},
headers=HEADERS,
).json()["suggestions"]
if not suggestions:
raise SystemExit("No results found")
# Pick the first match
uprn = suggestions[0]["uprn"]
address = suggestions[0]["address"]
print(f"Resolved: {address} (UPRN {uprn})")
# Step 2: Enrich the property (key required)
prop = requests.get(
f"{BASE}/property/{uprn}/base/",
headers=HEADERS
).json()
print(f" Type: {prop.get('property_type')} | Beds: {prop.get('bedrooms')}")
print(f" Floor area: {prop.get('epc_floor_area')}m²")
print(f" EPC score: {prop.get('current_energy_efficiency')} (potential: {prop.get('potential_energy_efficiency')})")
# A property with no recorded sale returns null here, and a format spec on
# None raises TypeError — so decide the text before formatting it. The
# JavaScript version below is already safe: it uses optional chaining.
sold = prop.get('last_sold_price_gbp')
sold_text = f"£{sold:,} on {prop.get('last_sold_date')}" if sold is not None else "no recorded sale"
print(f" Last sold: {sold_text}")
# Step 3: Pull comparable sales
comps = requests.get(
f"{BASE}/comparables/{uprn}/",
params={"count": 5},
headers=HEADERS
).json()
print(f"\nNearby comparables ({comps['total_results']} found):")
for c in comps["comparables"][:3]:
print(f" {c['address']} — £{c['sold_let_price']:,} ({c['sold_let_date']})")
# Step 4: EPC deep-dive (construction age, floor area, certificate id)
epc = requests.get(
f"{BASE}/epc-checker/{uprn}/",
headers=HEADERS
).json()
print(f"\nEPC detail:")
print(f" Score: {epc['current_energy_efficiency']} → potential {epc['potential_energy_efficiency']}")
print(f" Floor area: {epc['epc_floor_area']}m²")
print(f" Construction age: {epc.get('construction_age_band', 'N/A')}")
print(f" Last assessed: {epc['last_epc_date']}")
The Python version is 41 lines including whitespace and comments. The JavaScript is 36 lines. Both go from a natural-language search query to a full property record with recent comparables, one resolved UPRN, three API calls.
Building the JavaScript widget
Server-side is one pattern. The more common use case is a live typeahead in a browser form. Here's a minimal implementation using vanilla JS and the Find endpoint, no framework, no dependencies:
<input id="address-input" type="text" placeholder="Start typing an address...">
<ul id="suggestions"></ul>
<script>
const API_KEY = "YOUR_API_KEY";
const headers = { Authorization: `Api-Key ${API_KEY}` };
const input = document.getElementById("address-input");
const list = document.getElementById("suggestions");
let debounceTimer;
input.addEventListener("input", () => {
clearTimeout(debounceTimer);
const q = input.value.trim();
if (q.length < 2) { list.innerHTML = ""; return; }
debounceTimer = setTimeout(async () => {
// Find
const res = await fetch(`https://api.homedata.co.uk/address/find/?q=${encodeURIComponent(q)}`, { headers });
const { suggestions } = await res.json();
list.innerHTML = suggestions.map(s =>
`<li data-uprn="${s.uprn}">${s.address}</li>`
).join("");
}, 200);
});
list.addEventListener("click", async (e) => {
const li = e.target.closest("li[data-uprn]");
if (!li) return;
const uprn = li.dataset.uprn;
input.value = li.textContent;
list.innerHTML = "";
// Retrieve: API key required
const res = await fetch(
`https://api.homedata.co.uk/property/${uprn}/base/`,
{ headers: { Authorization: `Api-Key ${API_KEY}` } }
);
const prop = await res.json();
// Auto-fill your form fields
document.getElementById("bedrooms").value = prop.bedrooms ?? "";
document.getElementById("property-type").value = prop.property_type ?? "";
document.getElementById("floor-area").value = prop.epc_floor_area ?? "";
document.getElementById("epc-score").value = prop.current_energy_efficiency ?? "";
});
</script>
The 200ms debounce prevents hammering the Find endpoint on every keystroke. Both Find and Retrieve need an API key, so in production you'd proxy these calls through your own backend rather than embedding the key in client-side JS.
Practical patterns
Pattern 1: Mortgage or conveyancing intake form
The user types their address into a search field. You resolve it to a UPRN via Find, then call Retrieve at the /base/ tier to get last sold price, property type, bedrooms, and EPC data. Then call the Flood Risk endpoint separately with the same UPRN for the NAFRA2 flood band. The form auto-fills. Your underwriters get pre-qualified data instead of applicant self-reporting.
Pattern 2: Lettings platform listing creation
Agent types the property address. Find returns the UPRN. Retrieve at the /base/ tier populates: bedrooms, property type, floor area, EPC band. They can override manually if anything's wrong. Listing creation goes from a 5-minute form to a 30-second confirmation.
Pattern 3: AVM input enrichment
Your automated valuation model needs clean structured inputs. Instead of asking users for property type and bedrooms (which they often get wrong), resolve the address to a UPRN and pull the data directly. Chain to Comparables for recent sales within 0.5 miles with matching bedrooms. You've built a lightweight AVM input pipeline in 50 lines.
Pattern 4: Insurance quoting
Address search → Retrieve the /base/ tier → read current_energy_efficiency and epc_floor_area. Separately call the Flood Risk endpoint with the same UPRN for NAFRA2 flood band. You have the key risk factors for a buildings insurance quote without the user filling in a single property characteristic.
API reference quick look
| Endpoint | Auth | Returns |
|---|---|---|
| /address/find/?q= | API key | Address suggestions with UPRN |
| /address/postcode/{postcode}/ | API key | All addresses at a postcode |
| /property/{uprn}/{tier}/ | Api-Key required | Enriched property data: tiers: address/base/core/complete |
| /comparables/{uprn}/ | Api-Key required | Comparable sales within 0.5 miles |
| /epc-checker/{uprn}/ | Api-Key required | EPC certificate (7 fields: score, floor area, construction age, EPC id) |
Auth header format
All authenticated endpoints use Authorization: Api-Key YOUR_API_KEY, not Bearer. The Bearer format returns a 403.
Next steps
Pay as you go: 100 tokens = £1 and your first top-up is matched 100%, enough to build and test a real integration. A monthly subscription earns bonus tokens when you go live. Address find and postcode endpoints cost 2 tokens each, and address retrieve costs 5 tokens, reflecting real licensing costs but still far cheaper than per-lookup competitors.
- Get your API key: free, no card required
- API reference: all endpoints, parameters, and response schemas
- Address Lookup API overview: use cases, pricing, and field reference
- What is a UPRN?, if you want the full backstory on why UPRNs exist
Start building with a free API key →
Pay as you go · EPC, Land Registry, council tax, schools · First top-up matched 100%