!

Buyer-fit scores for direct mail and lead targeting

Photo of Alex Wilkinson
Alex Wilkinson
CEO of Houski
2026-06-04

The fastest way to waste a direct mail budget is to mail the wrong neighbourhood. A retirement community pitch dropped on a street full of young families converts at close to nothing. A private school flyer mailed to a retirement enclave is paper in the recycling. Every marketer running property-based campaigns in Canada knows this, and most still target on postal codes and gut feel because the data to do better has been hard to get.

Houski carries four buyer-fit scores on every property that map directly onto the segments marketers actually sell to, score_family, score_retirement, score_education, and score_safety. They let you build a mailing list or a lead filter that targets the neighbourhoods that fit the offer, at the address level, across the whole country.

This post walks through what each score measures, how to pull them, how to build a targeted list, and how lead-generation platforms can score inbound addresses on the fly.

What the buyer-fit scores measure

Each score is an integer from 0 to 10 on every property, describing the area around it. Higher is always better.

  • score_family. How well the area suits families with children. A high score means a neighbourhood where families with children are common.
  • score_retirement. How well the area suits retirees. A high score means a neighbourhood with an older, more settled population.
  • score_education. Access to schools in the area. A high score means more schools within easy reach. It reflects how close and plentiful nearby schools are, not their ratings.
  • score_safety. How safe the area is, based on local crime levels. A high score means a safer neighbourhood.

A point worth keeping in mind for segmentation. score_family and score_retirement are not opposites, but they peak in different places. A suburb full of schools and playgrounds scores high on family and moderate on retirement. A quiet, amenity-rich, low-maintenance enclave scores the reverse. Targeting on the right one of the two is most of the battle.

Because higher is better, the filters read naturally. score_family_gte=8 returns the strongest family neighbourhoods. score_retirement_gte=8 returns the strongest retirement fit. Combine a buyer-fit threshold with score_safety_gte to add a safety floor to any campaign.

Pulling buyer-fit scores for one address

Resolve a street address to a property_id first. This is a live call against the real API, regenerated on every render:

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', '1');
    url.searchParams.set('query', '62+Forest+Manor+Rd+Toronto+ON');

    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": 0.019999999552965164,
  "data": [
    {
      "address": "609 62 Forest Manor Road",
      "property_id": "16da199304dbddb1"
    }
  ],
  "error": "",
  "match_meta": [
    {
      "match_value": 1.0,
      "property_id": "16da199304dbddb1"
    }
  ],
  "price_quote": false,
  "result_total": 1,
  "time_ms": 597
}

Then pull all four buyer-fit scores for that property_id at once:

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('api_key', 'YOUR_API_KEY');
    url.searchParams.set('property_id_eq', '10000a939ca95e87');
    url.searchParams.set('select', 'address,city,score_family,score_retirement,score_education,score_safety');

    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.429999977350235,
  "data": [
    {
      "address": "2202 62 Forest Manor Road",
      "city": "Toronto",
      "property_id": "10000a939ca95e87",
      "score_education": 10,
      "score_family": 10,
      "score_retirement": 9,
      "score_safety": 6
    }
  ],
  "error": "",
  "pagination": {
    "current_page": 1,
    "has_next_page": false,
    "has_previous_page": false,
    "page_total": 1
  },
  "price_quote": false,
  "result_total": 1,
  "time_ms": 47,
  "ui_info": {
    "address": "2202 62 Forest Manor Road",
    "address_link": "ca/on/toronto/henry-farm/2202-62-forest-manor-road",
    "address_slug": "2202-62-forest-manor-road",
    "city": "Toronto",
    "city_id": "6cdbdee2492718ed",
    "city_link": "ca/on/toronto",
    "city_slug": "toronto",
    "community": "Henry Farm",
    "community_id": "b9b28936a3929915",
    "community_link": "ca/on/toronto/henry-farm",
    "community_slug": "henry_farm",
    "country": "Canada",
    "country_abbreviation": "CA",
    "country_abbreviation_id": "9ace2b6431b7f1be",
    "country_abbreviation_link": "ca",
    "country_slug": "canada",
    "property_id": "10000a939ca95e87",
    "province": "Ontario",
    "province_abbreviation": "ON",
    "province_abbreviation_id": "146699ee774499d3",
    "province_abbreviation_link": "ca/on",
    "province_slug": "ontario"
  }
}

