Houski serves property data for Canada, the United States, Great Britain, Latvia, Slovenia, Czechia, France, Spain, the Netherlands, Finland, Poland, Norway, Switzerland, Estonia, Austria and South Korea. That is more than 220 million properties behind one API key, one set of endpoints and one schema.
The point of one schema is that code written for Calgary runs unchanged for Lyon, Kraków or Busan. The honest part is that the countries are not identical underneath, and a few differences matter before you build. This post walks through both.
What is the same everywhere
The endpoints and the query language. /properties, /search, /aggregate, /map, /geocoding, /location, /predict and /avm work the same way in every country. Filters are a field, an operator suffix and a value, so property_type_eq=House means the same thing in Norway as it does in Texas.
The field names. A field is called the same thing wherever it appears. interior_sq_m is the floor area in square metres in every country, and construction_year is the year built in every country.
The place hierarchy. Every property sits in a country, a province or region, a city and, where the country has them, a community. The country_abbreviation, province_abbreviation and city parameters scope a query the same way everywhere. The /location endpoint lists the places that hold data, so a place menu never has to be written by hand:
const houski_data = async (): Promise<LocationResponse> => {
// You must copy the LocationResponse type declarations from the
// Houski API documentation to strongly type the response
const url = new URL('https://api.houski.ca/location');
url.searchParams.set('api_key', 'YOUR_API_KEY');
url.searchParams.set('resolution', 'country');
const response = await fetch(url);
const data = await response.json();
return data;
}
(async () => {
let data: LocationResponse = await houski_data();
// Log the response
console.log(data);
})();
{
"cache_hit": true,
"cost_cents": 0.016,
"data": {
"flat": [],
"nested": {
"countries": [
{
"abbreviation": "AT",
"id": "142e28be7ac18272",
"link": "at",
"name": "Austria",
"provinces": [],
"slug": "austria"
},
{
"abbreviation": "CA",
"id": "9ace2b6431b7f1be",
"link": "ca",
"name": "Canada",
"provinces": [],
"slug": "canada"
},
{
"abbreviation": "CZ",
"id": "679e47cff1d8d20e",
"link": "cz",
"name": "Czechia",
"provinces": [],
"slug": "czechia"
},
{
"abbreviation": "EE",
"id": "4c453a35d29e64ff",
"link": "ee",
"name": "Estonia",
"provinces": [],
"slug": "estonia"
},
{
"abbreviation": "FI",
"id": "d0d66df232b3306c",
"link": "fi",
"name": "Finland",
"provinces": [],
"slug": "finland"
},
{
"abbreviation": "FR",
"id": "ab154f820778087b",
"link": "fr",
"name": "France",
"provinces": [],
"slug": "france"
},
{
"abbreviation": "LV",
"id": "abdd6d0f95d16047",
"link": "lv",
"name": "Latvia",
"provinces": [],
"slug": "latvia"
},
{
"abbreviation": "NL",
"id": "5415fd86d346b2e6",
"link": "nl",
"name": "Netherlands",
"provinces": [],
"slug": "netherlands"
},
{
"abbreviation": "NO",
"id": "9d08fc5ab702aae8",
"link": "no",
"name": "Norway",
"provinces": [],
"slug": "norway"
},
{
"abbreviation": "PL",
"id": "c0fda925c9d464d4",
"link": "pl",
"name": "Poland",
"provinces": [],
"slug": "poland"
},
{
"abbreviation": "SI",
"id": "ccfbe01fb7f7e08d",
"link": "si",
"name": "Slovenia",
"provinces": [],
"slug": "slovenia"
},
{
"abbreviation": "KR",
"id": "25999da998ed58d9",
"link": "kr",
"name": "South Korea",
"provinces": [],
"slug": "south_korea"
},
{
"abbreviation": "ES",
"id": "d5a2a53821ed7e7a",
"link": "es",
"name": "Spain",
"provinces": [],
"slug": "spain"
},
{
"abbreviation": "CH",
"id": "3c3b0201fea9e2ee",
"link": "ch",
"name": "Switzerland",
"provinces": [],
"slug": "switzerland"
},
{
"abbreviation": "GB",
"id": "90882893598cc98f",
"link": "gb",
"name": "United Kingdom",
"provinces": [],
"slug": "united_kingdom"
},
{
"abbreviation": "US",
"id": "c148472ce4bd39d",
"link": "us",
"name": "United States",
"provinces": [],
"slug": "united_states"
}
]
}
},
"error": "",
"is_flat": false,
"is_nested": true,
"price_quote": false,
"result_total": 16,
"time_ms": 4089
}
Property ids. A property_id is unique and stable across every country, so one table in your system can hold properties from all sixteen.
The scores. Every score_ field runs from 0 to 10, and higher is always better, in every country.
Pricing. Most fields cost a tenth of a cent per field per property in every country, with the same 99 dollar monthly minimum on the account.
What differs, and why
Money comes back in its own currency. A French price is in euros, a British one in pounds, a Korean one in won, and so on: dollars for Canada and the United States, pounds for Great Britain, euros for Latvia, Slovenia, France, Spain, the Netherlands, Finland, Estonia and Austria, and koruna, zloty, kroner and francs for Czechia, Poland, Norway and Switzerland. Convert before you put two countries on one axis.
Scores are ranked within one country. A score is built from the sources that exist in each country, and the sources differ, so the same number does not describe the same thing on both sides of a border. A 7 in Calgary and a 7 in Rotterdam are each a statement about their own country. Compare places inside a country freely. To compare across a border, pick a pair of places you know well on each side and normalize against them yourself.
Not every field exists in every country. A field with no source in a country is left empty there, never filled with a placeholder. A few examples: snowfall exists only in Canada and the United States, council_tax_band only in England and Wales, and some scores have no source in some countries (Austria has no safety score, and Switzerland and South Korea have no water quality score). The fields reference describes each field, and a count aggregation tells you how well a field is filled in the market you care about.
The property list covers different things. In most countries the list holds flats and houses alike. In France, Spain, Finland and Austria it holds houses only.
Demographics follow what each country publishes. Each country's figures sit on the smallest areas the country publishes them for, and where a figure is published only for a larger area, the small areas inside draw on the larger area's figure. Household income in several European countries, for example, is one figure per region.
Valuations need a model that passes its checks. A property is valued only by the models of its own country. Where a country has no model for a target that passes Houski's accuracy checks, that estimate is left empty rather than borrowed from another country. The AVM information page lists the models that serve, with their accuracy.
Histories differ. Recorded sales appear as Sold events where a country or region publishes them. Outside Canada and the United States, listing histories carry Listed and Unlisted events. Permits exist where they are published, which covers whole countries such as France, Poland, Finland and South Korea and cities and regions elsewhere.
Three calls across borders
A count of the French properties Houski holds:
const houski_data = async (): Promise<AggregateResponse> => {
// You must copy the AggregateResponse type declarations from the
// Houski API documentation to strongly type the response
const url = new URL('https://api.houski.ca/aggregate');
url.searchParams.set('aggregation', 'count');
url.searchParams.set('api_key', 'YOUR_API_KEY');
url.searchParams.set('country_abbreviation', 'fr');
url.searchParams.set('field', 'property_id');
const response = await fetch(url);
const data = await response.json();
return data;
}
(async () => {
let data: AggregateResponse = await houski_data();
// Log the response
console.log(data);
})();
{
"cache_hit": true,
"cost_cents": 1.0,
"data": [
{
"aggregation": "count",
"field": "property_id",
"value": "19110961"
}
],
"error": "",
"price_quote": false,
"time_ms": 10
}
The median neighbourhood safety score across the Netherlands, ranked within the Netherlands:
const houski_data = async (): Promise<AggregateResponse> => {
// You must copy the AggregateResponse type declarations from the
// Houski API documentation to strongly type the response
const url = new URL('https://api.houski.ca/aggregate');
url.searchParams.set('aggregation', 'median');
url.searchParams.set('api_key', 'YOUR_API_KEY');
url.searchParams.set('country_abbreviation', 'nl');
url.searchParams.set('field', 'score_safety');
const response = await fetch(url);
const data = await response.json();
return data;
}
(async () => {
let data: AggregateResponse = await houski_data();
// Log the response
console.log(data);
})();
{
"cache_hit": true,
"cost_cents": 1.0,
"data": [
{
"aggregation": "median",
"field": "score_safety",
"value": "9"
}
],
"error": "",
"price_quote": false,
"time_ms": 26
}
British homes with their council tax band and value estimate, in pounds:
const houski_data = async (): Promise<PropertiesResponse> => {
// You must copy the PropertiesResponse type declarations from the
// Houski API documentation to strongly type the response
const url = new URL('https://api.houski.ca/properties');
url.searchParams.set('api_key', 'YOUR_API_KEY');
url.searchParams.set('country_abbreviation', 'gb');
url.searchParams.set('results_per_page', '5');
url.searchParams.set('select', 'address,city,province_abbreviation,council_tax_band,estimate_list_price');
const response = await fetch(url);
const data = await response.json();
return data;
}
(async () => {
let data: PropertiesResponse = await houski_data();
// Log the response
console.log(data);
})();
{
"cache_hit": true,
"cost_cents": 1.2,
"data": [
{
"address": "The Haven Church Street",
"city": "New Quay",
"council_tax_band": "E",
"estimate_list_price": 377969,
"property_id": "104c369b238f89cc",
"province_abbreviation": "WLS"
},
{
"address": "The Spinney Rectory Square",
"city": "New Quay",
"estimate_list_price": 513085,
"property_id": "1064575f7e08a75e",
"province_abbreviation": "WLS"
},
{
"address": "19 Cwm Halen",
"city": "New Quay",
"council_tax_band": "D",
"estimate_list_price": 299080,
"property_id": "108250aa7fc2ce67",
"province_abbreviation": "WLS"
},
{
"address": "15 Dolphin Court",
"city": "New Quay",
"council_tax_band": "E",
"estimate_list_price": 402323,
"property_id": "1090b829a0eba96d",
"province_abbreviation": "WLS"
},
{
"address": "22 Cylch-Y-Llan",
"city": "New Quay",
"council_tax_band": "C",
"estimate_list_price": 266260,
"property_id": "109604606283b0a9",
"province_abbreviation": "WLS"
}
],
"error": "",
"pagination": {
"current_page": 1,
"has_next_page": true,
"has_previous_page": false,
"page_total": 4206175
},
"price_quote": false,
"result_total": 21030875,
"time_ms": 329,
"ui_info": {
"country": "United Kingdom",
"country_abbreviation": "GB",
"country_abbreviation_id": "90882893598cc98f",
"country_abbreviation_link": "gb",
"country_slug": "united_kingdom"
}
}
Swap the country_abbreviation and the same calls run anywhere Houski covers.
Building for more than one country
- Keep the country on every row. Select country_abbreviation alongside the money fields so your code always knows which currency it is holding.
- Normalize scores per country before you rank properties from different countries in one list.
- Treat empty as unknown. An empty field means there is no information, not zero and not none.
- Build place menus from /location, so a newly added region shows up on its own.
- Check coverage per country with a count aggregation before you promise a feature in a new market.
Getting started
- Sign up for API access
- List the countries and regions with the /location call above
- Run one query in two countries and compare the responses side by side
- Read the fields reference for any field whose coverage matters to you
Details on the pricing page.
One schema does not pretend the world is uniform. It makes the differences visible in the same place, so your code handles them once. Explore the API or browse the datasets.
