npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

htag-sdk

v2.0.0

Published

Official TypeScript SDK for HtAG Location Intelligence APIs — address search, property data, and market analytics for Australia

Readme

htag-sdk

The official TypeScript SDK for the HtAG Location Intelligence API.

Provides typed access to Australian address data, property valuations, sales records, and market analytics. Zero runtime dependencies -- uses native fetch.

import { HtAgApiClient } from 'htag-sdk';

const client = new HtAgApiClient({
  apiKey: process.env.HTAG_API_KEY!,
  environment: 'prod',
});

const results = await client.address.geocode({ address: '100 George St Sydney' });
for (const r of results.results) {
  console.log(`${r.address_label}  (${r.address_key})`);
}

Migrating to 2.0.0

2.0.0 is a major release: public bedroom-filter types are narrower and unsupported inputs can now fail locally before a request is sent. Review the migration changes below before upgrading from 1.9.6.

Breaking changes in this package

| Change | 1.9.6 | 2.0.0 | What to do | |--------|-------|-------|------------| | bedrooms type on public bedroom-filtered trends and markets.summary | string \| string[] | BedroomsFilter ('All' \| '1'…'5' \| (string & {})) | Pass a scalar value. Arrays no longer type-check; a singleton array is only accepted and unwrapped at runtime. | | markets.trends.price/rent/yieldHistory/yearsToOwn, markets.summary with two bedroom values | issued a comma-joined value, not two independently applied bedroom filters | throws InvalidParameterError before sending | Issue one request per bedroom count and combine the results. | | markets.trends.demandProfile({ propertyType }) (also periodEndMin / periodEndMax) | issued a request; the API did not apply the filter | throws InvalidParameterError before sending | Remove the unsupported parameter; it does not provide server-side filtering. | | intentHub.listEventTypes({ category }), internal.address.insights({ streetLocPid }) | silently ignored by the API | throws InvalidParameterError | Drop the parameter. |

InvalidParameterError is exported and extends the SDK's HtAgError, so it is catchable alongside the other typed errors.

Request and page-size changes

  • Sold-search filter names are corrected in this package. property.soldSearch sends the API's canonical camelCase filter names, including startDate, endDate, saleValueMin and saleValueMax. Public SDK parameter names are unchanged. address_key stays snake_case; rented-search and other endpoint mappings are unchanged by this correction.
  • Array parameters are sent as repeated keys, not comma-joined values. areaId: ['A', 'B'] becomes area_id=A&area_id=B. Commas inside a value are preserved.
  • mbCategory2021 on address geocoding is sent as mb_category_2021.
  • Pagination is controlled by the server unless explicitly supplied. TypeScript 1.9.6 and 2.0.0 both omit a client-side default limit on rented search. The reviewed server contract defaults to 100; set limit explicitly when your application needs a particular page size.

Compatibility and verification scope

The sold wire correction targets both the pre-alias route source 6d549400 and current-main route source 9877c67e: both declare the canonical camelCase filter names. This removes the package's dependency on the newer server aliases. Local real-handler replay tests cover these two source baselines; they are not authenticated live DEV or production tests, and 6d549400 is not a verified production API image identifier.

Compatibility checks also cover a captured set of valid calls from the published 1.9.6 npm and PyPI packages. Those tested calls are not rejected by the newer unknown-parameter guard. This is not a guarantee for every 1.9.6 input, older versions, or custom HTTP clients. Existing installed clients do not receive this package correction until upgraded.

Server-side changes are separate

PR #381 adds sold-search snake_case aliases and rejects unknown query names on both sold and rented search. Its recorded deployment is DEV-only; installing this package does not deploy these server changes or establish their live status. The sold wire correction above works with either reviewed route version.

  • The pre-alias sold route declares camelCase filters; undeclared snake_case filters can be ignored. The newer route accepts both and prefers camelCase when both are supplied.
  • The reviewed rented route already accepts both naming styles. It is still affected by the newer unknown-parameter validation, like sold search.
  • With the newer validation, unknown names return HTTP 400 before repository work. Without it, an undeclared name can be ignored. Check your target environment's API contract before relying on this server-side validation.