Look at how the four scores spread across the response - the point is the shape of the profile, not any single number. Wherever an address lands on each segment, that spread is a profile you can target, not a guess.

Set select to only the scores you use.

Aggregating buyer-fit across a city

The aggregate endpoint returns one number per market, which is how you find the right neighbourhoods before you pull a single address.

Median family fit for Vaughan, a suburb built largely for families:

API request
TypeScript code
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('city', 'Vaughan');
    url.searchParams.set('country_abbreviation', 'ca');
    url.searchParams.set('field', 'score_family');
    url.searchParams.set('province_abbreviation', 'on');

    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);
})();
API response
JSON
{
  "cache_hit": true,
  "cost_cents": 1.0,
  "data": [
    {
      "aggregation": "median",
      "field": "score_family",
      "value": "10"
    }
  ],
  "error": "",
  "price_quote": false,
  "time_ms": 50
}

Median retirement fit for Victoria, a long-standing retirement destination:

API request
TypeScript code
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('city', 'Victoria');
    url.searchParams.set('country_abbreviation', 'ca');
    url.searchParams.set('field', 'score_retirement');
    url.searchParams.set('province_abbreviation', 'bc');

    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);
})();
API response
JSON
{
  "cache_hit": true,
  "cost_cents": 1.0,
  "data": [
    {
      "aggregation": "median",
      "field": "score_retirement",
      "value": "7"
    }
  ],
  "error": "",
  "price_quote": false,
  "time_ms": 49
}

Compare the medians across your candidate markets and the campaign targets pick themselves. The cities that score high on the segment you are selling to are where the mailing list should be dense.

A buyer-fit comparison across markets

Run each score as a median across your candidate markets and you get a segment fingerprint per city. Each cell is the indicative median score from 0 to 10 with a plain read in brackets so the meaning is obvious: (good) is a strong fit, (ok) is middling, (bad) is weak. Run the aggregate calls above for current values. Higher is always better, and numbers shift as the model updates:

CityProvinceFamilyRetirementEducationSafety
VaughanON10 (good)10 (good)8 (good)7 (ok)
OakvilleON9 (good)5 (ok)7 (ok)8 (good)
CalgaryAB8 (good)6 (ok)7 (ok)7 (ok)
MontrealQC5 (ok)4 (bad)9 (good)6 (ok)
KelownaBC6 (ok)9 (good)4 (bad)7 (ok)
VictoriaBC2 (bad)7 (ok)6 (ok)7 (ok)

The spread is the whole point. Victoria scores a 2 on family and a 7 on retirement, the fingerprint of a city built for downsizers where a family offer falls flat. Kelowna pushes retirement to 9 but thins out to a 4 on schools. Montreal runs young and school-dense, top of the set on education and bottom on retirement. Vaughan maxes both family and retirement at 10. A campaign that treats these markets as interchangeable wastes half its spend, and the scores let you send each offer to the market that actually fits it.

Building a targeted mailing list

For a mailing list you usually want the full universe of addresses that fit, not just listed properties, so you leave the active-listing filter off and let the buyer-fit thresholds do the targeting. The property call to pull a family-targeted list in one city looks like this:

API request
Python code
from dataclasses import dataclass
from dataclasses_json import dataclass_json
import requests

params = {}
params['api_key'] = 'YOUR_API_KEY'
params['city'] = 'Vaughan'
params['country_abbreviation'] = 'ca'
params['province_abbreviation'] = 'on'
params['results_per_page'] = '5'
params['score_family_gte'] = '8'
params['score_safety_gte'] = '6'
params['select'] = 'address,city,province_abbreviation'

