Skip to main content
29M UK properties, one API call. Get an API key →

Error handling

Every error arrives under an error key. Most are an object with a stable code, a message and a status, and most of those also say what to try in a hint and link to their own entry on this page in docs. A few responses still use an older shape, or send error as a plain message string. Branch on the HTTP status first, switch on error.code case-insensitively, and check whether error is a string before you read error.code. message and hint are written for people and may be reworded, so never match on them.

Error format

Errors come in three shapes. Since 21 October 2026 almost all are the first; a client still has to survive the other two. The codes differ in case: the platform's are UPPER_SNAKE_CASE (INVALID_API_KEY, RESOURCE_NOT_FOUND) and the endpoints' are lowercase (uprn_not_found), so a client comparing err.code == "BAD_REQUEST" silently misses invalid_parameter. Normalise the case before you compare.

Shape Looks like Where it comes from
Standard object {"error": {"code": "uprn_not_found", "message": "...", "hint": "...", "docs": "...", "status": 404}} Almost every error. Always code, message and status. Every 404, and the address, parameter and access codes below, add hint and docs; key and token-balance refusals do not
Older coded object {"error": {"code": "invalid_uprn", "message": "..."}} Lowercase code with no status, from some endpoints rejecting your parameters
Plain string {"error": "Invalid UPRN or address token."} No code at all. A few 400s still answer this way; a 404 no longer does. Read it as the message
Error Response
{
  "error": {
    "code": "uprn_not_found",
    "message": "We don't hold a property with UPRN 10091234567. Homedata covers residential and commercial addresses in the UK; land parcels and other non-postal UPRNs aren't covered. You weren't charged for this call.",
    "hint": "For one home, use /address/match/?address=<address>&postcode=<postcode>. For every address in a postcode, use /address/postcode/<postcode>/. Use the UPRN returned for the property.",
    "docs": "https://homedata.co.uk/docs/errors#uprn_not_found",
    "status": 404
  }
}
Field Type Description
error object | string Present on every non-2xx response, but not always an object: a few 400s put a plain message string here. Check the type before reading code.
error.code string Machine-readable and stable: switch on this. Platform codes are UPPER_SNAKE_CASE, endpoint codes lowercase, so compare case-insensitively. Each one has an entry under Error codes.
error.message string What went wrong, written for people. A free error's message ends "You weren't charged for this call." May be reworded, so don't match on it.
error.hint string What to try next, such as the route to use instead. Safe to show to a person; may be reworded. Not on every error.
error.docs string A link to the code's entry on this page, https://homedata.co.uk/docs/errors#<code>. Not on every error.
error.status integer Mirrors the HTTP status code, so the body is self-describing if you log it on its own. The older coded object and the plain string lack it, so read the HTTP status itself.

An empty answer is not an error

When a call works but finds nothing, it answers 200 with its usual fields, empty, plus a reason that says why and links to the same entries as an error. An empty 200 is charged at the endpoint's normal rate, so check your input before calling again. Errors are free: see HTTP status codes.

{
  "count": 0,
  "results": [],
  "reason": {
    "code": "no_match",
    "message": "Nothing matched these filters.",
    "hint": "Check the filter values, or widen them.",
    "docs": "https://homedata.co.uk/docs/errors#no_match"
  }
}

HTTP status codes

Only a served response is charged. Any 4xx other than a 402, and any 5xx, is refunded automatically however your key is billed: a charge taken before the failure comes back. On a wallet key X-Tokens-Charged reads 0 on that response; on a quota key the call is returned to your allowance and no token header is sent.

200

OK

Request succeeded. Response body contains the requested data. If it found nothing, it says why in a reason, and it is still charged.

400

Bad request

A parameter is missing or in the wrong format. Where it can, the error names it in error.parameter and the message gives the format we expect. See Bad requests for each code.

{"error": {"parameter": "uprn", "code": "invalid_parameter", "message": "The parameter 'uprn' should be ...; we got 'abc'. You weren't charged for this call.", "hint": "Fix the value and try again. The docs list every parameter and its format.", "docs": "https://homedata.co.uk/docs/errors#invalid_parameter", "status": 400}}
401

Unauthorized

Do not branch on 401 to detect a missing key: a request with no key returns 403, not 401. 401 is rare and means something else: the key was recognised but its server-side configuration is missing. Only the Explore and listing-address endpoints return it, and the code is the lowercase auth_error, not an upper-case constant.