Known limitations

  • own_status on address keys is separate work (PR #265), not part of this release.
  • The reviewed API accepts limit and offset on six reference concordance *-to-h3 routes; those parameters were absent from the reviewed spec.
  • Client-side refusals of unsupported filters are package behavior, not evidence of a server deployment. No live test or registry-publication outcome is implied by these migration notes.

Installation

npm install htag-sdk

Or with your preferred package manager:

yarn add htag-sdk
pnpm add htag-sdk

Requires Node.js 18+ (for native fetch).

Quick Start

1. Get an API Key

Sign up at developer.htagai.com and create an API key from the Settings page.

2. Create a Client

import { HtAgApiClient } from 'htag-sdk';

const client = new HtAgApiClient({
  apiKey: 'sk-org--your-org-id-your-key-value',
  environment: 'prod',   // 'dev' or 'prod'
});

Or use a custom base URL:

const client = new HtAgApiClient({
  apiKey: 'sk-...',
  baseUrl: 'https://api.staging.htagai.com',
});

3. Make Requests

// Geocode an address
const results = await client.address.geocode({ address: '15 Miranda Court Noble Park' });
console.log(`${results.total} matches`);

// Get property estimates
const est = await client.property.estimates({
  addressKey: '15MIRANDACOURTNOBLEPARKvic3174',
});
for (const record of est.results) {
  console.log(`Price estimate: $${record.price_estimate?.toLocaleString()}`);
  console.log(`Last sold: $${record.last_sold_price?.toLocaleString()} on ${record.last_sold_date}`);
}

Usage

Address Geocode

Resolve a free-text address to structured location data with geographic identifiers.

const results = await client.address.geocode({
  address: '100 Hickox St Traralgon',
  limit: 5,         // max results (1 - 50)
});

for (const match of results.results) {
  console.log(match.address_label);
  console.log(`  Key: ${match.address_key}`);
  console.log(`  Location: ${match.lat}, ${match.lon}`);
}

Address Standardisation

Standardise raw address strings into structured, canonical components.

const result = await client.address.standardise({
  addresses: [
    '12 / 100-102 HICKOX STR TRARALGON, VIC 3844',
    '15a smith st fitzroy vic 3065',
  ],
});

for (const item of result.results) {
  if (item.error) {
    console.log(`Failed: ${item.input_address} -- ${item.error}`);
  } else {
    const addr = item.standardised_address!;
    console.log(item.input_address);
    console.log(`  -> ${addr.street_number} ${addr.street_name} ${addr.street_type}`);
    console.log(`     ${addr.suburb_or_locality} ${addr.state} ${addr.postcode}`);
    console.log(`  Key: ${item.address_key}`);
  }
}

Address Environment

Retrieve environmental risk data for an address including flood, bushfire, heritage, and zoning.

const env = await client.address.environment({
  address: '15 Miranda Court, Noble Park VIC 3174',
});
for (const record of env.results) {
  console.log(`Bushfire: ${record.bushfire}, Flood: ${record.flood}`);
  console.log(`Heritage: ${record.heritage}, Zoning: ${record.zoning}`);
}

Address Demographics

Retrieve socio-economic indices (SEIFA) and housing tenure data.

const demo = await client.address.demographics({
  address: '15 Miranda Court, Noble Park VIC 3174',
});
for (const record of demo.results) {
  console.log(`IRSAD: ${record.IRSAD}, IER: ${record.IER}`);
}

Property Summary

Retrieve physical property attributes for an address.

const summary = await client.property.summary({
  addressKey: '100102HICKOXSTREETTRARALGONVIC3844',
});
for (const record of summary.results) {
  console.log(`Type: ${record.property_type}`);
  console.log(`Beds: ${record.beds}, Baths: ${record.baths}, Parking: ${record.parking}`);
  console.log(`Land: ${record.lot_size} sqm, Floor: ${record.floor_area} sqm`);
}

Property Estimates

Retrieve valuation estimates and transaction history for an address.

const est = await client.property.estimates({
  addressKey: '100102HICKOXSTREETTRARALGONVIC3844',
});
for (const record of est.results) {
  console.log(`Price estimate: $${record.price_estimate?.toLocaleString()}`);
  console.log(`Rent estimate: $${record.rent_estimate}/wk`);
  console.log(`Last sold: $${record.last_sold_price?.toLocaleString()} on ${record.last_sold_date}`);
}

Property Market

Retrieve market position indicators for an address.

const mkt = await client.property.market({
  addressKey: '100102HICKOXSTREETTRARALGONVIC3844',
});
for (const record of mkt.results) {
  console.log(`Rental %: ${record.rental_percentage}`);
  console.log(`Years to own: ${record.years_to_own}`);
  console.log(`Hold period: ${record.hold_period} years`);
}

Sold Property Search

Search for recently sold properties near an address or coordinates.

const sold = await client.property.soldSearch({
  address: '100 George St, Sydney NSW 2000',
  radius: 2000,              // metres
  propertyType: 'house',
  saleValueMin: 500_000,
  saleValueMax: 2_000_000,
  bedroomsMin: 3,
  startDate: '2024-01-01',
});

console.log(`${sold.total} properties found`);
for (const prop of sold.results) {
  const price = prop.sold_price ? `$${prop.sold_price.toLocaleString()}` : 'undisclosed';
  console.log(`  ${prop.street_address}, ${prop.suburb} -- ${price} (${prop.sold_date})`);
}

All filter parameters are optional:

| Parameter | Type | Description | |-----------|------|-------------| | address | string | Free-text address to centre the search on | | addressKey | string | GNAF address key | | lat, lon | number | Coordinates for point-based search | | radius | number | Search radius in metres (default 2000, max 5000) | | proximity | string | 'any', 'sameStreet', or 'sameSuburb' | | propertyType | string | 'house', 'unit', 'townhouse', 'land', 'rural' | | saleValueMin, saleValueMax | number | Price range filter (AUD) | | bedroomsMin, bedroomsMax | number | Bedroom count range | | bathroomsMin, bathroomsMax | number | Bathroom count range | | carSpacesMin, carSpacesMax | number | Car space range | | startDate, endDate | string | Date range (ISO 8601, e.g. '2024-01-01') | | landAreaMin, landAreaMax | number | Land area in sqm | | limit | number | Rows per page (1-1000). Omitted → server default 100 | | offset | number | Rows to skip before limit (>= 0). Omitted → 0 |

Sold-search filters are sent using the API's canonical camelCase names. The public TypeScript parameter names above are unchanged; address_key remains snake_case.

Pagination

limit and offset behave identically on soldSearch() and rentedSearch(). Both default to limit=100, offset=0 on the server — the SDK sends nothing when you omit them, so you always get the documented behaviour. offset is a row offset applied after ordering, not a page number: { limit: 25, offset: 50 } returns rows 51-75.

total is the number of rows in the page you just received — not the count of all matching records — and it is the billable row count, so rows skipped by offset are never charged.

const limit = 100;
let offset = 0;

for (;;) {
  const page = await client.property.soldSearch({
    addressKey: '100102HICKOXSTREETTRARALGONVIC3844',
    limit,
    offset,
  });
  for (const prop of page.results) {
    // ...
  }
  if (page.total < limit) break;   // short page → last page
  offset += limit;
}

Changed in this release: the server default for limit on both searches is now 100 (previously 500 on rented search, and offset was ignored entirely on sold search). Pass limit: 500 explicitly if you relied on the old page size.

Market Summary

Get headline market metrics at suburb or LGA level.

const summary = await client.markets.summary({
  level: 'suburb',
  areaId: ['SAL10001'],
  propertyType: ['house'],
});

for (const record of summary.results) {
  console.log(`${record.suburb} (${record.state_name})`);
  console.log(`  Typical price: $${record.typical_price?.toLocaleString()}`);
  console.log(`  Rent: $${record.rent}/wk`);
}

Market Growth

Retrieve cumulative or annualised growth rates for price, rent, and yield.

const growth = await client.markets.growthCumulative({
  level: 'suburb',
  areaId: ['SAL10001'],
  propertyType: ['house'],
});

for (const record of growth.results) {
  console.log(`1Y price growth: ${record.one_y_price_growth}`);
  console.log(`5Y price growth: ${record.five_y_price_growth}`);
}

Market Trends

Access historical trend data via client.markets.trends. All trend methods share the same parameter signature:

// Price history
const prices = await client.markets.trends.price({
  level: 'suburb',
  areaId: ['SAL10001'],
  propertyType: ['house'],
  periodEndMin: '2020-01-01',
  limit: 50,
});
for (const p of prices.results) {
  console.log(`${p.period_end}: $${p.typical_price?.toLocaleString()} (${p.sales} sales)`);
}

// Rent history
const rents = await client.markets.trends.rent({
  level: 'suburb', areaId: ['SAL10001'],
});

// Yield history
const yields = await client.markets.trends.yieldHistory({
  level: 'suburb', areaId: ['SAL10001'],
});

// Search interest index (buy/rent search indices)
const search = await client.markets.trends.searchIndex({
  level: 'suburb', areaId: ['SAL10001'],
});

// Hold period
const hold = await client.markets.trends.holdPeriod({
  level: 'suburb', areaId: ['SAL10001'],
});

// Growth rates (price, rent, yield changes)
const growth = await client.markets.trends.growthRates({
  level: 'suburb', areaId: ['SAL10001'],
});

// Demand profile (sales by dwelling type and bedrooms)
const demand = await client.markets.trends.demandProfile({
  level: 'suburb', areaId: ['SAL10001'],
});

// Stock on market
const som = await client.markets.trends.stockOnMarket({
  level: 'suburb', areaId: ['SAL10001'],
});

// Days on market
const dom = await client.markets.trends.daysOnMarket({
  level: 'suburb', areaId: ['SAL10001'],
});

// Clearance rate
const cr = await client.markets.trends.clearanceRate({
  level: 'suburb', areaId: ['SAL10001'],
});

// Vacancy rate
const vac = await client.markets.trends.vacancy({
  level: 'suburb', areaId: ['SAL10001'],
});

Common trend parameters:

| Parameter | Type | Description | |-----------|------|-------------| | level | 'suburb' | 'lga' | Geographic level (required) | | areaId | string[] | Area identifiers (required) | | propertyType | string[] | ['house'], ['unit'], etc. | | periodEndMin | string | Filter from this date | | periodEndMax | string | Filter up to this date | | bedrooms | string | string[] | Bedroom filter | | limit | number | Max results (default 100, max 1000) | | offset | number | Pagination offset |

Internal API

Some endpoints require the internal_api scope on your API key. These are accessed via the client.internal namespace:

// Address search (trigram similarity matching)
const results = await client.internal.address.search({ q: '100 George St Sydney' });

// Address insights (enriched address data)
const insights = await client.internal.address.insights({
  address: '15 Miranda Court, Noble Park VIC 3174',
});

// Automated Valuation Model (batch, up to 50 properties)
const avm = await client.internal.property.avm({
  addressKey: ['100102HICKOXSTREETTRARALGONVIC3844'],
});

// Market snapshots with filtering
const snapshots = await client.internal.markets.snapshots({
  level: 'suburb',
  propertyType: ['house'],
  areaId: ['SAL10001'],
});

// Advanced market query with logical filters
const queryResults = await client.internal.markets.query({
  level: 'suburb',
  mode: 'search',
  property_types: ['house'],
  typical_price_min: 500_000,
  logic: {
    and: [
      { field: 'one_y_price_growth', gte: 0.05 },
      { field: 'vacancy_rate', lte: 0.03 },
    ],
  },
});

// Internal trend endpoints
const supply = await client.internal.markets.trends.supplyDemand({
  level: 'suburb', areaId: ['SAL10001'],
});
const perf = await client.internal.markets.trends.performance({
  level: 'suburb', areaId: ['SAL10001'],
});

If you call an internal method without the required scope, the API will return a 403 error.

Request Cancellation

All methods accept an AbortSignal for cancellation:

const controller = new AbortController();

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);