response = requests.get('https://api.houski.ca/properties', params=params)

if response.status_code == 200:
    json_data = response.json()

    print(json_data)

    # You must copy the PropertiesResponse type declarations from the 
    # Houski API documentation to strongly type the response
    typed_response = PropertiesResponse.from_dict(json_data)

    # Log the response
    print(typed_response)
else:
    print(f'Failed to get data: {response}')
API response
JSON
{
  "cache_hit": true,
  "cost_cents": 0.19999998807907104,
  "data": [
    {
      "address": "70 Gallant Place",
      "city": "Vaughan",
      "property_id": "10001e50d0fd8d30",
      "province_abbreviation": "ON"
    },
    {
      "address": "8270 Islington Avenue",
      "city": "Vaughan",
      "property_id": "1000731c7b3f2976",
      "province_abbreviation": "ON"
    },
    {
      "address": "73 Cartwright Boulevard",
      "city": "Vaughan",
      "property_id": "10007c2d0b0befda",
      "province_abbreviation": "ON"
    },
    {
      "address": "9980 Dufferin Street",
      "city": "Vaughan",
      "property_id": "1001086bb620f29c",
      "province_abbreviation": "ON"
    },
    {
      "address": "811 110 Promenade Circle",
      "city": "Vaughan",
      "property_id": "10010f00dfcde7e0",
      "province_abbreviation": "ON"
    }
  ],
  "error": "",
  "pagination": {
    "current_page": 1,
    "has_next_page": true,
    "has_previous_page": false,
    "page_total": 20556
  },
  "price_quote": false,
  "result_total": 102780,
  "time_ms": 50,
  "ui_info": {
    "city": "Vaughan",
    "city_id": "5b1a2c16d0ccff7d",
    "city_link": "ca/on/vaughan",
    "city_slug": "vaughan",
    "country": "Canada",
    "country_abbreviation": "CA",
    "country_abbreviation_id": "9ace2b6431b7f1be",
    "country_abbreviation_link": "ca",
    "country_slug": "canada",
    "province": "Ontario",
    "province_abbreviation": "ON",
    "province_abbreviation_id": "146699ee774499d3",
    "province_abbreviation_link": "ca/on",
    "province_slug": "ontario"
  }
}

The score_family_gte=8 filter restricts the list to strong family neighbourhoods, and the score_safety_gte=6 adds a safety floor so the campaign avoids the lowest-scoring streets. Page through to assemble the full list, then merge it into your mail house format. Swap the threshold field to score_retirement_gte for a retirement campaign, or stack score_education_gte for a private-school or tutoring offer.

How lead-generation platforms use this

A lead-generation platform can score inbound addresses the moment they arrive rather than mailing blind. When a prospect submits an address, resolve it and pull the buyer-fit scores in two calls, then route the lead to the campaign or the agent that fits the neighbourhood. A high family and education score routes to the family-focused agent, a high retirement score to the downsizing specialist. The scoring adds a fraction of a cent per lead and lifts the relevance of every follow-up.

Limitations to be honest about

  • Each score describes the area, not the household at the address. A family-fit neighbourhood will contain retirees and vice versa. The scores target the area, and direct mail is an area game.
  • They update on a schedule, not in real time. A new school or a new development that changed a neighbourhood's character may not be reflected until the next refresh.
  • Each score is a relative ranking, not a census count. A family 9 is a stronger family fit than a 5. It is not a percentage of families on the street.
  • The scores reflect the present and are built from public, government, and crowdsourced data, so the very latest local shifts may lag.

A full example: a two-segment campaign across a metro

The workflow marketers ask about most is splitting one metro into two campaigns. You sell to both families and downsizers and you want one mailing list for each, with no overlap and a safety floor on both.

Pull the family list and the retirement list for the metro with the threshold filters, then de-duplicate any address that lands on both before you mail. To check a single address a salesperson wants to qualify, resolve it and pull its scores:

API request
Python code
from dataclasses import dataclass
from dataclasses_json import dataclass_json
import requests

