API reference
Overview
Base URL /v1. All requests and responses are JSON. Send a UPRN or a UK postcode; get back coordinates and a precision tier. Lookups are free and unlimited.1,000 successful lookups are free per account, then a one-time £20 unlocks unlimited use. Unmatched lookups never count against your allowance.
Authentication
Get a key from the dashboard or via POST /v1/signup. The demo endpoints below work without a key.
Precision & coverage
Every response carries a precision field:
| Tier | Meaning |
|---|---|
| property | Exact building coordinate, via a UPRN or an address match against the register. matched_via says which. |
| postcode | Postcode-centroid fallback. Typically 50 m – several hundred metres from the building. |
| unmatched | Nothing resolved. Free — not counted against your allowance. |
Property-level coverage: England & Wales — homes, commercial premises, and public buildings, via the domestic and non-domestic EPC registers and Display Energy Certificates. Postcode centroids: England, Wales, Scotland. Northern Ireland (BT) postcodes return unmatched for data-licensing reasons.
How matching works
An address is text, not a place. Turning one into the other is a matching problem, and it pays to know exactly how the matching is done, because the failure mode of a geocoder is not "no answer": it is a confident answer in the wrong place.
The reference set
Ordnance Survey gives every addressable building in Britain a Unique Property Reference Number (UPRN) and publishes each one's coordinates as open data. Energy Performance Certificates carry both a written address and its UPRN, and nearly every home sold or rented since 2008 has one. Joining the two gives roughly 20 million verified examples of "this text means that building". That join is the reference set every lookup runs against.
Cleaning and variants
The same address gets written many ways: "Apartment A" against "Flat A", "St Johns Road" against "Saint Johns Road", "12A" against "12 A". Both the reference set and your input are cleaned by one shared set of rules: abbreviations spelled out, punctuation dropped, number and letter fused. Each reference address is then stored under several spellings at once, including a form with words like Flat and Apartment removed (the identifying letter or number always stays) and a form with all spaces removed. Your input is looked up the same way, most exact form first. A match only ever happens within the input's own postcode.
Refusing to guess
Two rules keep precision up. First, when two different buildings produce the same cleaned text, that spelling is dropped from the reference set entirely, because answering through it would be a coin flip. Second, every reference entry is tested against its postcode's real boundary polygon; entries that sit outside the postcode they claim (the register does contain UPRNs pointing at the wrong town) are removed, and the removals are kept on file so a future rebuild cannot reintroduce them.
The fallback ladder
You always get the most precise answer the evidence supports, labelled. A verified building match returns property. Failing that, the postcode centroid returns postcode, usually within a street or two. Failing that, unmatched, which costs you nothing. The response never dresses one tier up as another.
Single geocode
| Param | Notes |
|---|---|
uprn | Positive integer, up to 12 digits. Tried first. |
postcode | UK postcode, any casing or spacing. |
address | Optional address text. Fuzzy-matched against the register within the postcode; a confident match returns building-level coordinates with matched_via: "address" and a confidence score. |
curl -H "X-API-Key: $KEY" \
"BASE/v1/geocode?uprn=100012"
{
"query": { "uprn": 100012 },
"matched": true,
"precision": "property",
"lat": 51.4566671,
"lon": -2.5591032,
"uprn": 100012
}
Batch geocode
Up to 500 queries per call, answered in order. The response includes a per-tier summary. This is what the upload tool uses under the hood, and it suits spreadsheet pipelines: send address rows from Excel or CSV, get lat and lon back in order.
curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{ "queries": [ {"uprn": 100012}, {"postcode": "SW1A 1AA"} ] }' \
"BASE/v1/geocode/batch"
{
"results": [
{ "query": {"uprn": 100012}, "matched": true, "precision": "property",
"lat": 51.4566671, "lon": -2.5591032, "uprn": 100012 },
{ "query": {"postcode": "SW1A 1AA"}, "matched": true, "precision": "postcode",
"lat": 51.5010, "lon": -0.1416 }
],
"summary": { "property": 1, "postcode": 1, "unmatched": 0 }
}
Add "preview": true to the body for a preview of up to 25 queries with the same response. Nothing is metered in free mode, and the response carries preview_remaining: null.
Add "preview": true to the body for a free preview: up to 25 queries, the same response, nothing counted against your 1,000 lookups. Free-tier previews draw on their own allowance of 300 rows per day, counted per key and per network (unlocked keys are not capped); the response carries preview_remaining, and a spent allowance returns 429 preview_limit while a full call still works. Only a full call spends your 1,000 lookups.
Demo endpoints
No key required — these power the site's try-it box and the tool's 25-row preview for visitors without a key (a key previews through "preview": true above, also free). Same response shapes as above. The keyless demo is limited to 300 lookups per IP per day (a free key has no such limit); the batch form accepts up to 25 queries per call and returns demo_remaining.
Signup
curl -X POST -H "Content-Type: application/json" \
-d '{ "email": "you@company.co.uk" }' "BASE/v1/signup"
{ "api_key": "…", "usage_count": 0, "paid_tier": false, "free_limit": null, "unlocked": true }{ "api_key": "…", "usage_count": 0, "paid_tier": true, "free_limit": 1000, "unlocked": false }
One key per email. The key is shown once in the response and also emailed to you.
Forgotten keys: send { "email": "..." } and the key is re-emailed to that address (the response never reveals whether an account exists).
Usage
curl -H "X-API-Key: $KEY" "BASE/v1/usage"
{
"email": "you@company.co.uk",
"usage_count": 342,
"paid_tier": false,
"free_limit": null,
"unlocked": true,
"breakdown": { "property": 291, "postcode": 44, "unmatched": 7 }
}
Checkout
Returns a Stripe Checkout URL for the one-time £20 unlock. On completion your key becomes unlimited — nothing else changes.
curl -X POST -H "X-API-Key: $KEY" "BASE/v1/checkout"
{ "checkout_url": "https://checkout.stripe.com/c/pay/…" }
Errors & limits
| Status | Meaning |
|---|---|
| 400 | Malformed request — missing or invalid parameters. |
| 401 | invalid_api_key |
| 402 | free_tier_exhausted — body includes checkout_url. Unlock for £20 once. |
| 500 | lookup_failed: the lookup failed on our side. Nothing was charged (any reserved lookups are refunded); retry in a moment. |
| 429 | Rate limited: 600 requests/minute per key (rate_limited), the daily demo allowance (demo_limit), or the daily preview allowance of 300 rows per key and per network (preview_limit). |
{
"error": "free_tier_exhausted",
"usage_count": 1000,
"free_limit": 1000,
"checkout_url": "BASE/dashboard"
}
Rate limits protect the infrastructure: 600 requests/minute per key, 3 signups per IP per day.