{"error": {"code": "auth_error", "message": "API key configuration missing"}}
402

Insufficient tokens

The request costs more tokens than your balance holds. Both response shapes use insufficient_tokens and say what the request needs (error.required) and what you have (error.available), in tokens. Most endpoints also send error.status and error.topup_url, a link that opens a one-off top-up:

{"error": {"code": "insufficient_tokens", "message": "This call costs 5 tokens and your balance is 3.", "status": 402, "required": 5, "available": 3, "topup_url": "https://homedata.co.uk/subscription?view=billing&pay=one-off&amount=20&from=api"}}

Listings, reveals and listing-address return the smaller shape, with no status or topup_url field:

{"error": {"code": "insufficient_tokens", "message": "Listing address reveal costs 20 tokens and you have 3. Top up at https://homedata.co.uk/subscription?view=billing&pay=one-off&amount=20&from=api to continue.", "required": 20, "available": 3}}

Top up and retry. Nothing else about the request needs changing, and retrying without topping up will not clear it. Where the response carries error.topup_url, send people there rather than to a link you have saved.

403

Forbidden

Every key problem lands here, including sending no key at all, but not every 403 is a key problem. Missing, wrong, revoked or unknown keys all arrive as 403 carrying INVALID_API_KEY. "Authentication credentials were not provided." means no key reached us, so check you are sending the Authorization: Api-Key header before you suspect the key itself. Running out of tokens is not a 403 at all: that is 402 insufficient_tokens.

{"error": {"code": "INVALID_API_KEY", "message": "Authentication credentials were not provided.", "status": 403}}

A widget embed token (it starts wt_) sent in place of an API key gets its own code, widget_token_not_api_key. A widget token only works inside the embed snippet; the response links to the embed docs and to your API keys page, where you get a key that calls the API. Nothing is charged.

{"error": {"embed_docs_url": "https://homedata.co.uk/widgets#embed", "api_keys_url": "https://homedata.co.uk/developer/api-keys", "code": "widget_token_not_api_key", "message": "This is a widget token, not an API key. A widget token only works inside the embed snippet on your allowed domain. To call the API directly, use an API key from your dashboard: https://homedata.co.uk/developer/api-keys", "status": 403}}

Data limited to enterprise accounts answers enterprise_only: property listings for accounts opened after 16 September 2026, and listing filters other than uprn for any account that is not enterprise. The key and the request are fine, nothing is charged, and topping up, retrying or rotating the key will not clear it. The body carries a contact object saying who to talk to about access, an enquire_url, and alternatives: any paid route you can call instead. Show those to the caller rather than retrying.

{"error": {"code": "enterprise_only", "message": "Property listings is limited to enterprise accounts, so topping up will not unlock it. To discuss enterprise access, email [email protected] or ... You weren't charged for this call.", "hint": "Available to you now: GET /api/property_sale_events/?uprn=100040253100 (20 tokens). See `alternatives`.", "docs": "https://homedata.co.uk/docs/errors#enterprise_only", "status": 403, "contact": {...}, "enquire_url": "https://homedata.co.uk/contact", "alternatives": [{"method": "GET", "path": "/api/property_sale_events/?uprn=100040253100", "cost_tokens": 20, "description": "..."}]}}
404

Not found

We hold no record of what you asked for, or the route does not exist. Every 404 is a coded object with a hint and a docs link. For a property it is usually uprn_not_found: the UPRN is a land parcel or another one that never gets post. An incomplete route, such as /address?q=… or /property/ with no UPRN, gets RESOURCE_NOT_FOUND with a hint naming the route to use. See Not found for each code.

{"error": {"uprn": "10091234567", "code": "uprn_not_found", "message": "We don't hold a property with UPRN 10091234567. Homedata covers residential and commercial addresses in the UK; land parcels and other non-postal UPRNs aren't covered. You weren't charged for this call.", "hint": "For one home, use /address/match/?address=<address>&postcode=<postcode>. ...", "docs": "https://homedata.co.uk/docs/errors#uprn_not_found", "status": 404}}

A UPRN we hold no record of returns 404, and an empty or malformed one returns 400. Both are refunded in full (on a wallet key X-Tokens-Charged reads 0), and repeating the call gives the same answer, so treat it as final rather than retrying.

