How to get property data by address in JavaScript

Photo of Alex Wilkinson
Alex Wilkinson
CEO of Houski
2026-11-13

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:

API request
TypeScript code
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);
})();
API response
JSON
{
  "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.

JavaScript code
// 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:

JavaScript code
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:

API request
TypeScript code
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);
})();
API response
JSON
{
  "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.