params = {}
params['api_key'] = 'YOUR_API_KEY'
params['max_results'] = '1'
params['query'] = '62+Forest+Manor+Rd+Toronto+ON'

response = requests.get('https://api.houski.ca/search', params=params)

if response.status_code == 200:
    json_data = response.json()

    print(json_data)

    # You must copy the SearchResponse type declarations from the 
    # Houski API documentation to strongly type the response
    typed_response = SearchResponse.from_dict(json_data)

    # Log the response
    print(typed_response)
else:
    print(f'Failed to get data: {response}')
API response
JSON
{
  "cache_hit": true,
  "cost_cents": 0.019999999552965164,
  "data": [
    {
      "address": "609 62 Forest Manor Road",
      "property_id": "16da199304dbddb1"
    }
  ],
  "error": "",
  "match_meta": [
    {
      "match_value": 1.0,
      "property_id": "16da199304dbddb1"
    }
  ],
  "price_quote": false,
  "result_total": 1,
  "time_ms": 597
}

And the score pull against the returned property_id:

API request
Python code
from dataclasses import dataclass
from dataclasses_json import dataclass_json
import requests

params = {}
params['api_key'] = 'YOUR_API_KEY'
params['property_id_eq'] = '10000a939ca95e87'
params['select'] = 'score_family,score_retirement,score_education,score_safety'

response = requests.get('https://api.houski.ca/properties', params=params)

if response.status_code == 200:
    json_data = response.json()

    print(json_data)

    # You must copy the PropertiesResponse type declarations from the 
    # Houski API documentation to strongly type the response
    typed_response = PropertiesResponse.from_dict(json_data)

    # Log the response
    print(typed_response)
else:
    print(f'Failed to get data: {response}')
API response
JSON
{
  "cache_hit": true,
  "cost_cents": 0.41999998688697815,
  "data": [
    {
      "address": "2202 62 Forest Manor Road",
      "property_id": "10000a939ca95e87",
      "score_education": 10,
      "score_family": 10,
      "score_retirement": 9,
      "score_safety": 6
    }
  ],
  "error": "",
  "pagination": {
    "current_page": 1,
    "has_next_page": false,
    "has_previous_page": false,
    "page_total": 1
  },
  "price_quote": false,
  "result_total": 1,
  "time_ms": 46,
  "ui_info": {
    "address": "2202 62 Forest Manor Road",
    "address_link": "ca/on/toronto/henry-farm/2202-62-forest-manor-road",
    "address_slug": "2202-62-forest-manor-road",
    "city": "Toronto",
    "city_id": "6cdbdee2492718ed",
    "city_link": "ca/on/toronto",
    "city_slug": "toronto",
    "community": "Henry Farm",
    "community_id": "b9b28936a3929915",
    "community_link": "ca/on/toronto/henry-farm",
    "community_slug": "henry_farm",
    "country": "Canada",
    "country_abbreviation": "CA",
    "country_abbreviation_id": "9ace2b6431b7f1be",
    "country_abbreviation_link": "ca",
    "country_slug": "canada",
    "property_id": "10000a939ca95e87",
    "province": "Ontario",
    "province_abbreviation": "ON",
    "province_abbreviation_id": "146699ee774499d3",
    "province_abbreviation_link": "ca/on",
    "province_slug": "ontario"
  }
}

In your own code, assign each address to the family campaign or the retirement campaign by whichever score is higher, drop anything below your safety floor, and you have two clean, non-overlapping lists from one metro. The scores that built the lists also tell each salesperson why an address landed where it did.

What to build next

Whether you are building a mailing list, splitting a metro into segments, or scoring inbound leads in real time, it starts with one property call or one aggregate call. The fields are documented, the filters compose cleanly, and a full-city pull of address and coordinate fields runs tens of dollars, which the $99 monthly minimum covers.

Get started at /api-documentation/quick-start. Pricing starts at $99 a month, and you can have a targeted list running in your terminal in under five minutes.