500

Internal server error

A 500 can mean a temporary outage or an internal error. Check the JSON error.code: service_unavailable means retry with backoff, respecting Retry-After. Endpoint-specific temporary errors include rmas_unavailable and rmas_index_not_ready. The retry sample also handles 503 responses from API gateway routes. Other 500 errors follow the normal error path; contact us if they persist.

504

Gateway timeout

Request took too long to process. Retry with exponential backoff. If frequent, the endpoint may be under heavy load.

Error codes

Each code below is the one in error.code (or reason.code on an empty answer), and each has its own link: an error's docs field opens it here. Codes are a contract; the wording of message and hint may change. A code is not tied to one status everywhere, so branch on the HTTP status first, then switch on error.code case-insensitively.

Keys and access

A 403 is not always a key problem. Only INVALID_API_KEY means the key itself is wrong.

INVALID_API_KEY 403 Not charged

The key is missing, wrong, revoked or unknown, or not allowed on this endpoint. A request with no key at all lands here too, not on 401, with the message "Authentication credentials were not provided."

What to try: Send the key as Authorization: Api-Key <your key>, not Bearer, then check it on your API keys page.

widget_token_not_api_key 403 Not charged

You sent a widget embed token (it starts wt_) where an API key goes. A widget token only works inside the embed snippet on your allowed domain, and it has no secret half, so it can never authenticate an API call.

What to try: To call the API directly, use an API key: error.api_keys_url links to your API keys page. To show data on your site, keep the token in the embed snippet and follow error.embed_docs_url, the embed docs.

enterprise_only 403 Not charged

The data is limited to enterprise accounts, so topping up will not unlock it. Property listings (/property_listings/ and /property_listings/<id>/) answer this way for accounts opened after the switch to tokens on 16 September 2026, for whom listings are arranged per customer, and listing filters other than uprn answer this way for any account that is not enterprise. The key and the request are fine.