try {
  const results = await client.address.geocode({
    address: '100 George St',
    signal: controller.signal,
  });
} catch (err) {
  if (err instanceof HtAgError && err.message === 'Request aborted') {
    console.log('Request was cancelled');
  }
}

Parameter Semantics

Behaviour that is easy to get wrong, and what this SDK does about it.

List parameters are sent as repeated keys

{ areaId: ['VIC3121', 'VIC3141'] } goes on the wire as ?area_id=VIC3121&area_id=VIC3141. The checked 1.9.6 package comma-joined multiple values, which could be interpreted as one identifier rather than separate filters.

Commas inside a value are preserved, which matters for free-text parameters such as the internal AVM address list.

bedrooms is a single value on the public API

markets.trends.price / rent / yieldHistory / yearsToOwn and markets.summary filter on one bedroom count. Arrays no longer type-check. At runtime, a list with multiple values raises InvalidParameterError before the request is sent:

await client.markets.trends.price({ level: 'suburb', areaId: ['SAL10001'], bedrooms: '3' }); // ok
await client.markets.trends.price({ ...same, bedrooms: ['3', '4'] });                        // throws

Issue one request per bedroom count and combine the results. A one-element array is still accepted and unwrapped at runtime, but typed callers must pass a scalar. The internal trend endpoints genuinely do accept a list and still take string[].

