Error Handling
Errors arrive under an error key in three shapes, and a client has to handle all three. Most endpoints return a coded object. Some return error as a plain message string with no code at all. Branch on the HTTP status first, compare codes case-insensitively, never assume error.status is present, and check whether error is a string before you read error.code.
Error format
The three shapes, and the trap between the first two: the codes differ in case. A client comparing err.code == "BAD_REQUEST" silently misses invalid_uprn and never fires. Normalise the case before you compare.
| Shape | Looks like | Where it comes from |
|---|---|---|
| Platform envelope | {"error": {"code": "INVALID_API_KEY", "message": "...", "status": 403}} | Usually an UPPER_SNAKE_CASE code, and the only shape that carries status. Authentication uses upper case; token-balance refusals use lowercase insufficient_tokens |
| Endpoint coded object | {"error": {"code": "invalid_uprn", "message": "..."}} | Lowercase code, usually no status. An endpoint rejecting your parameters |
| Plain string | {"error": "Provide one of: postcode, lat+lng, or uprn"} | No code at all. Read it as the message |
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Property not found.",
"status": 404
}
}
| Field | Type | Description |
|---|---|---|
| error | object | string | Present on every non-2xx response, but not always an object. Most endpoints put a coded object here; some put a plain message string with no code. Always check the type before reading code. |
| error.code | string | Machine-readable code. Errors raised by the API use UPPER_SNAKE_CASE constants; some endpoints return their own validation codes in lowercase. Safe to switch on if you compare case-insensitively. |
| error.message | string | Human-readable description. May change, so don't match on this programmatically. |
| error.status | integer | Mirrors the HTTP status code, so the body is self-describing if you log it on its own. Only the platform envelope carries it, so never rely on it being there. |
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.
OK
Request succeeded. Response body contains the requested data.
Bad Request
Missing or invalid parameters. Check the message field for specifics.
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.
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:
Listings, reveals and listing-address return the smaller shape, with no status or topup_url field:
Top up and retry. Nothing else about the request needs changing, and retrying without topping up will not clear it.
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. If the code is anything else, or absent, the key is fine and something about the request is not allowed, so do not send the caller off to check their credentials. Read the message to tell them apart: "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, and it means the key is fine, so handle it as a balance problem rather than a credential one.
One 403 does not use the envelope at all: asking the listings endpoint for a filter your plan does not include returns error as a plain string with no code. Handle that shape as well, as described under Error format.
The listings endpoint is arranged per customer, on request, for accounts opened after the switch to tokens on 16 September 2026. Until it is switched on for your account, a call to /property_listings/ is refused with the lowercase code premium_endpoint and HTTP status 403. The key is fine and so is the request, and nothing is charged: retrying or rotating the key will not clear it. The body carries a contact object with who to talk to about access; show it to the caller rather than retrying. To request access, register interest.
Not Found
The requested resource doesn't exist. For property endpoints, this means no data for that UPRN.
Internal Server Error
Something went wrong on our side. These are rare and we're alerted automatically. If persistent, please contact us.
Gateway Timeout
Request took too long to process. Retry with exponential backoff. If frequent, the endpoint may be under heavy load.
Error codes
Errors raised by the API carry these UPPER_SNAKE_CASE codes. Endpoints also return their own codes in lowercase, and those are not tied to one status: invalid_uprn and invalid_postcode come back with 400, while not_found, uprn_not_found and postcode_not_found come back with 404. A lowercase code is not always the same word as its upper-case counterpart, so an absent record may arrive as not_found rather than RESOURCE_NOT_FOUND. Branch on the HTTP status first, then switch on error.code case-insensitively.
| Code | Status | Description |
|---|---|---|
| BAD_REQUEST | 400 | The request could not be processed as sent |
| auth_error | 401 | Key recognised but its configuration is missing. Explore and listing-address only. Not what you get for a missing key; that is 403 |
| insufficient_tokens | 402 | Not enough tokens for this request; error.required and error.available say how many. Most endpoints include error.status and error.topup_url; listings, reveals and listing-address omit them. Top up, then retry |
| INVALID_API_KEY | 403 | Every key rejection: missing, wrong, revoked, unknown, or not allowed on this endpoint. Read the message to tell them apart |
| premium_endpoint | 403 | Listings on an account opened after 16 September 2026 that has not had it switched on. The key is fine; error.contact says who to ask for access. Not charged |
| RESOURCE_NOT_FOUND | 404 | No data for the requested resource (UPRN, postcode, URN, etc.) |
| not_found | 404 | The same thing from an endpoint's own check, in lowercase. Variants include uprn_not_found and postcode_not_found, so treat any 404 as an absent record rather than matching on one code |
| INTERNAL_ERROR | 500 | Something failed on our side. Logged automatically |
| SERVICE_UNAVAILABLE | 503 | Data source temporarily unavailable. Retry with backoff |
| UPSTREAM_TIMEOUT | 504 | The request took too long upstream. 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. A key rejection always carries # INVALID_API_KEY; a plan restriction comes back as a bare string # with no code, and swapping the key will not fix it. if code == "INVALID_API_KEY": # A missing key arrives here too, not on 401 raise AuthError(message) # Remove the filter or change plan. The key is not the problem. 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 503: seconds to wait before retrying |
Best practice: back off on transient errors
# Python: retry with backoff import time def api_call(url, headers, max_retries=3): for attempt in range(max_retries): response = requests.get(url, headers=headers) if response.status_code in (503, 504): wait = 2 ** attempt # 1s, 2s, 4s 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 goHomedata API errors always arrive under an error key, in one of three shapes: a platform envelope with a status, an endpoint code without one, or a plain message string. Codes are usually upper case in the platform envelope and lowercase in endpoint responses, but insufficient_tokens can use either object shape. Branch on the HTTP status, compare codes case-insensitively, and handle all three.
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, OpenAPI docs, sandbox — 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