A user types an address into your form, and your code needs the facts of that home: property type, bedrooms, bathrooms, floor area, construction year and an estimated value. This post shows a complete JavaScript program that does it with the Houski property data API and the fetch function built into Node 18 or later. It needs no packages.
Houski covers more than 220 million properties in 16 countries under one schema, with data updated daily. No license or Multiple Listing Service (MLS) membership is needed.
Search for an address with fetch
The search endpoint takes a typed address and returns the best matches first. The match is fuzzy, so typos and half-typed addresses still find the home. This block is a real call against the live API, run each time this page loads:
const houski_data = async (): Promise<SearchResponse> => {
// You must copy the SearchResponse type declarations from the
// Houski API documentation to strongly type the response
const url = new URL('https://api.houski.ca/search');
url.searchParams.set('api_key', 'YOUR_API_KEY');
url.searchParams.set('max_results', '5');
url.searchParams.set('query', '123+main+st+calgary');
url.searchParams.set('select', 'address,property_type,estimate_list_price,bedroom,bathroom_half,bathroom_full');
const response = await fetch(url);
const data = await response.json();
return data;
}
(async () => {
let data: SearchResponse = await houski_data();
// Log the response
console.log(data);
})();
{
"cache_hit": true,
"cost_cents": 2.1499999,
"data": [
{
"address": "123 Main Street",
"bathroom_full": 1,
"bathroom_half": 0,
"bedroom": 3,
"estimate_list_price": 123919,
"property_id": "aafadb43ded908a3",
"property_type": "Townhouse"
},
{
"address": "123 Main Street",
"bathroom_full": 1,
"bathroom_half": 0,
"bedroom": 3,
"estimate_list_price": 234224,
"property_id": "ae06f5914a609e6d",
"property_type": "House"
},
{
"address": "123 Midcrest Crescent SE",
"bathroom_full": 2,
"bathroom_half": 0,
"bedroom": 3,
"estimate_list_price": 585563,
"property_id": "d5f6e3fa9a720e57",
"property_type": "House"
},
{
"address": "123 Masters Street SE",
"bathroom_full": 3,
"bathroom_half": 1,
"bedroom": 4,
"estimate_list_price": 676479,
"property_id": "4a7f48fcb67f69a4",
"property_type": "House"
},
{
"address": "123 Marquis Green SE",
"bathroom_full": 4,
"bathroom_half": 1,
"bedroom": 3,
"estimate_list_price": 770055,
"property_id": "8708c4475c5f506c",
"property_type": "House"
}
],
"error": "",
"match_meta": [
{
"match_value": 0.7,
"property_id": "aafadb43ded908a3"
},
{
"match_value": 0.7,
"property_id": "ae06f5914a609e6d"
},
{
"match_value": 0.7,
"property_id": "d5f6e3fa9a720e57"
},
{
"match_value": 0.7,
"property_id": "4a7f48fcb67f69a4"
},
{
"match_value": 0.7,
"property_id": "8708c4475c5f506c"
}
],
"price_quote": false,
"result_total": 5,
"time_ms": 58
}
The query parameter holds the address, max_results takes 1 to 25, and select names the fields to return. With no select, a match carries only its address and property_id.
A complete Node.js program
Save this as get_property_data_by_address.mjs. The key is read from the HOUSKI_API_KEY environment variable and is never written into the source.
// Usage: node get_property_data_by_address.mjs "100 main calgary"
const API_URL = "https://api.houski.ca";
const API_KEY = process.env.HOUSKI_API_KEY;
if (!API_KEY) {
console.error("Set HOUSKI_API_KEY first.");
process.exit(1);
}
// Send one GET request to the Houski API and return the parsed JSON answer.
async function houski(endpoint, parameters = {}) {
const query = new URLSearchParams({ api_key: API_KEY, ...parameters });
const response = await fetch(`${API_URL}/${endpoint}?${query}`);
if (!response.ok) {
console.error(`Houski answered ${response.status}: ${await response.text()}`);
process.exit(1);
}
return response.json();
}
const address = process.argv.slice(2).join(" ") || "100 main calgary";
// select names the fields to return. Only the values that come back are billed.
const answer = await houski("search", {
query: address,
max_results: 1,
select: "property_type,bedroom,bathroom_full,interior_sq_m,construction_year,estimate_sale_price",
});
if (answer.data.length === 0) {
console.error("No property matched that address.");
process.exit(1);
}
// A field with no value for this home is left out of the row, so print what came back.
for (const [field, value] of Object.entries(answer.data[0])) {
console.log(`${field}: ${value}`);
}
console.log(`Cost: ${answer.cost_cents} cents`);
Every request is a GET that answers in JSON, so URLSearchParams and fetch are the whole client. The same program in Python, TypeScript, Rust and curl is in the examples repository.
Keep the API key on the server
The key goes in the api_key query parameter, so anyone who can see the request can copy the key. Never call the API from browser code, and never ship the key in a bundle. Call Houski from your own server and let the browser talk to that server:
import { createServer } from "node:http";
createServer(async (request, response) => {
const typed = new URL(request.url, "http://localhost").searchParams.get("address") ?? "";
const answer = await houski("search", { query: typed, max_results: 5 });
response.setHeader("Content-Type", "application/json");
response.end(JSON.stringify(answer.data));
}).listen(3000);
The browser sends the typed address to your route and receives the matches. The key stays in the environment of the server.
Missing values are left out of the row
A field with no value for a home is not in its row at all. In JavaScript, reading it gives undefined, so check before you format a number. Looping over Object.entries, as the program does, prints only what came back.
Values that no record states are worked out by Houski and labelled Inferred. Many fields have a matching field ending in _source, and exclude=inferred leaves the inferred values out of a response.
The same code works in every country
The schema is the same everywhere, and money comes back in the currency of each country. When you already know the place, the properties endpoint can find a home by its address inside a city. This live request asks for a home in Manchester, England:
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('address', '22+Sussex+Avenue');
url.searchParams.set('api_key', 'YOUR_API_KEY');
url.searchParams.set('city', 'manchester');
url.searchParams.set('country_abbreviation', 'gb');
url.searchParams.set('province_abbreviation', 'eng');
url.searchParams.set('select', 'address,construction_year,interior_sq_m,roof_material,score_flood,score_fire,estimate_sale_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": 0.52,
"data": [
{
"address": "22 Sussex Avenue",
"construction_year": 1939,
"estimate_sale_price": 692932,
"interior_sq_m": 146.0,
"property_id": "100550be3b8495f7",
"score_fire": 5,
"score_flood": 10
}
],
"error": "",
"pagination": {
"current_page": 1,
"has_next_page": false,
"has_previous_page": false,
"page_total": 1
},
"price_quote": false,
"result_total": 1,
"time_ms": 14,
"ui_info": {
"address": "22 Sussex Avenue",
"address_link": "gb/eng/manchester/unknown/22-sussex-avenue",
"address_slug": "22-sussex-avenue",
"city": "Manchester",
"city_id": "311dc539c85e1c6b",
"city_link": "gb/eng/manchester",
"city_slug": "manchester",
"community": "Unknown",
"community_link": "gb/eng/manchester/unknown",
"country": "United Kingdom",
"country_abbreviation": "GB",
"country_abbreviation_id": "90882893598cc98f",
"country_abbreviation_link": "gb",
"country_slug": "united_kingdom",
"property_id": "100550be3b8495f7",
"province": "England",
"province_abbreviation": "ENG",
"province_abbreviation_id": "5336aa935fab8e24",
"province_abbreviation_link": "gb/eng",
"province_slug": "england"
}
}
The place scope runs from country_abbreviation to province_abbreviation to city, and each level needs every level above it. The scores run from 0 to 10, and higher is always better.
Cost and price quotes
Billing is on the values returned, never on how much was searched. Most fields cost $0.001 per value returned, and a field with no value is not billed. The program above selects six fields for one home, so a lookup costs a little over half a cent at most.
Add price_quote: "true" to the parameters of any request and the API returns the cost of that request for free, with no data. Every answer also reports its own cost in cost_cents. Pay as you go has a $99 monthly minimum in United States dollars, and every account sets its own maximum monthly spend. The pricing page lists the rates, and the field reference lists the more than 450 fields you can select.
Get started
Get a key and run the program: the quick start guide takes a few minutes.