Filters the API does not implement

The checked older SDK accepted these filters, but the reviewed API routes do not implement them. They now raise InvalidParameterError before any request:

| Method | Parameter | Why | |--------|-----------|-----| | markets.trends.demandProfile | propertyType, periodEndMin, periodEndMax | the endpoint filters on level, areaId, limit, offset only | | intentHub.listEventTypes | category | the route declares no query parameters | | internal.address.insights | streetLocPid | the route accepts address, addressKeys, legalParcelId, mbCategory2021 |

Omitting them, or passing undefined / [], is still a no-op.

Sold and rented search

These rules describe the reviewed API source contract; package installation is not a server deployment. See verification scope.

  • property.soldSearch sends canonical camelCase sold filters; address_key remains snake_case. The newer server additionally accepts snake_case aliases, with explicit camelCase values (including false and zero) taking precedence. Rented search accepts both spellings in both reviewed route versions.
  • Omitting startDate applies a default window of 90 days before the effective endDate. It is not a maximum lookback; an explicit earlier date is passed through for server-side filtering.
  • Sale price bounds are inclusive and exclude unknown prices. includeSaleValueUnknown applies only when no price bound is set.
  • The newer server rejects unknown query names on both searches with HTTP 400 unknown_query_parameters, before repository work. For example, bedrooms=3 is not a sold-search filter; use bedroomsMin/bedroomsMax. Typoed names are not automatically corrected. Older route versions may silently ignore unknown names. This server behavior is separate from the SDK's client-side validation.
  • total counts rows in the returned page, not all matches. Page until total < limit, using the same explicit limit and filters for each request.