What to try: Read error.alternatives: it is always present, lists any paid route you can call instead (for example a home's listing history from /property_sale_events/?uprn=<UPRN>), and is empty when there is none. error.contact says who to talk to about enterprise access and error.enquire_url links to the enquiry form, so show those to the caller rather than retrying. Before 22 October 2026 listings refused with premium_endpoint and other filters with a plain string; switch on enterprise_only instead.

auth_error 401 Not charged

The key was recognised but its server-side configuration is missing. Only the Explore and listing-address endpoints send it. It is not what a missing key gets; that is 403.

What to try: Contact us: nothing on your side will clear it.

Token balance

The call costs more tokens than your balance holds. error.required and error.available say how many. Most endpoints also send error.status and error.topup_url, a link to a one-off top-up; listings, reveals and listing-address omit both.

What to try: Top up, then send the same request again. Where the response has error.topup_url, send people to that link rather than one you have saved, as it can change. Retrying without topping up will not clear it.

Not found

Every 404 is free and final: the same call gives the same answer.

uprn_not_found 404 Not charged

We hold no property with that UPRN. It is usually a land parcel or another UPRN that never gets post, which we do not cover (see coverage). Also sent by /property_lr_titles/?property=<UPRN>, /property_lr_titles/<UPRN>/ and /address/retrieve/<UPRN>/. A list filtered by such a UPRN answers 200 with this as its reason, and that call is charged.

What to try: The API's hint: "For one home, use /address/match/?address=<address>&postcode=<postcode>. For every address in a postcode, use /address/postcode/<postcode>/. Use the UPRN returned for the property."

property_not_found 404 Not charged

The same as uprn_not_found, from older property endpoints.

What to try: As for uprn_not_found.

title_not_found 404 Not charged

We hold the property, but no Land Registry title is linked to it (/property_lr_titles/<UPRN>/). /property_lr_titles/?property=<UPRN> for a home with no title at all answers 200 with this as its reason, and is charged.

What to try: Titles cover England and Wales. A flat or a new build may sit under a parent title: try ?title_no=.

postcode_not_found 404 Not charged

We hold no data for that postcode.

What to try: Check that the postcode is complete and in Great Britain, for example SW1A 2AA.

listing_not_found 404 Not charged

No listing has that id.

What to try: Check the id. Listings are found by UPRN with /property_listings/?uprn=; for accounts opened after 16 September 2026, listings are arranged per customer.

RESOURCE_NOT_FOUND 404 Not charged

The URL, or the id in it, does not exist. Since 29 October 2026 an incomplete address or property route gets this with a hint naming the right one: /address or /property with nothing after it, with or without the trailing slash or the /api/ prefix, and whatever query you add, such as /address?q=… or /address/?postcode=….

What to try: For one home, use /address/match/?address=<address>&postcode=<postcode>. For every address in a postcode, use /address/postcode/<postcode>/. For a property by UPRN, use /property/<uprn>/. Otherwise check the URL and the id.

not_found 404 Not charged

An older lowercase code a few endpoints still send for an absent record.

What to try: Treat any 404 as an absent record rather than matching on one code.

Address lookup found no single address

/address/match/ sends these as a free 422, and still returns its original reason string (no_match or multiple_matches) beside the error object. The older address search, /address/find/, still served to keys created before 8 October 2026, sends the first two as a 200 reason instead, and that call is charged.

address_no_match 422 Not charged

No address matched what you sent.

What to try: Send a house number or name, street and postcode.

looks_like_grid_ref 422 Not charged

What you sent looks like an OS grid reference, such as TL4256 5508. Address lookup matches postal addresses only.

What to try: Send the postal address instead. Land with no postal address is not covered.

multiple_matches 422 Not charged

More than one address matched.

What to try: Add the flat or unit, and the house number or name.

Bad requests

Every 400 is free. Fix the request, then send it again.

invalid_parameter 400 Not charged

A list endpoint got a filter in the wrong format, or none of its required filters: /property_lr_titles/, /property_sales/, /property_sale_events/, /property_listings/ (for accounts opened after 16 September 2026, listings are arranged per customer) and /agent_stats/. error.parameter names the filter and the message gives the format we expect. A custom property call (/property/<uprn>/?with=) where none of the add-ons is one we know also gets it, with error.valid_slugs listing the names you can use. The older address search sends it as a 422.

What to try: Fix the value the message names. The API reference lists every parameter and its format.

invalid_query 400 Not charged

The older address search, /address/find/, got q shorter than 2 characters.

What to try: Send at least 2 characters, or use /address/match/.

invalid_postcode 400 Not charged

/address/postcode/<postcode>/ got something that is not a whole postcode.

What to try: Send the full postcode, for example SW1A 2AA.

invalid_uprn 400 Not charged

The UPRN in the URL is not digits or a uprn_token.

What to try: Send the UPRN as digits only, as address lookup returns it.

missing_parameters 400 Not charged

/address/match/ needs both address and postcode.

What to try: Send both.

result_window_exceeded 400 Not charged

/live-listings/search/ was asked for a page past the 10,000th result (page times page_size over 10,000, for example page 51 at a page_size of 200). Searches that include withdrawals, and back_on_market searches, are not limited.

What to try: Narrow the location or the filters. Live listings search is arranged per customer.

BAD_REQUEST 400 Not charged

The request could not be processed as sent, for example a body that is too large.

What to try: Check the request against the API reference.

Empty answers

These are not errors. The call worked and found nothing, so it answers 200 with the usual fields (an empty results, suggestions or addresses) plus a reason object, and it is charged at the endpoint's normal rate. uprn_not_found, title_not_found, address_no_match and looks_like_grid_ref can also arrive as a reason.code, as described above. Nearby searches (schools, healthcare, crime, risks and similar) carry no reason: inside coverage, none within the radius is the answer itself.

no_match 200 Charged

A list endpoint's filters matched nothing; a postcode has no addresses; /lr-sales/ found no confirmed sales; agent market share found no listings in the outward postcodes; or a home's titles were all excluded by title_no or ownership.

What to try: Check the filter values, or widen them.

data_unavailable 200 Charged

Listed buildings, planning designations or conservation areas: the dataset is not loaded, so nothing was checked (data_available: false). It is not an all-clear.

What to try: Do not read it as "none found". Try again later.

Our side

Every 5xx is free. A 500 is either a temporary outage or an internal error, so read error.code before deciding to retry: the retry sample retries only the temporary codes below.

INTERNAL_ERROR 500 Not charged

Something failed on our side. It is logged automatically.

What to try: Handle it as a normal error rather than retrying in a loop. If it persists, contact us.

service_unavailable 500 Not charged

A data source is temporarily unavailable. The JSON error and its retry hint are kept.

What to try: Retry with backoff, waiting at least Retry-After seconds when the response sends it. Compare the code case-insensitively: some responses send it upper case.

rmas_unavailable 500 Not charged

The rental appraisal's comparables read timed out. The call is refunded.

What to try: Retry with backoff.

rmas_index_not_ready 500 Not charged

The rental appraisal's search index is still being built.

What to try: Retry with backoff; it clears once the index is ready.

wallet_unavailable 503 Not charged

Your token balance could not be read for a moment, so the call was not run.

What to try: Retry with backoff.

UPSTREAM_TIMEOUT 504 Not charged

The request took too long upstream.

What to try: Retry with backoff.

Handling errors in code

# Python: branch on the status, then the code
def raise_for_homedata_error(response):
    """Raise for a failed response. Returns None when the record is absent."""
    err = response.json().get("error", {})

    # Shape 3: a plain string. Check before you call .get()
    if isinstance(err, str):
        message, code = err, ""
    else:
        message = err.get("message", "")
        # Codes are UPPER_SNAKE_CASE from the platform, lowercase from
        # endpoints, so always normalise before comparing
        code = (err.get("code") or "").upper()

    # Status first: it is always there, error.status is not
    if response.status_code == 403:
        # Not every 403 is a key problem.
        if code == "INVALID_API_KEY":
            # A missing key arrives here too, not on 401
            raise AuthError(message)
        if code == "WIDGET_TOKEN_NOT_API_KEY":
            # A widget embed token (wt_...) was sent as the API key. It only
            # works in the embed; error.api_keys_url is where to get a key.
            raise AuthError(message)
        # ENTERPRISE_ONLY and the rest: the key is fine, so do not ask for
        # a new one. error.alternatives lists any route you can call instead.
        raise ApiError(f"403: {message}")
    elif response.status_code == 402:
        raise AllowanceError(message)  # retrying will not help
    elif response.status_code == 404:
        return None
    elif response.status_code == 400:
        # code may be "" on the plain-string shape
        raise ValidationError(code or "bad_request", message)
    else:
        raise ApiError(f"{response.status_code}: {message}")


response = requests.get(url, headers=headers)
# Raises, or gives back None when the record simply is not there
data = response.json() if response.ok else raise_for_homedata_error(response)

Rate limiting

An authenticated response reports your usage in headers. Wallet keys get the token headers below and no rate-limit headers at all; quota keys get the X-RateLimit-* family instead.

Header Description
X-Tokens-Charged Tokens this request cost (0 when it was refunded)
X-Tokens-Balance Tokens left on your balance after this request
X-Call-Weight Same number as X-Tokens-Charged, kept for older integrations
Retry-After On a temporary-unavailability 500: seconds to wait before retrying

Best practice: back off on transient errors

# Python: retry with backoff
import time
import requests

def api_call(url, headers, max_retries=3):
    for attempt in range(max_retries):
        response = requests.get(url, headers=headers)
        code = ""
        if response.status_code == 500:
            try:
                payload = response.json()
            except ValueError:
                payload = None
            error = payload.get("error") if isinstance(payload, dict) else None
            if isinstance(error, dict):
                code = str(error.get("code", "")).lower()
        temporary = code in (
            "service_unavailable", "rmas_unavailable", "rmas_index_not_ready"
        )
        if response.status_code in (503, 504) or (response.status_code == 500 and temporary):
            wait = 2 ** attempt  # 1s, 2s, 4s
            retry_after = response.headers.get("Retry-After", "")
            if retry_after.isdigit():
                wait = max(wait, int(retry_after))
            time.sleep(wait)
            continue
        # Anything else that failed goes through raise_for_homedata_error above,
        # so an error body is never returned as if it were data
        if not response.ok:
            # return, so a 404's None is not thrown away
            return raise_for_homedata_error(response)
        return response.json()
    raise Exception("Still failing after retries")

Integrate into your own product

Pay as you go
Homedata API Scientific, granular measurements

Homedata API errors always arrive under an error key. Almost all are an object with a stable code, a message and a status, and most add a hint saying what to try and a docs link to that code on this page. A few still use an older lowercase object with no status, or a plain message string. Branch on the HTTP status, compare codes case-insensitively, and handle all three shapes.

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 →