UK property data API endpoint reference
Responses are JSON unless a route specifies CSV. Authentication uses Authorization: Api-Key YOUR_KEY on every request.
https://api.homedata.co.uk/api
Find the property data you need
Resolve a UK address to a UPRN, then retrieve property data as JSON. Use the EPC endpoint for energy ratings and certificate details, or choose a property tier for sold prices, title data and environmental risk in one response.
New to the API? Start with the developer guide or make your first request.
Your revealed leads
Read the leads you have already revealed in Lead Source using your API key. These reads cost 0 tokens; revealing a lead is billed separately.
- Revealed listings: GET
/api/explore/my-leads/ - Revealed properties: GET
/api/explore/my-leads/properties/ - Full CSV download: GET
/api/explore/leads/export.csv
Read a page at a time
Both list routes return JSON, newest reveal first. Send page (default 1) and page_size (default 200, maximum 500). Both must be positive integers; invalid values return HTTP 400 with Must be a positive integer.
curl 'https://api.homedata.co.uk/api/explore/my-leads/?page=2&page_size=100' \
-H 'Authorization: Api-Key YOUR_KEY'
The response’s meta contains total (all matching leads, not just this page), page, page_size and has_more. For example, with 250 matching leads, page 2 at size 100 has:
"meta": {"total": 250, "page": 2, "page_size": 100, "has_more": true}
Increment page while has_more is true. Use the same parameters on /api/explore/my-leads/properties/ to page through revealed properties.
Download everything as CSV
The export downloads all your revealed leads without paging. Optionally filter by reveal date with from and to, each in YYYY-MM-DD format. Dates are inclusive UK dates; to must be on or after from. Omit both for the full export.
curl 'https://api.homedata.co.uk/api/explore/leads/export.csv?from=2026-11-01&to=2026-11-03' \
-H 'Authorization: Api-Key YOUR_KEY' \
-o revealed-leads.csv
Use the dedicated export.csv route: it always returns CSV, regardless of Accept or format. Adding ?format=csv to a list route is unsupported and returns 404.
CRM Match: quote, confirm, enrich
Run the dashboard’s contact matching and move scoring from your code. Matching runs on home.co.uk, where the scores live. This wallet billing workflow uses https://homedata.co.uk/api/v1/crm-match, with Authorization: Api-Key YOUR_KEY. No session or CSRF token is needed. There is no extra request charge.
- Quote: POST
/matchwith a JSONcsvstring. Up to 2,000 rows and 2,000,000 characters. Headers are detected; optionally passmappingwithhas_header(boolean),address(zero-based column index), and optionalname,email,phoneindices. - Confirm: re-post the exact CSV and mapping with the returned
run_idandmax_tokensequal to the accepted price. One token per scored contact, deduplicated to one contact per property (first contact wins). Matched-but-unscored and unmatched contacts are free. - Enrich: POST
/enrichwithtier(Base:base, Core:core, Complete:complete) and up to 10 contacttokens. Packs use the property API’s prices: 10, 25 and 50 tokens respectively, per property served. Sending this request authorises those pack charges.
Quote request and response
POST https://homedata.co.uk/api/v1/crm-match/match
{"csv":"name,address\nAlex,12 High Street London SW1A 1AA"}
{"submitted":1,"matched_count":1,"scored_count":1,
"unscored_count":0,"unmatched_count":0,"skipped":0,
"pending_payment":true,"run_id":"sealed quote token",
"price":1,"price_per_contact":1,"requoted":false}
Confirm and results
{"csv":"name,address\nAlex,12 High Street London SW1A 1AA",
"run_id":"the returned quote token","max_tokens":1}
// Response includes the same counts, plus:
{"charged":1,"balance":99,
"matches":[{"lead":"Alex","lead_email":null,"lead_phone":null,
"masked_address":"High Street, SW1A","score":55,"band":"warm"}],
"contacts":[{"lead":"Alex","lead_email":null,"lead_phone":null,
"address":"12 High Street London SW1A 1AA",
"masked_address":"High Street, SW1A","score":55,"band":"warm",
"token":"sealed enrichment token"}],"unmatched":[]}
Quotes contain counts and price only. If nothing is scored, the first call returns free results directly. matches is the hottest-first preview (up to 100); contacts contains every matched property, including unscored rows with null score and band; unmatched echoes your own unmatched contacts. Only addresses go to the resolver; names, emails and phones are rejoined within the request. Uploads and results are never stored. Responses use Cache-Control: no-store.
Enrichment response
POST https://homedata.co.uk/api/v1/crm-match/enrich
{"tier":"base","tokens":["the contact's returned token"]}
{"tier":"base","fields":[],"results":[
{"index":0,"status":"ok","tokens":10,"data":{"uprn":100012345}}
],"out_of_tokens":false}
The data object is the full property-tier response (abbreviated above). Match results expose only the masked property address and a sealed token, never a resolved UPRN or property ID. Your own input address is echoed unchanged. A paid pack includes its full address and UPRN. Tokens expire after 120 minutes and are bound to your organisation. Per-row statuses are ok, expired, failed, not_found or out_of_tokens; only successful packs are charged. Keep successful rows and stop sending batches on out_of_tokens. Pack enrichment retries are new paid property calls.
For individual columns, replace tier with fields, for example ["epc_rating","council_tax_band"]. Available keys: epc_rating, council_tax_band, bedrooms, floor_area, construction_age_band, property_type, last_sold, garden, parking, roof, broadband. Uses the dashboard’s per-field prices and masking; do not send both fields and tier.
Retries, expiry and errors
A quote lasts 30 minutes and is bound to the exact file, mapping and organisation. An expired or changed quote, or a price exceeding max_tokens, returns a new pending_payment quote without charging or revealing rows. Review it before confirming. Retry a lost confirmation response with the same body and run ID: wallet debits are idempotent. A 503 with unconfirmed:true means the charge may have landed; retry that same confirmation. A 503 with charged:false means no debit occurred.
401: missing or invalid key. 403: revoked, expired or unlinked organisation key, or a key whose endpoint list does not include CRM Match (endpoint_not_allowed; standard customer keys include it). 402: insufficient wallet tokens, with required, available and topup_url. 422: invalid CSV, mapping, tier or batch. 429: rate limit (12 match requests and 120 enrich batches per minute). 503: resolver or wallet unavailable. The incident kill switch returns 404 from /match and /enrich.
Complete endpoint catalogue
Every endpoint currently offered in the Playground. Enterprise data is arranged with our team.
-
Address Match GET
/address/match/ -
Property, Discovery GET
/property/:uprn/ -
Property, Address tier GET
/property/:uprn/address/ -
Property, Base tier GET
/property/:uprn/base/ -
Property, Core tier GET
/property/:uprn/core/ -
Property, Complete tier GET
/property/:uprn/complete/ -
Property, Custom (?with=…) GET
/property/:uprn/ -
All Property Listings EnterpriseEnquire about this data
-
Sale Events GET
/property_sale_events/ -
Rooms GET
/properties/:uprn/rooms/ -
Roof GET
/properties/:uprn/roof/ -
Garden GET
/properties/:uprn/garden/ -
Parking GET
/properties/:uprn/parking/ -
EPC Renovations GET
/properties/:uprn/epc_renovations/ -
Ground stability GET
/properties/:uprn/ground_stability/ -
Dimensions GET
/properties/:uprn/dimensions/ -
Land Area GET
/properties/:uprn/land/ -
Construction GET
/properties/:uprn/construction/ -
EPC Rating GET
/properties/:uprn/epc/ -
EPC History GET
/properties/:uprn/epc_history/ -
Environmental Risks GET
/risks/:risk_type/ -
Deprivation Index GET
/deprivation/ -
Price Trends GET
/price_trends/:outcode/ -
Price Distributions GET
/price_distributions/:outcode/ -
Postcode Lookup GET
/address/postcode/:postcode/ -
Land Registry Sales EnterpriseEnquire about this data
-
Council Tax Band GET
/council_tax_band/:uprn/ -
Council Tax Full GET
/council_tax/:uprn/ -
Comparables EnterpriseEnquire about this data
-
Search live listings EnterpriseEnquire about this data
-
Unlock UPRN + Address POST
/listing-address/ -
Land Registry Titles GET
/property_lr_titles/:uprn/ -
Boundary Autocomplete GET
/boundaries/autocomplete/ -
Planning Applications GET
/planning/search/ -
Crime Data GET
/crime/ -
Solar Assessment GET
/solar-assessment/:uprn/ -
Postcode Profile GET
/postcode-profile/ -
Price Growth GET
/price-growth/:outcode/ -
Schools Nearby GET
/schools/nearby -
Broadband Speed GET
/broadband/ -
Mobile Coverage GET
/mobile-coverage/ -
Mobile coverage (one address) GET
/properties/:uprn/mobile_coverage/ -
Amenities (all) GET
/amenities/ -
Food GET
/amenities/food/ -
Education GET
/amenities/education/ -
Healthcare (OSM) GET
/amenities/healthcare/ -
Financial GET
/amenities/financial/ -
Civic GET
/amenities/civic/ -
Worship GET
/amenities/worship/ -
Culture GET
/amenities/culture/ -
Convenience GET
/amenities/convenience/ -
Green Spaces GET
/amenities/green_spaces/ -
Transport GET
/amenities/transport/ -
Shops GET
/amenities/shops/ -
Fuel Stations (all) GET
/fuel-stations/ -
Petrol Stations GET
/fuel-stations/petrol/ -
EV Chargers GET
/fuel-stations/ev/ -
Healthcare (all) GET
/healthcare/ -
GPs GET
/healthcare/gps/ -
Dentists GET
/healthcare/dentists/ -
Pharmacies GET
/healthcare/pharmacies/ -
Hospitals GET
/healthcare/hospitals/ -
Demographics GET
/demographics/ -
Listed Buildings GET
/listed-buildings/ -
Stamp Duty Calculator GET
/calculators/stamp-duty/ -
Mortgage Calculator GET
/calculators/mortgage/ -
Rental Appraisal POST
/rental-appraisal/
Everything you can know about a UPRN now lives behind one URL: /property/{uprn}/. Pick a tier, build your own with ?with=, or just call the bare URL for a discovery menu of what's available. Same fetchers, same response shape, predictable bundle pricing.
| Behaviour | URL | Cost | Returns |
|---|---|---|---|
| Discovery | /property/{uprn}/ | 1 | Menu: slug list with per-slug cost + per-UPRN availability + tier shortcuts |
| Custom | /property/{uprn}/?with=… | sum | Pick slugs, pay per slug at native weights |
| Address | /property/{uprn}/address | 5 | Address fields only: replaces /address/retrieve/ |
| Base | /property/{uprn}/base | 10 | Address + rooms + EPC + lr_title + last sold + construction + dimensions + garden/parking |
| Core | /property/{uprn}/core | 25 | Base + council tax band + flood + schools + broadband + crime + demographics + valuation estimate + solar + lr_sales + title boundaries (GeoJSON; all linked titles) + planning constraints (listed / conservation / designations) |
| Complete | /property/{uprn}/complete | 50 | Core + council tax full (GBP charges) + comparables + live listings + full env risks + deprivation + planning application history + price trends/distributions/growth + full title (ownership + boundary) |
Names we don't recognise. In a custom call, any add-on name in ?with= that we don't recognise is listed in unknown_slugs, with valid_slugs beside it, and the names we do know are served and charged as usual. If none of the names is known, the call is refused with a free 400, invalid_parameter, whose valid_slugs lists the names you can use. ?fields= is accepted as another name for ?with=.
/address/retrieve/{uprn}/ and /properties/{uprn}/ (plural) are deprecated aliases. They are still served and have no removal date yet. Migrate by updating the URL: same UPRN, same response shape, same cost on the address-retrieve path.
Lean property record: 15 base fields covering property type and classification, EPC scores and floor area, windows, fireplaces, extensions, the last Land Registry sale, and price estimates. Add address, rooms, title and more with ?with= (e.g. ?with=address,rooms). Returns data for any of the UK's 29M+ addressable properties.
curl https://api.homedata.co.uk/properties/10093609154 \ -H "Authorization: Api-Key YOUR_API_KEY"
import requests
response = requests.get(
"https://api.homedata.co.uk/properties/10093609154",
headers={"Authorization": "Api-Key YOUR_API_KEY"}
)
data = response.json()
print(data["property_type"], data["last_sold_date"])
const response = await fetch( "https://api.homedata.co.uk/properties/10093609154", { headers: { "Authorization": "Api-Key YOUR_API_KEY" } } ); const data = await response.json(); console.log(data.property_type, data.last_sold_date);
View example response
{
"uprn": 10093609154,
"property_type": "Flat",
"classification_code": "RD06",
"epc_floor_area": 56.0,
"current_energy_efficiency": 84,
"potential_energy_efficiency": 84,
"last_epc_date": "2020-07-28",
"windows_type": "Double glazed",
"fireplaces": 0,
"extensions": 0,
"last_sold_date": "2019-11-15",
"last_sold_price": 135000,
"last_sold_txid": "9A1C2E3F-…",
"predicted_price": 142000,
"average_area_price": 198000
}
These are the 15 base fields. Add more with ?with= (e.g. ?with=address,rooms): see the Data Dictionary.
Energy Performance Certificate data. Returns current and potential energy efficiency scores (1–100), last EPC date, floor area, construction age band, and certificate ID.
curl https://api.homedata.co.uk/epc-checker/100023336956/ \ -H "Authorization: Api-Key YOUR_API_KEY"
import requests
response = requests.get(
"https://api.homedata.co.uk/epc-checker/100023336956/",
headers={"Authorization": "Api-Key YOUR_API_KEY"}
)
epc = response.json()
print(f"Rating: {epc['current_energy_rating']}") # the A-G letter
print(f"Score: {epc['current_energy_efficiency']}") # the SAP score, 1-100
const response = await fetch( "https://api.homedata.co.uk/epc-checker/100023336956/", { headers: { "Authorization": "Api-Key YOUR_API_KEY" } } ); const epc = await response.json(); console.log(`Rating: ${epc.current_energy_rating}`); // the A-G letter console.log(`Score: ${epc.current_energy_efficiency}`); // the SAP score, 1-100
View example response
{
"uprn": 10093609154,
"current_energy_efficiency": 84,
"potential_energy_efficiency": 84,
"current_energy_rating": "B",
"potential_energy_rating": "B",
"last_epc_date": "2020-07-28",
"epc_floor_area": 59,
"construction_age_band": "2007-2011",
"epc_id": "1813679032512020072814144822200478"
}
Environmental risk assessment by UPRN. Returns risk level, score, and contextual data. Use all to get every risk type in a single request.
Available risk types
flood
radon
noise
landfill
coal_mining
invasive_plants
air_quality_today
all
# Single risk type curl "https://api.homedata.co.uk/risks/flood/?uprn=100023336956" \ -H "Authorization: Api-Key YOUR_API_KEY" # All risk types in one call curl "https://api.homedata.co.uk/risks/all/?uprn=100023336956" \ -H "Authorization: Api-Key YOUR_API_KEY"
import requests
response = requests.get(
"https://api.homedata.co.uk/risks/flood/",
params={"uprn": "100023336956"},
headers={"Authorization": "Api-Key YOUR_API_KEY"}
)
data = response.json()
for risk in data["results"]:
print(f"{risk['risk_type']}: {risk['label']} (score {risk['score']})")
const response = await fetch( "https://api.homedata.co.uk/risks/flood/?uprn=100023336956", { headers: { "Authorization": "Api-Key YOUR_API_KEY" } } ); const data = await response.json(); data.results.forEach(r => console.log(`${r.risk_type}: ${r.label} (score ${r.score})`) );
View example response
{
"risk_type": "flood",
"uprn": 100023336956,
"location": {
"lat": 51.503541,
"lon": -0.12767
},
"results": [
{
"risk_type": "flood_rivers_sea",
"label": "Very low",
"score": 1,
"score_unit": "band",
"intersects": true,
"distance_m": 0,
"radius_description": "At property",
"properties": {
"description": "Less than 1 in 1,000 chance of flooding each year"
}
},
{
"risk_type": "flood_surface_water",
"label": "Low",
"score": 2,
"score_unit": "band",
"intersects": true,
"distance_m": 0,
"radius_description": "At property",
"properties": {
"description": "Between 1 in 1,000 and 1 in 100 chance of flooding each year"
}
}
]
}
One API call returns the full area profile for a UK postcode. Bundles deprivation index, broadband speed, sold price history, nearby schools (Ofsted), transport stops, Census 2021 demographics, environmental risk (flood + radon), council tax band distribution, property count by type, and HPI trend. Equivalent to PropertyData's /postcode-key-stats: we return it in a single call.
postcode (required), full UK postcodecurl "https://api.homedata.co.uk/postcode-profile?postcode=SW1A%202AA" \ -H "Authorization: Api-Key YOUR_API_KEY"
from homedata import Homedata
client = Homedata("YOUR_API_KEY")
profile = client.postcode_profile.get("SW1A 2AA")
print(profile.deprivation.imd_decile) # 8
print(profile.broadband.avg_download_mbps) # 142.3
print(profile.census.tenure.owned_outright_pct) # 20.5
import { Homedata } from '@homedata/js'
const client = new Homedata('YOUR_API_KEY')
const profile = await client.postcodeProfile.get('SW1A 2AA')
console.log(profile.deprivation.imd_decile) // 8
console.log(profile.broadband.full_fibre_available_pct) // 62.0
Response shape
Attribution
location is a postcode centroid published under the Open Government Licence. The API returns this notice in the attribution field, and you must reproduce it wherever you publish the coordinates:
Contains OS data © Crown copyright and database right 2026. Contains Royal Mail data © Royal Mail copyright and database right 2026. Contains National Statistics data © Crown copyright and database right 2026. Licensed under the Open Government Licence v3.0.
Monthly median asking prices by outcode (e.g. E1, SW1A, M3) over the past 12 months. Also returns a volatility score (0–100) based on month-to-month price variation.
outcode (path), UK outcode (e.g. E1, SW1A)curl https://api.homedata.co.uk/price_trends/SW1A/ \ -H "Authorization: Api-Key YOUR_API_KEY"
import requests
response = requests.get(
"https://api.homedata.co.uk/price_trends/SW1A/",
headers={"Authorization": "Api-Key YOUR_API_KEY"}
)
data = response.json()
for month, price in data["monthly_average_prices"].items():
print(f"{month}: £{price:,}")
const response = await fetch( "https://api.homedata.co.uk/price_trends/SW1A/", { headers: { "Authorization": "Api-Key YOUR_API_KEY" } } ); const data = await response.json(); Object.entries(data.monthly_average_prices).forEach(([month, price]) => console.log(`${month}: £${price.toLocaleString()}`) );
View example response
{
"outcode": "SW1A",
"monthly_average_prices": {
"2025-03": 5750000,
"2025-04": 5147500,
"2025-05": 1000000,
"2025-06": 8600000,
"2025-07": 1900000,
"2025-08": 1850000,
"2025-09": 2000000,
"2025-11": 8600000,
"2025-12": 6269900,
"2026-01": 5225000,
"2026-02": 5975000
},
"volatility_score": 100
}
Price distribution data for an outcode. Shows percentile breakdowns (25th, 50th, 75th) and averages by property type, useful for market segmentation and pricing analysis.
outcode (path), UK outcode (e.g. E1, SW1A)curl https://api.homedata.co.uk/price_distributions/E1/ \ -H "Authorization: Api-Key YOUR_API_KEY"
import requests
response = requests.get(
"https://api.homedata.co.uk/price_distributions/E1/",
headers={"Authorization": "Api-Key YOUR_API_KEY"}
)
data = response.json()
for price_range, count in data["bins"].items():
print(f"£{price_range}: {count} properties")
const response = await fetch( "https://api.homedata.co.uk/price_distributions/E1/", { headers: { "Authorization": "Api-Key YOUR_API_KEY" } } ); const data = await response.json(); Object.entries(data.bins).forEach(([range, count]) => console.log(`£${range}: ${count} properties`) );
View example response
{
"outcode": "E1",
"bins": {
"125000-695000": 342,
"695000-1265000": 187,
"1265000-1835000": 45,
"1835000-2405000": 12,
"2405000-2975000": 3
}
}
List all addresses in a postcode. Returns UPRNs so you can chain into other endpoints. Use URL-encoded or concatenated postcodes (e.g. SW1A2AA or SW1A%202AA).
Authorization: Api-Key YOUR_KEYcurl https://api.homedata.co.uk/address/postcode/SW1A2AA/ \
-H "Authorization: Api-Key YOUR_KEY"
import requests
response = requests.get(
"https://api.homedata.co.uk/address/postcode/SW1A2AA/",
headers={"Authorization": "Api-Key YOUR_KEY"},
)
data = response.json()
for addr in data["addresses"]:
print(f"{addr['address']} (UPRN: {addr['uprn']})")
const response = await fetch( "https://api.homedata.co.uk/address/postcode/SW1A2AA/", { headers: { "Authorization": "Api-Key YOUR_KEY" } } ); const data = await response.json(); data.addresses.forEach(a => console.log(`${a.address} (UPRN: ${a.uprn})`) );
View example response
{
"postcode": "SW1A 2AA",
"count": 1,
"addresses": [
{
"uprn": 100023336956,
"uprn_token": "gAAAAABm4xK…",
"address": "PRIME MINISTER & FIRST LORD OF THE TREASURY, 10 DOWNING STREET, LONDON, SW1A 2AA",
"building_name": "",
"building_number": "10",
"sub_building": "",
"street": "Downing Street",
"town": "London"
}
]
}
URL encoding: Use %20 or remove spaces entirely in the URL path. Do NOT use +: it's treated literally in URL paths.
Match a submitted address and postcode to one property. This endpoint matches a complete submission; it does not provide search or typeahead suggestions.
Required parameters: address and postcode.
Authentication: Authorization: Api-Key YOUR_KEY.
200 OK: the address-level retrieve body plus address_resolution. Costs 5 tokens. Use the returned UPRN for further property data; no separate address retrieve call is needed. Weak fuzzy matches are explicitly flagged in address_resolution. Enrichment parameters (level, fields, with) do not apply to this endpoint.
422 Unprocessable Entity: reason is no_match when no property matches, or multiple_matches when the submission is ambiguous. Neither failure is charged and neither returns candidates. Correct the address or add the missing flat or building details before retrying.
curl --get "https://api.homedata.co.uk/address/match/" \ --data-urlencode "address=10 Downing Street" \ --data-urlencode "postcode=SW1A 2AA" \ -H "Authorization: Api-Key YOUR_KEY"
An estimate of what one home would sell for, or let for each month. Every estimate comes with a range, a confidence level and the comparable evidence behind it: how many nearby sales and listings it rests on, what stage each had reached, and how recent they are. The response also says where each input came from, so you can see when we filled a gap and send the real value next time.
Authorization: Api-Key YOUR_KEYinclude_comps=false. The estimate is the same either way. A refused call (any 4xx other than 402) and a 5xx are refunded.Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| uprn | integer | yes | The home's UPRN. No UPRN yet? Find it with Address Match. |
| type | string | optional | sale (default) for a sale value, or rent for a monthly rent. |
| include_comps | boolean | optional | Default true: the response lists the comparable homes (25 tokens). false returns the estimate and counts without the list (10 tokens). |
| bedrooms | integer | optional | Bedrooms, 0 or more. Overrides our record. |
| bathrooms | integer | optional | Bathrooms, 0 or more. Overrides our record. |
| property_type | string | optional | One of Detached, Semi-Detached, Terraced, Flat, Maisonette or Bungalow, spelt exactly so. Overrides our record. |
| floor_area_m2 | number | optional | Measured internal floor area in square metres, above 0. Overrides the EPC figure and our own estimate of the area. |
Every override is optional. Leave them out and we use our record of the home; send one when you know better, for example after a viewing or an extension, and the response marks that input override.
curl --get "https://api.homedata.co.uk/valuations/estimate/" \
--data-urlencode "uprn=999999000202" \
--data-urlencode "type=sale" \
-H "Authorization: Api-Key YOUR_API_KEY"
import requests
response = requests.get(
"https://api.homedata.co.uk/valuations/estimate/",
params={"uprn": 999999000202, "type": "rent", "bedrooms": 3},
headers={"Authorization": "Api-Key YOUR_API_KEY"},
timeout=30,
)
data = response.json()
print(data["estimate_gbp"], data["confidence"])
View example response (an invented home, synthetic UPRN, 1 comparable of each kind shown)
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: 3f9c2a7e5b1d4e08a6c4f2b19d7e5a31
X-Call-Weight: 25
{
"uprn": 999999000202,
"type": "sale",
"estimate_gbp": 285000,
"range_low_gbp": 271000,
"range_high_gbp": 299000,
"confidence": "high",
"confidence_score": 74,
"dispersion_pct": 9.8,
"method": "weighted_median_ppsf",
"area_stats": {
"average_price": 279400, "median_price": 281000,
"min_price": 238000, "max_price": 325000,
"sample_size": 18, "median_price_per_sqft": 312
},
"evidence": {
"comps_used": 18,
"signal_counts": {"sold": 9, "agreed": 4, "reduced": 2, "stale": 1, "active": 2},
"newest_comp_date": "2026-10-28",
"oldest_comp_date": "2025-11-14",
"index_months_available": 24,
"partial": false,
"unavailable_sources": []
},
"subject": {
"uprn": 999999000202, "property_type": "Terraced",
"bedrooms": 3, "bathrooms": 1, "epc_floor_area": 85.0,
"latitude": 53.8204, "longitude": -1.5763
},
"inputs": {
"bedrooms": {"value": 3, "source": "property_record", "predicted": false},
"property_type": {"value": "Terraced", "source": "property_record"},
"bathrooms": {"value": 1, "source": "property_record"},
"floor_area_m2": {"value": 85.0, "source": "epc"}
},
"sold_comparables": [
{"uprn": 999999000318, "address": "14 Example Terrace", "postcode": "LS6 2AB",
"bedrooms": 3, "property_type": "Terraced", "sold_price": 279500,
"sold_date": "2026-08-22", "distance_miles": 0.12, …}
],
"active_listings": [
{"uprn": 999999000427, "address": "Example Road, Leeds", "postcode": "LS6 2AD",
"bedrooms": 3, "property_type": "Terraced", "listing_price": 295000,
"latest_status": "For sale", "added_date": "2026-10-02", "distance_miles": 0.31, …}
],
"response_time_seconds": 3.412
}
Response fields
| Field | Type | Description |
|---|---|---|
| estimate_gbp | integer | The estimate in pounds: a sale value to the nearest £1,000, or a monthly rent to the nearest £25. |
| range_low_gbp, range_high_gbp | integer | The likely range around the estimate. It is wider when the evidence agrees less. |
| confidence | string | high, medium or low, read from confidence_score: 70 and above is high, 40 to 69 is medium, below 40 is low. |
| confidence_score | integer | 0 to 100. How strongly the evidence supports the estimate: how much there is, how closely it agrees, how near it is and whether the floor area is known. |
| dispersion_pct | number | null | How widely the comparable prices spread. Lower means they agree more closely. null when there are too few to measure. |
| method | string | How the estimate was reached, for example weighted_median_ppsf (from price per square foot and the floor area) or weighted_median_comparable (from comparable prices when no floor area is known). |
| evidence | object | What the estimate rests on. comps_used is the number of comparables; signal_counts splits them by stage: sold, agreed (sale agreed), reduced, stale (listed for a long time) and active. newest_comp_date and oldest_comp_date date the evidence; index_months_available is the months of local price history used. partial is true when part of the evidence could not be read in time, with unavailable_sources naming it; the estimate is still given, and a retry later uses the full set. Rent estimates add estimate_basis and estimate_note, which say in plain words what the rent rests on. |
| subject | object | The home valued: uprn, property_type, bedrooms, bathrooms, epc_floor_area, latitude and longitude. |
| inputs | object | The four inputs the estimate used, bedrooms, property_type, bathrooms and floor_area_m2, each as {value, source}. source is override (you sent it), property_record (our record), epc (the EPC), predicted (our own estimate, worth replacing if you know the real value) or unavailable. |
| area_stats | object | Prices across the comparables: average_price, median_price, min_price, max_price, sample_size, and median_price_per_sqft when floor areas are known. |
| sold_comparables, active_listings | array | Up to 6 of the strongest comparables of each kind: recent sales (or lets, for rent) and current listings. Each has its UPRN, address, postcode, bedrooms, type, price, date and distance_miles from the home. Left out with include_comps=false. |
| comps_included, comp_counts | boolean, object | Only with include_comps=false: comps_included is false and comp_counts gives how many of each kind the full response would have listed. |
| rent_pcm_gbp, rent_pw_gbp | integer | Only with type=rent: the rent per calendar month (the same as estimate_gbp) and per week. gross_yield_pct is added when we hold a sale value for the home. |
| response_time_seconds | number | How long the valuation took to compute. |
Response headers
| Header | Description |
|---|---|
| X-Request-Id | A unique id for the call. Log it, and quote it if you contact us about a response. |
| X-Call-Weight | The tokens the call was charged: 25 with the comparables, 10 without. |
Speed: show a loading state
A valuation is worked out when you ask for it, so it is slower than a plain data lookup. The first call for a home usually takes 3 to 4 seconds. Repeating the same request on the same day returns in well under a second. Some homes, flats especially, can take 10 to 15 seconds.
Show a loading state while you wait rather than a blank panel, and only call it when a value is needed. Give the call at least half a minute before you time it out.
Error codes
Errors use the standard envelope (error.code, message). Every error except 402 is refunded. See Errors for how to handle each status.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_uprn | uprn is missing or not a whole number. |
| 400 | invalid_type | type is not sale or rent. |
| 400 | invalid_bedrooms, invalid_bathrooms, invalid_floor_area_m2, invalid_property_type | An override is out of range or not one of the accepted values. The message says what is allowed. |
| 400 | valuation_failed | A sale value needs bedrooms and a property type, and we hold no value for one of them for this home. Send the missing bedrooms or property_type; the message names it. |
| 402 | insufficient_tokens | Your balance is below the cost of the call. Top up, then retry. |
| 403 | INVALID_API_KEY | No API key was sent, or the key is wrong or revoked. |
| 404 | not_found | We hold no home with that UPRN. |
| 404 | valuation_failed | We hold no location for the home, so there is nothing nearby to compare it with. |
| 500 | valuation_unavailable | The evidence could not be read in time. Retry after the Retry-After seconds. |
| 500 | internal_error | The valuation could not be completed. Retry; if it persists, send us the X-Request-Id. |
Enriched property data for a known UPRN. Use after Address Match or Address by Postcode to get property intelligence.
{tier} is a path segment, address or base (each tier is a strict superset of the one below; core and complete extend further still).Enrichment tiers
/address/
Address fields, UPRN, coordinates: 5 tokens
/base/
+ property type, bedrooms, bathrooms, EPC, floor area, last sold price: 10 tokens
# Address tier: 5 tokens curl https://api.homedata.co.uk/property/100023336956/address/ \ -H "Authorization: Api-Key YOUR_API_KEY" # Base tier (adds property type, beds, EPC, sold price) 10 tokens curl https://api.homedata.co.uk/property/100023336956/base/ \ -H "Authorization: Api-Key YOUR_API_KEY"
import requests
response = requests.get(
"https://api.homedata.co.uk/property/100023336956/base/",
headers={"Authorization": "Api-Key YOUR_API_KEY"}
)
addr = response.json()
print(f"{addr['full_address']}: {addr.get('bedrooms')} bed {addr.get('property_type')}")
const response = await fetch( "https://api.homedata.co.uk/property/100023336956/base/", { headers: { "Authorization": "Api-Key YOUR_API_KEY" } } ); const addr = await response.json(); console.log(`${addr.full_address}: ${addr.bedrooms} bed ${addr.property_type}`);
View example response (address tier)
{
"uprn": 100023336956,
"udprn": 7832854,
"usrn": 8400709,
"full_address": "10 DOWNING STREET, LONDON, SW1A 2AA",
"address_line_1": "10 Downing Street",
"address_line_2": "",
"address_line_3": "",
"building_name": "",
"building_number": "10",
"sub_building": "",
"street_name": "Downing Street",
"locality": "",
"town_name": "London",
"post_town": "London",
"postcode": "SW1A 2AA",
"outward_postcode": "SW1A",
"latitude": 51.5033,
"longitude": -0.1276
}
View example response (base tier)
{
// ... all address fields above, plus:
"property_type": "Semi-Detached",
"bedrooms": 4,
"predicted_bedrooms": 4,
"bathrooms": 2,
"habitable_rooms": 6,
"floors": 3,
"epc_floor_area": 180.0,
"predicted_floor_area": 175.0,
"current_energy_efficiency": 62,
"potential_energy_efficiency": 78,
"last_epc_date": "2020-07-28",
"last_sold_date": "2019-11-15",
"last_sold_price_gbp": 925000,
"last_sold_txid": "B8F2A1...",
"classification_code": "RD03"
}
Broadband speed and coverage data for any UK postcode: average and max download/upload speeds, superfast/ultrafast/gigabit availability percentages, and full-fibre coverage. Pass an outcode such as RH15 instead for premises-weighted availability across the whole area, at the same price as a postcode lookup. Data sourced from Ofcom's Connected Nations report.
Authorization: Api-Key YOUR_KEYpostcode or outcodeParameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| postcode | string | unless outcode is given | UK postcode. Spaces are stripped automatically. e.g. SW1A2AA or SW1A 2AA |
| outcode | string | unless postcode is given | UK outward code, e.g. RH15. Case and spaces are ignored. Use instead of postcode. |
curl "https://api.homedata.co.uk/broadband?postcode=SW1A2AA" \
-H "Authorization: Api-Key YOUR_API_KEY"
import requests
response = requests.get(
"https://api.homedata.co.uk/broadband",
params={"postcode": "SW1A2AA"},
headers={"Authorization": "Api-Key YOUR_API_KEY"},
)
data = response.json()
print(f"Avg download: {data['avg_download_speed']} Mbps")
print(f"Gigabit available: {data['gigabit_available_pct']}%")
const response = await fetch( "https://api.homedata.co.uk/broadband?postcode=SW1A2AA", { headers: { "Authorization": "Api-Key YOUR_API_KEY" } } ); const data = await response.json(); console.log(`Avg download: ${data.avg_download_speed} Mbps`); console.log(`Gigabit available: ${data.gigabit_available_pct}%`);
View example response
{
"postcode": "SW1A 2AA",
"avg_download_speed": 67.4,
"max_download_speed": 362.0,
"avg_upload_speed": 18.2,
"max_upload_speed": 36.0,
"superfast_available_pct": 98.0,
"ultrafast_available_pct": 72.0,
"gigabit_available_pct": 45.0,
"full_fibre_available_pct": 45.0,
"below_uso_pct": 0.5,
"connections_count": 24,
"premises_count": 26,
"data_year": 2026
}
Outcode summary
Request /broadband?outcode=RH15. The illustrative response below has availability percentages, without postcode speed or connection fields. Each percentage uses postcodes in the current release that report that metric, weighted by premises count. Missing premises counts use the mean count of known postcodes for that metric; if none has a positive count, a plain mean is used. Results are rounded to two decimals.
{
"outcode": "RH15",
"data_year": 2026,
"postcode_count": 812,
"premises_total": 15890,
"postcodes_missing_premises": 4,
"superfast_available_pct": 98.41,
"ultrafast_available_pct": 80.12,
"gigabit_available_pct": 61.07,
"full_fibre_available_pct": 55.3,
"below_uso_pct": 0.21
}
- outcode (string)
- Normalised outward code.
- postcode_count (integer)
- Number of postcodes in this outcode in the current release.
- premises_total (integer or null)
- Sum of known premises counts across those postcodes; null if none are known.
- postcodes_missing_premises (integer)
- Number of postcodes without a premises count.
On data_year 2026 rows, premises_count and full_fibre_available_pct are carried from the July 2025 Ofcom release because the Spring 2026 postcode files dropped both columns. Outcode premises_total and full-fibre availability use those carried values.
A postcode with no Ofcom data returns 404 and the lookup is refunded. An outcode with no postcodes in the current release also returns 404.
Response fields
| Field | Type | Description |
|---|---|---|
| avg_download_speed | float | Average download speed in Mbps |
| max_download_speed | float | Maximum download speed in Mbps |
| avg_upload_speed | float | Average upload speed in Mbps |
| max_upload_speed | float | Maximum upload speed in Mbps |
| superfast_available_pct | float | % of premises with superfast broadband (≥30 Mbps) available |
| ultrafast_available_pct | float | % of premises with ultrafast broadband (≥300 Mbps) available |
| gigabit_available_pct | float | % of premises with gigabit-capable connectivity |
| full_fibre_available_pct | float | % of premises with full-fibre (FTTP) available |
| below_uso_pct | float | % of premises below the Universal Service Obligation (10 Mbps down / 1 Mbps up) |
| connections_count | integer | Number of active broadband connections at this postcode |
| premises_count | integer | Total addressable premises at this postcode |
| data_year | integer | Year of the Ofcom Connected Nations dataset (e.g. 2026) |
Error codes
| Status | Code | Meaning |
|---|---|---|
| 422 | missing_postcode | No postcode query parameter provided |
| 422 | invalid_postcode | Postcode doesn't match UK format (e.g. SW1A 2AA) |
| 404 | not_found | Valid postcode but no Ofcom data available for it |
| 500 | service_unavailable | Broadband data temporarily unavailable (retry with backoff, respecting Retry-After when present). |
| 400 | invalid_request | Both postcode and outcode supplied; pass exactly one. |
| 400 | invalid_outcode | Invalid outward code; use a value such as RH15. |
Predicted mobile coverage from Ofcom for EE, Three, O2 and Vodafone. Ratings combine 4G and 5G and describe coverage in-home and outdoors. The postcode call returns one entry for every address Ofcom holds in a full UK postcode; for a single home, ask for it by UPRN and you get exactly that address, never a neighbour's.
Authorization: Api-Key YOUR_KEYpostcode (required), full UK postcode, spaces optional. Outcodes are rejected.Three ways to ask
| Request | Returns | Tokens per request |
|---|---|---|
| /mobile-coverage/?postcode=SW1A2AA | Every address Ofcom holds in the postcode | 1 |
| /properties/{uprn}/mobile_coverage/ | Exactly one address | 1 |
| /property/{uprn}/?with=mobile_coverage | The same single-address block inside a property request. The request costs the property record's base charge plus 1 for mobile_coverage and each other add-on's own charge (e.g. ?with=broadband,mobile_coverage is 1 + 1 + 1 = 3) |
2 |
mobile_coverage is listed in the discovery menu and included in ?with=all. It is not part of the Base, Core or Complete tiers: add it to a chain when you want it.
Ratings
Each operator carries a rating from 0 to 4 and Ofcom's own description for it, passed through unchanged. Use the description as the label you show.
| rating | description |
|---|---|
| 4 | Good in-home and outdoor |
| 3 | Variable in-home, good outdoor |
| 2 | Good (outdoor only) |
| 1 | Variable (outdoor only) |
| 0 | Poor to none (outdoor only) |
| null | null: Ofcom has no record for this operator at this address |
Combined 4G/5G only. Ofcom supplies one combined rating per operator. There are no separate 4G or 5G figures, and no indoor 5G figure, so don't label or derive them.
0 is a result, null is not. A rating of 0 means Ofcom predicts poor to no coverage outdoors. An operator with rating: null means Ofcom has no data for it there: treat it as unknown, never as 0.
Dates are Ofcom's. data_date is the date Ofcom created the record for that address, not the date we fetched it. Addresses in the same postcode can carry different dates and different coverage.
curl "https://api.homedata.co.uk/mobile-coverage/?postcode=SW1A2AA" \ -H "Authorization: Api-Key YOUR_API_KEY" # One address, with broadband alongside curl "https://api.homedata.co.uk/property/100023336956/?with=broadband,mobile_coverage" \ -H "Authorization: Api-Key YOUR_API_KEY"
import requests
response = requests.get(
"https://api.homedata.co.uk/mobile-coverage/",
params={"postcode": "SW1A2AA"},
headers={"Authorization": "Api-Key YOUR_API_KEY"},
)
data = response.json()
for home in data["availability"]:
for operator, cover in home["operators"].items():
label = cover["description"] or "No Ofcom record" # null is unknown, not 0
print(home["uprn"], operator, label)
print(data["attribution"]["notice"]) # show it, unchanged
const response = await fetch( "https://api.homedata.co.uk/mobile-coverage/?postcode=SW1A2AA", { headers: { "Authorization": "Api-Key YOUR_API_KEY" } } ); const data = await response.json(); for (const home of data.availability) { for (const [operator, cover] of Object.entries(home.operators)) { console.log(home.uprn, operator, cover.description ?? "No Ofcom record"); } }
View example response
The shape of a postcode response, shortened to one address. The ratings and date here are illustrative; a live call returns Ofcom's current figures for each address.
{
"postcode": "SW1A 2AA",
"technology": "4G/5G combined",
"availability": [
{
"uprn": "100023336956",
"postcode": "SW1A 2AA",
"data_date": "2025-07-01",
"operators": {
"EE": { "rating": 4, "description": "Good in-home and outdoor" },
"Three": { "rating": 3, "description": "Variable in-home, good outdoor" },
"O2": { "rating": 2, "description": "Good (outdoor only)" },
"Vodafone": { "rating": null, "description": null }
},
"ofcom": { … }
}
],
"attribution": {
"source": "Ofcom",
"logo_url": "https://www.ofcom.org.uk/siteassets/resources/images/illustrations/logos/powered-by-ofcom-api.png",
"notice": "The data displayed on mobile coverage availability uses source data made available by Ofcom which is based on data from the mobile operators about how strong they think signal levels are at every location in the UK. Each mobile operator has a slightly different approach to displaying coverage on its own map, including assumptions on the handsets used, levels of call reliability and the expected signal loss when indoors or in car. Because Ofcom brings all mobile operator data together in a single place and holds it to a single, independent standard, the mobile coverage availability information above may display different levels of coverage than those seen on the operators' websites. We would therefore recommend you also check the mobile provider's coverage checker. Mobile coverage data is created by coverage modelling, and since this is based on computer predictions is not error free.\n\nOfcom update the source data quarterly and the mobile network operators update theirs, but there may be times when the maps are based on slightly different data and therefore show different coverage."
},
"ofcom": { … }
}
Response fields
| Field | Type | Description |
|---|---|---|
| postcode | string | The postcode, normalised |
| technology | string | Always 4G/5G combined |
| availability[] | array | An entry for each address (UPRN) in the postcode, with the fields below. A one-address call wraps a single entry instead: see One address |
| uprn | string | The address's UPRN |
| data_date | date | Ofcom's creation date for this address's record (ISO date). Not the date we fetched it |
| operators | object | Keyed EE, Three, O2, Vodafone. Each has rating (0 to 4, or null) and description (Ofcom's descriptor, or null) |
| attribution | object | source, logo_url and notice. Show the logo and the whole notice beside the coverage (see below) |
| ofcom | object | Ofcom's own record, unchanged: per address inside each entry, and the full postcode payload at the top level |
One address
/properties/{uprn}/mobile_coverage/ returns the property's uprn and one mobile_coverage object holding that address's entry, with technology, attribution and Ofcom's record for it. Read the ratings from mobile_coverage.operators, not from the top level. A ?with=mobile_coverage property request carries the same object under the same key.
{
"uprn": 100023336956,
"mobile_coverage": {
"uprn": "100023336956",
"postcode": "SW1A 2AA",
"data_date": "2025-07-01",
"operators": {
"EE": { "rating": 4, "description": "Good in-home and outdoor" },
"Three": { "rating": 3, "description": "Variable in-home, good outdoor" },
"O2": { "rating": 2, "description": "Good (outdoor only)" },
"Vodafone": { "rating": null, "description": null }
},
"ofcom": { … },
"technology": "4G/5G combined",
"attribution": { "source": "Ofcom", "logo_url": "…", "notice": "…" }
}
}
When Ofcom holds no record for that address the call returns 404, not charged, with the same envelope:
{
"uprn": 100023336956,
"mobile_coverage": { "available": false, "reason": "no data on file for this property" }
}
Crediting Ofcom
Mobile coverage is Ofcom's data, provided under Ofcom's API terms of use. Wherever you show it, show the Powered by Ofcom logo from attribution.logo_url and the whole attribution.notice beside it, unchanged. Don't shorten the notice or swap in your own summary.
Showing it to your users
Copy we use in our own reports, if it helps:
| Heading | Mobile coverage |
| Description | Predicted mobile coverage from Ofcom for EE, Three, O2 and Vodafone. Ratings combine 4G and 5G and describe coverage in-home and outdoors. |
| Date | Ofcom data date: {data_date} |
| No record | Ofcom has no mobile coverage record for this address. This does not mean there is no mobile signal. Check your provider's coverage map. |
| Unavailable | Mobile coverage is temporarily unavailable. Please try again. |
Error codes
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_postcode | Missing, invalid or incomplete postcode. An outcode on its own (e.g. SW1A) is rejected |
| 404 | not_found | Ofcom has no coverage record for the postcode, or for that exact address. Not charged. The postcode call answers error.code: not_found; the one-address call answers mobile_coverage: {"available": false, "reason": …} inside its envelope |
| 500 | service_unavailable | Mobile coverage is temporarily unavailable. Not charged, and never reported as missing coverage: retry later. Tell it apart from other server errors by error.code |
In a ?with= chain, a missing record or a temporary failure shows on the mobile_coverage module alone and is not charged; the other modules in the chain still return.
Search for administrative boundaries (local authorities, wards, parishes) by name. Returns boundary metadata. Available to every account.
Authorization: Api-Key {your_key} header required (every account)curl "https://api.homedata.co.uk/boundaries/autocomplete/?q=islington" \
-H "Authorization: Api-Key YOUR_API_KEY"
import requests
response = requests.get(
"https://api.homedata.co.uk/boundaries/autocomplete/",
params={"q": "islington"},
headers={"Authorization": "Api-Key YOUR_API_KEY"}
)
data = response.json()
for b in data["results"]:
print(f"{b['name']} (level {b['admin_level']})")
const response = await fetch( "https://api.homedata.co.uk/boundaries/autocomplete/?q=islington", { headers: { "Authorization": "Api-Key YOUR_API_KEY" } } ); const data = await response.json(); data.results.forEach(b => console.log(`${b.name} (level ${b.admin_level})`) );
View example response
{
"results": [
{
"id": 14445,
"name": "London Borough of Islington",
"display_name": "London Borough of Islington, Greater London, England",
"admin_level": 8
}
]
}
Returns every Land Registry title linked to a property in titles[], ordered by building footprint overlap (largest first, then title number). Each item is a GeoJSON Feature. For flats and new builds, read the whole list: a home can have several titles, including freehold and leasehold titles. The existing top-level geometry and properties still describe the first title, so existing integrations keep working.
/title_boundary/{uprn}/ also returns titles[], with title_no and geometry in each item; its top-level fields still hold the first title. A title without a boundary remains in the list with geometry: null.
Property add-ons on /properties/{uprn}/ and /property/{uprn}/?with=lr_title,title_boundary,lr_title_full use the same ordered list inside each title add-on. lr_title includes title metadata; title_boundary includes boundaries; lr_title_full includes metadata, ownership and geometry. Each add-on keeps its existing fields populated from its first title.
To list titles by property, use /property_lr_titles/?property={uprn}. This list keeps its paginated GeoJSON features response, ordered by footprint overlap; follow pagination to read every linked title.
curl https://api.homedata.co.uk/property_lr_titles/100023336956/ \ -H "Authorization: Api-Key YOUR_API_KEY"
import requests
response = requests.get(
"https://api.homedata.co.uk/property_lr_titles/100023336956/",
headers={"Authorization": "Api-Key YOUR_API_KEY"}
)
data = response.json()
for title in data["titles"]:
print(title["properties"]["title_no"], title["geometry"])
const response = await fetch( "https://api.homedata.co.uk/property_lr_titles/100023336956/", { headers: { "Authorization": "Api-Key YOUR_API_KEY" } } ); const data = await response.json(); for (const title of data.titles) { console.log(title.properties.title_no, title.geometry); }
View example response
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[-0.12764, 51.50332],
[-0.12758, 51.50338],
[-0.12771, 51.50341],
[-0.12777, 51.50335],
[-0.12764, 51.50332]
]
]
},
"properties": {
"url": "https://api.homedata.co.uk/property_lr_titles/48213907/",
"title_no": "NGL936618",
"property": 100023336956,
"record_status": "A",
"estate_interest_code": "F",
"estate_interest": "Freehold",
"title_class_code": "ABS",
"title_class": "Absolute",
"has_restrictive_covenant": false,
"ownership": null,
"created_at": "2024-06-15T10:30:00Z",
"updated_at": "2024-06-15T10:30:00Z"
},
"titles": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[-0.12764, 51.50332],
[-0.12758, 51.50338],
[-0.12771, 51.50341],
[-0.12777, 51.50335],
[-0.12764, 51.50332]
]
]
},
"properties": {
"url": "https://api.homedata.co.uk/property_lr_titles/48213907/",
"title_no": "NGL936618",
"property": 100023336956,
"record_status": "A",
"estate_interest_code": "F",
"estate_interest": "Freehold",
"title_class_code": "ABS",
"title_class": "Absolute",
"has_restrictive_covenant": false,
"ownership": null,
"created_at": "2024-06-15T10:30:00Z",
"updated_at": "2024-06-15T10:30:00Z"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[-0.12764, 51.50332],
[-0.1276, 51.50336],
[-0.12769, 51.50338],
[-0.12764, 51.50332]
]
]
},
"properties": {
"url": "https://api.homedata.co.uk/property_lr_titles/48213912/",
"title_no": "NGL936619",
"property": 100023336956,
"record_status": "A",
"estate_interest_code": "L",
"estate_interest": "Leasehold",
"title_class_code": "ABS",
"title_class": "Absolute",
"has_restrictive_covenant": false,
"ownership": null,
"created_at": "2024-06-15T10:30:00Z",
"updated_at": "2024-06-15T10:30:00Z"
}
}
]
}
Light shape (3 tokens): returns band, billing authority name and GSS code. For yearly + monthly charge in GBP and the full bundle, call /council_tax/{uprn}/ instead (5 tokens). Coverage: England, Scotland and Wales, fiscal year 2026-27.
Look up the council tax band for any UK residential property. Light response: band, billing authority name and GSS code. For yearly + monthly charge in GBP and the full bundle, call /council_tax/{uprn}/ (5 tokens). Charges come from each local authority's published rates.
{uprn} in the path (recommended), or by ?postcode= + building_number/building_name when no UPRN.curl "https://api.homedata.co.uk/council_tax_band/10012963615/" \ -H "Authorization: Api-Key YOUR_API_KEY" # Or by postcode + building when no UPRN: curl "https://api.homedata.co.uk/council_tax_band/?postcode=SW1A+2AA&building_number=10" \ -H "Authorization: Api-Key YOUR_API_KEY"
import requests uprn = 10012963615 response = requests.get( f"https://api.homedata.co.uk/council_tax_band/{uprn}/", headers={"Authorization": "Api-Key YOUR_API_KEY"} ) data = response.json() print(f"Council Tax Band: {data['council_tax_band']}")
const uprn = 10012963615; const response = await fetch( `https://api.homedata.co.uk/council_tax_band/${uprn}/`, { headers: { "Authorization": "Api-Key YOUR_API_KEY" } } ); const data = await response.json(); console.log(`Council Tax Band: ${data.council_tax_band}`);
View example response
{
"council_tax_band": "H",
"authority_name": "Westminster"
}
Full bundle (5 tokens): band, billing authority + GSS code, country, current-year yearly and monthly charge in GBP, 1991 valuation band bounds, and fiscal year. For just band + authority, use the lighter /council_tax_band/ (3 tokens).
Full council tax record for a UPRN. Returns the band, billing authority name and GSS code, country, the current-year yearly + monthly charge in GBP (2dp strings), 1991 valuation band bounds, and the fiscal year label. Coverage: 25.3M properties resolve to a 2026-27 charge across England, Scotland and Wales (~96.4% of bands have charge data; the remainder return null charges with band + authority still populated).
{uprn} in the path (UPRN required, postcode mode lives on the lighter /council_tax_band/ endpoint).curl "https://api.homedata.co.uk/council_tax/10012963615/" \ -H "Authorization: Api-Key YOUR_API_KEY"
import requests uprn = 10012963615 response = requests.get( f"https://api.homedata.co.uk/council_tax/{uprn}/", headers={"Authorization": "Api-Key YOUR_API_KEY"} ) data = response.json() print(f"Band {data['council_tax_band']} in {data['authority_name']}: £{data['yearly_charge_gbp']}/yr")
const uprn = 10012963615; const response = await fetch( `https://api.homedata.co.uk/council_tax/${uprn}/`, { headers: { "Authorization": "Api-Key YOUR_API_KEY" } } ); const data = await response.json(); console.log(`Band ${data.council_tax_band}: £${data.yearly_charge_gbp}/yr`);
View example response
{
"council_tax_band": "D",
"authority_name": "Cornwall",
"gss_code": "E06000052",
"country": "England",
"yearly_charge_gbp": "2389.56",
"monthly_charge_gbp": "199.13",
"value_low_gbp": "68001.00",
"value_high_gbp": "88000.00",
"fiscal_year_label": "2026-27"
}
Search planning applications within a radius of a UK postcode or a point. Returns every application inside the circle, with its status, type, decision and dates, how far it is from the centre and where it sits on the map. Our own records hold more than 13 million applications.
Answered from our own records. In the councils we cover, the search is answered from Homedata's own planning records and the response carries "source": "homedata_local_snapshot". A postcode search covers everything within radius_km of that postcode's centre, not only applications typed with that exact postcode.
An empty results list means no applications were found in the radius for those filters. Searches in other councils are answered in the same shape, without the source field.
Parameters
postcode UK postcode (e.g., SW1A 2AA). The search is centred on it. Either postcode or lat+lng required.lat, lng Coordinates for centre of search (alternative to postcode).radius_km Search radius in km around the centre. Default: 0.5, max: 5.days Only applications from last N days. Default: 90, max: 365.type Filter: Full, Outline, Heritage, Trees, Conditions.status Filter: Undecided, Approved, Refused, Withdrawn.page Page number (25 results per page).Response fields
count Applications found in the radius for your filters, across all pages. 0 with an empty results list means none were found.radius_km The radius the search used.source homedata_local_snapshot when the search was answered from our own records. Absent otherwise.distance_km On each result: how far the application is from the centre of the search, in km.location On each result: where the application is, as longitude and latitude. Placed at the site itself where the council published it, otherwise at its postcode's centre. Both fields are null for an application we cannot place.curl "https://api.homedata.co.uk/planning/search/?postcode=SW1A+2AA&radius_km=0.5&days=90" \ -H "Authorization: Api-Key YOUR_API_KEY"
import requests
response = requests.get(
"https://api.homedata.co.uk/planning/search/",
params={"postcode": "SW1A 2AA", "radius_km": 0.5, "days": 90},
headers={"Authorization": "Api-Key YOUR_API_KEY"}
)
data = response.json()
for app in data["results"]:
print(f"{app['reference']}: {app['description'][:80]}...")
const response = await fetch(
"https://api.homedata.co.uk/planning/search/?postcode=SW1A+2AA&radius_km=0.5&days=90",
{ headers: { "Authorization": "Api-Key YOUR_API_KEY" } }
);
const data = await response.json();
console.log(`${data.count} planning applications found`);
data.results.forEach(app => console.log(`${app.reference}: ${app.status}`));
Example response
{
"count": 27,
"page": 1,
"per_page": 25,
"radius_km": 0.5,
"days": 90,
"results": [
{
"reference": "26/01372/LBC",
"address": "10 Downing Street London SW1A 2AA",
"postcode": "SW1A 2AA",
"description": "Removal and replacement of internal doors...",
"status": "Pending Consideration",
"decision": null,
"type": "Listed Building Consent",
"size": null,
"authority": "London Borough of Westminster",
"ward": null,
"date_received": "2026-10-02",
"date_validated": "2026-10-06",
"date_decided": null,
"target_decision_date": null,
"consultation_end_date": null,
"case_officer": null,
"agent_name": null,
"agent_address": null,
"applicant_name": null,
"num_comments": null,
"num_documents": null,
"url": "https://idoxpa.westminster.gov.uk/...",
"comment_url": null,
"map_url": null,
"location": {
"longitude": -0.12767,
"latitude": 51.503541
},
"distance_km": 0.0,
"last_updated": "2026-10-29T06:12:44+00:00"
},
{
"reference": "26/06814/FULL",
"address": "Richmond House 79 Whitehall London SW1A 2NS",
"postcode": "SW1A 2NS",
"description": "Installation of replacement windows to the rear elevation...",
"status": "Pending Consideration",
"decision": null,
"type": "Full Planning Permission",
"size": null,
"authority": "London Borough of Westminster",
"ward": null,
"date_received": "2026-09-18",
"date_validated": "2026-09-22",
"date_decided": null,
"target_decision_date": null,
"consultation_end_date": null,
"case_officer": null,
"agent_name": null,
"agent_address": null,
"applicant_name": null,
"num_comments": null,
"num_documents": null,
"url": "https://idoxpa.westminster.gov.uk/...",
"comment_url": null,
"map_url": null,
"location": {
"longitude": -0.126118,
"latitude": 51.502466
},
"distance_km": 0.161,
"last_updated": "2026-10-28T06:09:51+00:00"
}
],
"source": "homedata_local_snapshot"
}
When the planning source is busy
Searches answered from our own records are never busy. For other councils the planning source can be busy for a short while. The API then answers 429 Too Many Requests with a Retry-After header and the same wait in seconds as retry_after in the body. A busy answer is free: it uses no tokens. Wait that many seconds and send the same search again. Once a search has succeeded, the same search is answered from cache for an hour.
Retry-After: 60
{
"error": "Planning data source is rate-limited to respect the upstream provider. Please retry shortly.",
"retry_after": 60
}
Search street-level crime near a UK postcode or coordinate. Returns crime counts aggregated by category (burglary, theft, violent crime, etc). Data sourced from Police UK covering England, Wales, and Northern Ireland. Updated monthly.
postcode (e.g. SW1A 2AA) or
lat + lng (required),
date (YYYY-MM, optional: defaults to latest),
category (optional: filter by crime type)
curl "https://api.homedata.co.uk/crime/?postcode=SW1A+2AA" \ -H "Authorization: Api-Key YOUR_API_KEY" # With date and category filter curl "https://api.homedata.co.uk/crime/?lat=51.5074&lng=-0.1278&date=2025-12&category=burglary" \ -H "Authorization: Api-Key YOUR_API_KEY"
import requests
response = requests.get(
"https://api.homedata.co.uk/crime/",
params={"postcode": "SW1A 2AA"},
headers={"Authorization": "Api-Key YOUR_API_KEY"}
)
data = response.json()
print(f"Total crimes: {data['total_crimes']} in {data['month']}")
for cat in data["categories"]:
print(f" {cat['label']}: {cat['count']}")
const response = await fetch( "https://api.homedata.co.uk/crime/?postcode=SW1A+2AA", { headers: { "Authorization": "Api-Key YOUR_API_KEY" } } ); const data = await response.json(); console.log(`${data.total_crimes} crimes in ${data.month}`); data.categories.forEach(c => console.log(` ${c.label}: ${c.count}`) );
View example response
{
"total_crimes": 847,
"month": "2025-12",
"search_point": {
"latitude": 51.50354,
"longitude": -0.127695
},
"categories": [
{
"category": "violent-crime",
"label": "Violence and sexual offences",
"count": 203,
"latest_month": "2025-12",
"sample_locations": [
{
"street": "On or near Whitehall",
"latitude": "51.504284",
"longitude": "-0.126366"
}
]
},
{
"category": "anti-social-behaviour",
"label": "Anti-social behaviour",
"count": 156,
"latest_month": "2025-12",
"sample_locations": []
},
{
"category": "theft-from-the-person",
"label": "Theft from the person",
"count": 112,
"latest_month": "2025-12",
"sample_locations": []
},
{
"category": "other-theft",
"label": "Other theft",
"count": 98,
"latest_month": "2025-12",
"sample_locations": []
},
{
"category": "burglary",
"label": "Burglary",
"count": 45,
"latest_month": "2025-12",
"sample_locations": []
}
],
"coverage_note": "England, Wales, and Northern Ireland. Scotland shows British Transport Police data only."
}
Categories: anti-social-behaviour, bicycle-theft, burglary, criminal-damage-arson, drugs, other-crime, other-theft, possession-of-weapons, public-order, robbery, shoplifting, theft-from-the-person, vehicle-crime, violent-crime. Use the category parameter to filter by type, or omit to get all.
Full solar energy potential assessment for a UK property. Returns estimated system size, annual generation, savings, payback period, and CO₂ offset. Based on property roof data, local solar irradiance (120 UK postcode zones), and standard PV calculation models. Designed for solar installers, energy advisors, and home improvement platforms.
uprn (path, required)curl https://api.homedata.co.uk/solar-assessment/10003469503/ \ -H "Authorization: Api-Key YOUR_API_KEY"
import requests UPRN = 10003469503 response = requests.get( f"https://api.homedata.co.uk/solar-assessment/{UPRN}/", headers={"Authorization": "Api-Key YOUR_API_KEY"} ) data = response.json() solar = data["solar_potential"] print(f"{solar['system_size_kwp']} kWp: £{solar['annual_savings_gbp']}/yr savings") print(f"Payback: {solar['payback_years']} years ({solar['co2_saved_kg_per_year']} kg CO₂/yr)")
const uprn = 10003469503; const response = await fetch( `https://api.homedata.co.uk/solar-assessment/${uprn}/`, { headers: { "Authorization": "Api-Key YOUR_API_KEY" } } ); const { solar_potential: s } = await response.json(); console.log(`${s.system_size_kwp} kWp: £${s.annual_savings_gbp}/yr, payback ${s.payback_years}y`);
View example response
{
"property": {
"uprn": 10003469503,
"address": "14 Acacia Avenue, Manchester, M20 4LN",
"property_type": "Semi-Detached",
"bedrooms": 3,
"construction_age_band": "1950-1966"
},
"roof": {
"estimated_area_m2": 72.0,
"usable_area_m2": 32.4,
"material": "Pitched, Slate",
"has_existing_solar": false,
"pitch_estimate_degrees": 35,
"orientation_estimate": "mixed"
},
"solar_potential": {
"max_panels": 18,
"system_size_kwp": 7.56,
"annual_generation_kwh": 4787,
"annual_savings_gbp": 673,
"annual_export_income_gbp": 145,
"system_cost_estimate_gbp": 13230,
"payback_years": 19.7,
"co2_saved_kg_per_year": 694,
"trees_equivalent_per_year": 33,
"local_irradiance_kwh_m2": 990
},
"with_battery": {
"annual_savings_gbp": 991,
"self_consumption_pct": 80,
"battery_cost_gbp": 4200,
"total_system_cost_gbp": 17430,
"payback_years": 17.6
},
"assumptions": {
"panel_wattage_w": 420,
"electricity_rate_p_kwh": 24.5,
"export_rate_p_kwh": 5.5,
"system_efficiency": 0.82
},
"data_quality": {
"confidence": "medium",
"missing_for_higher_confidence": ["os_footprint_area", "lidar_roof_pitch"],
"note": "Orientation assumed mixed, south-facing adds ~10-15% to estimates."
}
}
Use case: Solar installers, energy advisors, boiler-to-heat-pump assessors, home improvement tools. The has_existing_solar flag in the response (from our property database) indicates if solar panels are already present: useful for targeting leads.
Places around a property from OpenStreetMap, nearest first. Call /amenities/ for every group in one response,
or a single group: food, education, healthcare,
financial, civic, worship,
culture, convenience, green_spaces,
transport, shops.
Billing: 5 tokens for /amenities/, 1 token for a single group. Every account reaches this endpoint.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| uprn | string | one of* | UPRN the search is anchored on, e.g. 100023336956. The response echoes the resolved latitude and longitude. |
| radius_km | float | optional | Search radius in kilometres. Defaults to 8, maximum 25. The response echoes the value used. |
| lat + lng | float | one of* | WGS84 coordinates to search from, instead of a UPRN. |
| limit | integer | optional | Maximum results. Defaults to 50, maximum 200. |
| include_tags | boolean | optional | Include the full source tags for each result. Defaults to false. |
* Anchor the search with either uprn or lat + lng.
curl "https://api.homedata.co.uk/amenities/food/?uprn=100023336956&radius_km=0.5" \ -H "Authorization: Api-Key YOUR_API_KEY"
{
"uprn": 100023336956,
"group": "food",
"latitude": 51.503541,
"longitude": -0.12767,
"radius_km": 0.5,
"total": 6,
"type_counts": {
"cafe": 2,
"ice_cream": 1,
"restaurant": 1,
"pub": 1,
"fast_food": 1
},
"results": [
{
"osm_id": 11554148969,
"osm_type": "node",
"name": "The Drawing Room",
"type": "cafe",
"latitude": 51.505166,
"longitude": -0.126117,
"distance_m": 338
},
{
"osm_id": 12757688483,
"osm_type": "node",
"name": "The Royal Parks Ice-cream",
"type": "ice_cream",
"latitude": 51.502204,
"longitude": -0.130137,
"distance_m": 364
},
{
"osm_id": 13037832782,
"osm_type": "node",
"name": null,
"type": "cafe",
"latitude": 51.501456,
"longitude": -0.130079,
"distance_m": 459
}
]
}
Reading the response: a wide radius in a dense area returns the nearest results rather than everything:
the capture behind the example above came back with 50 for an 8 km search. type_counts totals each OSM tag in the result set, and every
result carries its distance_m from the property along with its OSM id and coordinates. name is
null for features OpenStreetMap has not named.
GPs, dentists and hospitals from the Care Quality Commission register, plus pharmacies from OpenStreetMap, nearest first. Call
/healthcare/ for all four, or one kind: gps,
dentists, pharmacies, hospitals.
Billing: 3 tokens for /healthcare/, 1 token for a single kind. Every account reaches this endpoint.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| uprn | string | one of* | UPRN the search is anchored on, e.g. 100023336956. The resolved coordinates come back as query.lat and query.lng. |
| radius_km | float | optional | Search radius in kilometres. Defaults to 1, maximum 10. The response echoes the radius and the limit it applied under query. |
| lat + lng | float | one of* | WGS84 coordinates to search from, instead of a UPRN. |
| limit | integer | optional | Maximum results per type. Defaults to 10, maximum 50. |
* Anchor the search with either uprn or lat + lng.
curl "https://api.homedata.co.uk/healthcare/gps/?uprn=100023336956&radius_km=2" \ -H "Authorization: Api-Key YOUR_API_KEY"
{
"query": {
"lat": 51.503541,
"lng": -0.12767,
"radius_km": 2.0,
"uprn": "100023336956",
"kind": "gps",
"types": [
"gp"
],
"limit": 10
},
"count": 10,
"type_counts": {
"gp": 10
},
"facilities": [
{
"id": "1-12687408410",
"source": "cqc",
"name": "Medloop",
"type": "gp",
"service_types": [
"Doctors/GPs",
"Doctors/GPs"
],
"address": {
"full_address": "24 Old Queen Street,London",
"postcode": "SW1H 9HP",
"local_authority": "Westminster",
"region": "London"
},
"location": {
"lat": 51.501,
"lng": -0.130912
},
"contact": {
"telephone": "3308180062",
"website": null
},
"cqc_url": "https://www.cqc.org.uk/location/1-12687408410",
"last_checked": "2024-01-22",
"distance_km": 0.36,
"distance_miles": 0.22
},
{
"id": "1-10241877096",
"source": "cqc",
"name": "Embankment Place Primary Healthcare",
"type": "gp",
"service_types": [
"Doctors/GPs",
"Doctors/GPs"
],
"address": {
"full_address": "1 Embankment Place,,London",
"postcode": "WC2N 6RH",
"local_authority": "Westminster",
"region": "London"
},
"location": {
"lat": 51.507987,
"lng": -0.124688
},
"contact": {
"telephone": null,
"website": null
},
"cqc_url": "https://www.cqc.org.uk/location/1-10241877096",
"last_checked": "2023-08-18",
"distance_km": 0.54,
"distance_miles": 0.33
}
],
"metadata": {
"sources": {
"gp": "Care Quality Commission",
"dentist": "Care Quality Commission",
"hospital": "Care Quality Commission",
"pharmacy": "OpenStreetMap (shop=pharmacy)"
},
"coverage": "United Kingdom",
"note": "Distance is straight-line (as the crow flies). CQC refreshes monthly; OSM via planet-osm extract. CQC doesn't regulate pharmacies \u2014 those come from OSM."
}
}
Reading the response: CQC entries carry the register id, address, telephone and a
cqc_url you can link to, plus last_checked: the date of the inspection CQC holds, not the date we
refreshed. Distances come as both distance_km and distance_miles, straight-line from the property.
The CQC register is refreshed monthly; pharmacies come from OpenStreetMap because CQC does not regulate them.
Petrol stations and EV charge points from OpenStreetMap, nearest first. Call /fuel-stations/ for both: each result is
tagged with its type, or one kind: petrol, ev.
Billing: 1 token. Every account reaches this endpoint.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| uprn | string | one of* | UPRN the search is anchored on, e.g. 100023336956. The response echoes the resolved latitude and longitude. |
| radius_km | float | optional | Search radius in kilometres. Defaults to 8, maximum 25. The response echoes the value used. |
| lat + lng | float | one of* | WGS84 coordinates to search from, instead of a UPRN. |
| limit | integer | optional | Maximum results. Defaults to 50, maximum 200. |
| include_tags | boolean | optional | Include the full source tags for each result. Defaults to false. |
* Anchor the search with either uprn or lat + lng.
curl "https://api.homedata.co.uk/fuel-stations/?uprn=100023336956&radius_km=2" \ -H "Authorization: Api-Key YOUR_API_KEY"
{
"uprn": 100023336956,
"kind": "all",
"latitude": 51.503541,
"longitude": -0.12767,
"radius_km": 2.0,
"total": 6,
"type_counts": {
"electric": 6
},
"results": [
{
"osm_id": 5584773894,
"osm_type": "node",
"name": "Source London",
"type": "electric",
"latitude": 51.506454,
"longitude": -0.123758,
"distance_m": 679
},
{
"osm_id": 12869392044,
"osm_type": "node",
"name": "Source London",
"type": "electric",
"latitude": 51.510999,
"longitude": -0.122007,
"distance_m": 1475
},
{
"osm_id": 5262372193,
"osm_type": "node",
"name": null,
"type": "electric",
"latitude": 51.494884,
"longitude": -0.12837,
"distance_m": 1550
}
]
}
Index of Multiple Deprivation (IMD) for any postcode in England: the overall rank/decile plus the seven underlying domains: income, employment, education, health, crime, barriers to housing & services, and living environment, and the two income-deprivation indices, for children (IDACI) and older people (IDAOPI). Sourced from MHCLG IMD open data.
Billing: 1 token per request. Auth: API key required.
curl "https://api.homedata.co.uk/deprivation/?postcode=SW1A+2AA" \ -H "Authorization: Api-Key YOUR_KEY"
Find schools near any property or postcode in England, with phase, age range, pupil numbers, type, and straight-line distance. Data sourced directly from the DfE's GIAS register (~27,000 open schools in England).
Ofsted data now included. The ofsted object returns rating (Outstanding, Good, Requires Improvement, Inadequate) and last_inspection date. Filter results with ?ofsted=Good. Coverage: ~27,000 schools with inspections.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| postcode | string | one of* | UK postcode. e.g. SW1A 2AA |
| lat + lng | float | one of* | WGS84 decimal coordinates. e.g. lat=51.5074&lng=-0.1278 |
| uprn | string | one of* | Anchor on a property instead: the search runs from that UPRN's coordinates. |
| radius_km | float | optional | Search radius in kilometres. Default: 3, maximum 10. |
| limit | int | optional | Max results to return. Range: 1–50. Default: 20 |
| phase | string | optional | Filter by school phase: Primary, Secondary, All-through, Nursery, 16 plus |
| ofsted | string | optional | Filter by Ofsted rating: Outstanding, Good, Requires Improvement, Inadequate |
* Anchor the search with one of postcode, lat + lng, or uprn. Send one anchor, not several. The official SDK clients do not expose uprn for this endpoint yet: call it directly if you need it.
curl "https://api.homedata.co.uk/schools/nearby?postcode=SW1A+2AA&radius_km=1&phase=Primary" \ -H "Authorization: Api-Key YOUR_API_KEY"
import requests
response = requests.get(
"https://api.homedata.co.uk/schools/nearby",
headers={"Authorization": "Api-Key YOUR_API_KEY"},
params={
"postcode": "SW1A 2AA",
"radius_km": 1,
"phase": "Primary",
}
)
data = response.json()
for school in data["schools"]:
ofsted = school["ofsted"]
rating = ofsted["rating"] if ofsted else "Not yet inspected"
print(f"{school['name']}: {rating} ({school['distance_km']}km)")
const params = new URLSearchParams({
postcode: 'SW1A 2AA',
radius_km: '1',
phase: 'Primary',
});
const response = await fetch(
`https://api.homedata.co.uk/schools/nearby?${params}`,
{ headers: { 'Authorization': 'Api-Key YOUR_API_KEY' } }
);
const data = await response.json();
data.schools.forEach(s =>
console.log(`${s.name}: ${s.ofsted?.rating ?? 'Not yet inspected'} (${s.distance_km}km)`)
);
View example response
{
"query": {
"lat": 51.5034,
"lng": -0.1276,
"radius_km": 1,
"postcode": "SW1A 2AA",
"phase": "Primary",
"ofsted": null
},
"count": 3,
"total_in_radius": 3,
"ofsted_summary": {},
"schools": [
{
"urn": 100024,
"name": "Westminster Cathedral Catholic Primary School",
"type": "Voluntary aided school",
"type_group": "Faith schools",
"phase": "Primary",
"gender": "Mixed",
"religious_character": "Roman Catholic",
"admissions_policy": "Selective",
"age_range": "3-11",
"number_of_pupils": 453,
"school_capacity": 480,
"percentage_fsm": 14.2,
"ofsted": null,
"address": {
"street": "Ambrosden Avenue",
"locality": null,
"town": "London",
"county": "Westminster",
"postcode": "SW1P 1QH"
},
"local_authority": "Westminster",
"contact": {
"website": "https://www.wccs.org.uk",
"telephone": "020 7798 9020",
"head_teacher": "Mrs Jane Smith"
},
"trust": null,
"location": {
"lat": 51.4968,
"lng": -0.1354
},
"distance_km": 0.34,
"distance_miles": 0.21
}
],
"metadata": {
"source": "GIAS, DfE Get Information About Schools",
"coverage": "England (~27,000 open schools)",
"note": "Distance is straight-line (as the crow flies)"
}
}
Error responses
| Status | Code | Meaning |
|---|---|---|
| 400 | – | Missing or unusable location: provide one of postcode, lat+lng, or uprn. This endpoint returns error as a plain message rather than a code object |
| 402 | insufficient_tokens | Not enough tokens for this request. Top up, then retry |
| 403 | INVALID_API_KEY | Key missing, wrong, revoked or unknown. A request with no key lands here too, not on 401 |
| 404 | RESOURCE_NOT_FOUND | School not found for the given URN (applies to /schools/{urn}) |
Use cases: School catchment analysis for estate agent listings, property valuation tools (proximity to top-rated schools is a significant price signal), family home search platforms, and conveyancing due diligence. Pair with Property Data for a complete area profile.
Capital-appreciation data for an outcode: annual growth rate, 1Y/3Y/5Y/10Y returns, and historical price index. Computed from Land Registry sold-price transactions.
Parameters
| Name | Type | Required | Validation | Example |
|---|---|---|---|---|
outcode |
string (path) | yes | UK outcode | SW1A |
View example response
{
"outcode": "SW1A",
"current_period": {
"from": "2025-06-17",
"to": "2025-08-12",
"median_sold_price": 2000000,
"sale_count": 3
},
"prior_period": {
"from": "2024-05-13",
"to": "2025-04-14",
"median_sold_price": 1400000,
"sale_count": 14
},
"yoy_change_pct": 42.86,
"yoy_change_gbp": 600000,
"trend": "rising",
"data_quality": "low"
}
ONS Census 2021 demographics for a postcode. The postcode is resolved to its MSOA (middle-layer super output area, ~7,500 residents), so the response describes that neighbourhood rather than the individual postcode unit.
Returns population counts, tenure breakdown (owned outright / owned with mortgage / social rented / private rented / rent-free), age bands, ethnicity, occupation, household size, and car ownership, plus MHCLG IMD 2019 deprivation when available. Includes the LSOA, MSOA and local authority codes so you can join to other ONS datasets.
Parameters
| Name | Type | Required | Validation | Example |
|---|---|---|---|---|
postcode |
string | yes | UK postcode | SW1A 2AA |
View example response
{
"postcode": "SW1A 2AA",
"area": {
"local_authority": null,
"local_authority_code": null,
"lsoa_code": null,
"lsoa_name": null,
"msoa_code": "E02000977"
},
"geography": {
"latitude": 51.503538,
"longitude": -0.127695,
"msoa_code": "E02000977",
"lsoa_code": null
},
"population": { "total": 6387, "households": 3432 },
"tenure": {
"owned_outright_pct": 16.35,
"owned_mortgage_pct": 7.4,
"shared_ownership_pct": 0.38,
"social_rented_pct": 23.92,
"private_rented_pct": 51.89,
"rent_free_pct": 0.06
},
"age": {
"age_0_15_pct": 7.34,
"age_16_29_pct": 29.53,
"age_30_44_pct": 24.58,
"age_45_64_pct": 25.5,
"age_65_plus_pct": 13.04
},
"ethnicity": {
"white_british_pct": 33.72,
"white_other_pct": 25.99,
"mixed_pct": 6.29,
"asian_pct": 22.2,
"black_pct": 4.02,
"other_pct": 7.12
},
"occupation": {
"managerial_professional_pct": 45.4,
"intermediate_pct": 17.66,
"routine_manual_pct": 12.81,
"never_worked_pct": 8.79
},
"household_size": { "one_person_pct": 56.53, "two_person_pct": 28.79, "three_plus_pct": 14.77 },
"car_ownership": { "no_car_pct": 75.92, "one_car_pct": 18.84, "two_plus_cars_pct": 5.24 },
"census_year": 2021,
"deprivation": null,
"data_sources": ["ONS Census 2021", "MHCLG IMD 2019"]
}
Historic England listed buildings within a radius of a UK postcode. Returns Grade I, II*, and II entries with name, location, list date, and a link back to the official Historic England record. Useful for planning, due-diligence, conveyancing, and heritage flows.
Authorization: Api-Key YOUR_KEYParameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| postcode | string | required | UK postcode. Spaces optional. |
| radius_km | number | optional | Search radius in km. Default 1, max 5. |
| grade | enum | optional | Filter to a single grade: I · II* · II. |
| limit | integer | optional | Maximum results. Defaults to 50, maximum 100. |
Response shape
{
"postcode": "SW1A 2AA",
"radius_km": 1.0,
"total": 50,
"grade_counts": { "I": 103, "II*": 149, "II": 574 },
"results": [
{
"list_entry": 1356989,
"name": "11, DOWNING STREET SW1",
"grade": "I",
"latitude": 51.5034, "longitude": -0.1278,
"date_listed": "1970-01-14",
"hyperlink": "https://historicengland.org.uk/listing/the-list/list-entry/1356989",
"distance_km": 0.019
}
]
}
curl "https://api.homedata.co.uk/listed-buildings/?postcode=SW1A2AA&grade=I" \
-H "Authorization: Api-Key YOUR_API_KEY"
A market rent per calendar month for one home, with a likely range, a confidence tier and the comparable rentals behind it, nearest first. Comparables are rentals with the same bedrooms and type, advertised in the 180 days up to the appraisal date. The search starts at 4 miles and doubles, to 8, 16 and 32 miles, until it has 100 different comparables. Each rent is weighted by one over its distance squared, with distance counted as at least a quarter of a mile so that next door does not outweigh the rest.
Authorization: Api-Key YOUR_KEYContent-Type: application/jsonThe home: send one of three
| Field | Type | Description |
|---|---|---|
| uprn | integer | The home's UPRN. Bedrooms and type come from our record when it has them. |
| address_line_1 + postcode | string | Matched to one home, then appraised as its UPRN. full_address is optional and helps with flats. A postcode alone is not enough. |
| lat + lng | number | A point (WGS84), when there is no address. Send beds and type with it. |
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| beds | integer | when our record has none | Bedrooms, 0 to 20. 0 is a studio. Always needed with lat and lng; not needed for a room. Overrides our record. |
| type | string | when our record has none | flat, house or room. detached, semi-detached, terraced, bungalow and maisonette are accepted too. A house matches every kind of house, never a flat. Overrides our record. |
| as_of | date | optional | The appraisal date, YYYY-MM-DD, from 2025-01-01 to today. Default today. The appraisal reads the market as it was on that date, so you can check past rents. |
| limit | integer | optional | How many comparables to return, 0 to 100. Default 100. The estimate always uses up to 100, whatever you ask to see. |
curl -X POST "https://api.homedata.co.uk/rental-appraisal/" \ -H "Authorization: Api-Key YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"uprn": 999999000101, "as_of": "2026-10-22"}'
import requests
response = requests.post(
"https://api.homedata.co.uk/rental-appraisal/",
json={"lat": 51.4645, "lng": -0.163, "beds": 2, "type": "flat"},
headers={"Authorization": "Api-Key YOUR_API_KEY"},
)
data = response.json()
print(f"{data['estimate_pcm']} pcm, likely {data['low_pcm']} to {data['high_pcm']}")
View example response (example values, synthetic UPRN, 2 of 100 comparables shown)
{
"subject": {
"uprn": 999999000101, "lat": 51.4645, "lng": -0.163, "postcode": "SW11 1AA",
"beds": 2, "beds_source": "property_record",
"type": "flat", "type_source": "property_record",
"address_resolution": null
},
"estimate_pcm": 2180,
"low_pcm": 2000,
"high_pcm": 2770,
"confidence": "high",
"comparables_used": 100,
"radius_mi": 4,
"distance_breakdown": {"within_0_25mi": 9, "within_0_5mi": 27, "within_1mi": 61,
"within_2mi": 100, "beyond_2mi": 0},
"sd_within_0_25mi": 240,
"as_of": "2026-10-22",
"data_as_of": "2026-10-21",
"method": "rmas-v1",
"calibration_version": "rmas-v1-2026-10-22-window2",
"comparables": [
{"listing_id": "5b0e2c1a-…", "rent_pcm": 2150, "postcode": "SW11 1AB", "type": "flat", "beds": 2,
"distance_mi": 0.04, "first_seen": "2026-09-30", "last_seen": "2026-10-19",
"thumbnail_url": "https://image-cache-1.home.co.uk/listings/…/images/….jpg", "weight": 0.0213},
{"listing_id": "9d41f7e3-…", "rent_pcm": 2250, "postcode": "SW11 1AD", "type": "flat", "beds": 2,
"distance_mi": 0.11, "first_seen": "2026-08-14", "last_seen": null,
"thumbnail_url": null, "weight": 0.0213}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
| subject | object | The home appraised: uprn, lat, lng, postcode, beds and type, with beds_source and type_source saying whether each came from your request or our record. address_resolution shows how an address was matched, or null. |
| estimate_pcm | integer | The estimate: market rent per calendar month, in pounds. |
| low_pcm, high_pcm | integer | The likely range. About 80% of real asking rents for homes like this fall inside it. |
| confidence | string | high, medium or low: how often estimates like this one land within 15% of the asking rent (70% or more, 40% or more, under 40%). Set by how closely the nearest comparables agree, how far the search went and how many it found. |
| comparables_used | integer | Different comparables behind the estimate: 100, unless fewer exist within 32 miles. |
| radius_mi | integer | How far the search went: 4, 8, 16 or 32 miles. |
| distance_breakdown | object | Comparables within 0.25, 0.5, 1 and 2 miles (each count includes the nearer ones), and beyond 2 miles. |
| sd_within_0_25mi | integer | null | Standard deviation of the rents within a quarter of a mile; null when there are fewer than 3. |
| as_of | date | The appraisal date. |
| data_as_of | date | The data date: the most recent sighting among the comparables, never later than as_of. |
| method, calibration_version | string | The method (rmas-v1) and the calibration the range and confidence were fitted with. |
| comparables[] | array | Nearest first, up to limit. Each has listing_id, rent_pcm, postcode, type, beds, distance_mi (miles from the home), first_seen and last_seen (when the advert was recorded; either can be null), thumbnail_url (a photo, or null when we hold none) and weight, its share of the estimate. |
Weekly and affordable rent
The response is monthly. Derive the weekly figure and the Affordable Rent the way our rental appraisal report does:
- Weekly rent = monthly rent × 12 ÷ 52. For the example, £2,180 pcm is £503.08 a week.
- Affordable Rent = 80% of the market rent. For the example, £1,744 pcm, or £402.46 a week.
To leave some comparables out and recalculate, take Σ(rent_pcm × weight) ÷ Σ(weight) over the ones you keep. Over all of them it gives estimate_pcm.
Error codes
Every error uses the standard envelope (error.code, message, hint) and is refunded.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_parameter | Something is missing or malformed: error.parameter names it. |
| 400 | invalid_body | The body is not a JSON object. |
| 404 | uprn_not_found | We hold no home with that UPRN. |
| 422 | address_not_matched | The address did not match exactly one home (reason, candidates). |
| 422 | location_unknown | We hold no location for the UPRN. Send lat and lng instead. |
| 422 | insufficient_comparables | Fewer than 5 comparables within 32 miles (comparables_found). Check the bedrooms and type. |
| 500 | rmas_unavailable | The comparables could not be read in time. Retry after the Retry-After seconds. |
| 500 | rmas_index_not_ready | The comparables index is not ready. This call is refunded. Retry after the Retry-After seconds. |
Calculate UK Stamp Duty Land Tax (SDLT) for England and Northern Ireland. Returns total tax, effective rate, and a band-by-band breakdown. Supports standard buyers, first-time buyers (with £300k relief), and additional-dwelling purchases (5% surcharge on the full price).
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| price | integer (£) | required | Property purchase price in GBP. Positive integer. |
| buyer_type | enum | required | standard · first_time · additional (second home / BTL). |
Response shape: first-time buyer, £350k
{
"price": 350000,
"buyer_type": "first_time",
"total_tax": 2500.0,
"effective_rate_pct": 0.714,
"breakdown": [
{ "band": "Up to £300,000 (first-time buyer relief)", "rate_pct": 0.0, "taxable_amount": 300000, "tax": 0.0 },
{ "band": "£300,000 – £500,000 (first-time buyer)", "rate_pct": 5.0, "taxable_amount": 50000, "tax": 2500.0 }
],
"jurisdiction": "England and Northern Ireland",
"thresholds_from": "1 April 2025"
}
curl "https://api.homedata.co.uk/calculators/stamp-duty/?price=350000&buyer_type=first_time" \
-H "Authorization: Api-Key YOUR_API_KEY"
Mortgage repayment calculator: given price, deposit, annual rate and term in years, returns the monthly payment, total repayment, total interest, and the LTV / LTI ratios. Standard amortising-loan formula.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| price | integer (£) | required | Property purchase price in GBP. |
| deposit | integer (£) | required | Up-front deposit, GBP. Non-negative. |
| rate | number (%) | required | Annual interest rate, percent (e.g. 4.5). |
| term | integer | optional | Mortgage term in years, 1–40. Default: 25. Echoed back as term_years. |
Response shape: £350k @ 4.5% / 25y, £35k deposit
{
"loan_amount": 315000,
"deposit": 35000,
"property_price": 350000,
"annual_rate_pct": 4.5,
"term_years": 25,
"monthly_payment": 1750.87,
"total_repayment": 525261.69,
"total_interest": 210261.69,
"ltv_pct": 90.0,
"lti_ratio": 14.99
}
curl "https://api.homedata.co.uk/calculators/mortgage/?price=350000&deposit=35000&rate=4.5&term=25" \
-H "Authorization: Api-Key YOUR_API_KEY"
Try every endpoint with your own key
The developer playground runs live requests against your account and gives you copy-paste curl / Python / Node / PHP snippets for every endpoint above.
Open the Playground →Calling from the browser?
Don't put your API key in client-side JavaScript. Create a public key scoped to the tools and domains you need, then call the relay at /embed/v2/{tool_slug} with the public key in an Authorization: Public-Key header. Or skip the wiring entirely: drop-in widget bundles are ready for the common tools.
Integrate into your own product
Pay as you goAll 40+ production endpoints return UPRN-matched, structured JSON responses with consistent field naming and OGL-licensed source attribution: covering 29M+ UK properties across address, property, EPC, risks, planning, council tax, and market data via a single authenticated API key.
Structured as JSON · queryable by UPRN or postcode · ready to embed in any application
Exact measurements
Real values (distances, concentrations, counts) not rounded ratings
29 million UK properties covered
Every address queryable by UPRN or postcode
REST API
JSON responses and OpenAPI docs: first call in under 5 minutes
Pay as you go: 100 tokens = £1 across every endpoint, no subscription needed. Bonus tokens with an optional monthly subscription, and your first top-up matched 100%. See pricing →
Sources
Further reading
Authentication · Data Dictionary · Error codes · Coverage matrix