Staying on an older release

Upgrading to 2.0.0 applies the sold wire correction. If you need direct REST instead, use canonical camelCase sold filter names, accepted by both reviewed source versions. For example, this is the request path and query (authentication must be configured separately in your client):

/v1/property/sold/search?address_key=AK&startDate=2025-09-10&endDate=2026-08-26&saleValueMin=600000&propertyType=house&propertyType=unit

Repeat list parameters rather than comma-joining them. Do not assume a naming style supported by one endpoint is accepted by every endpoint.

Error Handling

The SDK raises typed errors for API failures:

import {
  HtAgApiClient,
  HtAgError,
  AuthenticationError,
  RateLimitError,
  ValidationError,
  ServerError,
} from 'htag-sdk';

const client = new HtAgApiClient({ apiKey: 'sk-...' });

try {
  const results = await client.address.geocode({ address: 'Syd' });
} catch (err) {
  if (err instanceof AuthenticationError) {
    // 401 or 403 -- bad API key or insufficient scope
    console.error(`Auth failed (HTTP ${err.status})`);
  } else if (err instanceof RateLimitError) {
    // 429 -- throttled (after exhausting retries)
    console.error('Rate limited, try again later');
  } else if (err instanceof ValidationError) {
    // 400 or 422 -- bad request params
    console.error(`Invalid request: ${err.message}`);
    console.error('Details:', err.body);
  } else if (err instanceof ServerError) {
    // 5xx -- upstream failure (after exhausting retries)
    console.error(`Server error (HTTP ${err.status})`);
  } else if (err instanceof HtAgError) {
    // Network/timeout/other
    console.error(`Request failed: ${err.message}`);
  }
}

All errors carry:

  • message -- human-readable description
  • status -- HTTP status code (if applicable)
  • body -- raw response body
  • url -- the request URL that failed
  • cause -- the underlying error (for network failures)

Retries

The SDK automatically retries transient failures:

  • Retried statuses: 429, 500, 502, 503, 504
  • Network errors: connection failures, timeouts
  • Max retries: 3 (configurable)
  • Backoff: exponential (0.5s base, 2x multiplier, random jitter)

Configure retry behaviour:

const client = new HtAgApiClient({
  apiKey: 'sk-...',
  maxRetries: 5,       // default is 3
  timeout: 120_000,    // request timeout in ms (default 30000)
});

Configuration Reference

| Option | Type | Default | Description | |--------|------|---------|-------------| | apiKey | string | required | Your HtAG API key | | environment | 'dev' | 'prod' | 'prod' | API environment | | baseUrl | string | -- | Custom base URL (overrides environment) | | timeout | number | 30000 | Request timeout in milliseconds | | maxRetries | number | 3 | Maximum retry attempts | | retryBaseDelay | number | 500 | Base delay between retries in ms |

CommonJS

The package ships with both ESM and CommonJS builds:

// ESM (recommended)
import { HtAgApiClient } from 'htag-sdk';

// CommonJS
const { HtAgApiClient } = require('htag-sdk');

TypeScript

All types are exported for use in your application:

import type {
  AddressRecord,
  GeocodeRecord,
  SoldPropertyRecord,
  PropertySummaryRecord,
  PropertyEstimatesRecord,
  PropertyMarketRecord,
  MarketSummaryRecord,
  PriceHistoryOut,
  RentHistoryOut,
  BaseResponse,
  LevelEnum,
  PropertyTypeEnum,
} from 'htag-sdk';

Requirements

  • Node.js >= 18 (for native fetch)
  • No runtime dependencies

License

MIT