htag-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +459 -0
- package/dist/index.cjs +510 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +691 -0
- package/dist/index.d.ts +691 -0
- package/dist/index.js +474 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# htag-sdk
|
|
2
|
+
|
|
3
|
+
The official TypeScript SDK for the [HtAG](https://htagai.com) Location Intelligence API.
|
|
4
|
+
|
|
5
|
+
Provides typed access to Australian address data, property sales records, and market analytics. Zero runtime dependencies — uses native `fetch`.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { HtAgApiClient } from 'htag-sdk';
|
|
9
|
+
|
|
10
|
+
const client = new HtAgApiClient({
|
|
11
|
+
apiKey: process.env.HTAG_API_KEY!,
|
|
12
|
+
environment: 'prod',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const results = await client.address.search({ q: '100 George St Sydney' });
|
|
16
|
+
for (const r of results.results) {
|
|
17
|
+
console.log(`${r.address_label} (score ${r.score.toFixed(2)})`);
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install htag-sdk
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or with your preferred package manager:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
yarn add htag-sdk
|
|
31
|
+
pnpm add htag-sdk
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires Node.js 18+ (for native `fetch`).
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
### 1. Get an API Key
|
|
39
|
+
|
|
40
|
+
Sign up at [developer.htagai.com](https://developer.htagai.com) and create an API key from the Settings page.
|
|
41
|
+
|
|
42
|
+
### 2. Create a Client
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { HtAgApiClient } from 'htag-sdk';
|
|
46
|
+
|
|
47
|
+
const client = new HtAgApiClient({
|
|
48
|
+
apiKey: 'sk-org--your-org-id-your-key-value',
|
|
49
|
+
environment: 'prod', // 'dev' or 'prod'
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or use a custom base URL:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
const client = new HtAgApiClient({
|
|
57
|
+
apiKey: 'sk-...',
|
|
58
|
+
baseUrl: 'https://api.staging.htagai.com',
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. Make Requests
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// Search for addresses
|
|
66
|
+
const results = await client.address.search({ q: '15 Miranda Court Noble Park' });
|
|
67
|
+
console.log(`${results.total} matches`);
|
|
68
|
+
|
|
69
|
+
// Get insights for an address
|
|
70
|
+
const insights = await client.address.insights({
|
|
71
|
+
address: '15 Miranda Court, Noble Park VIC 3174',
|
|
72
|
+
});
|
|
73
|
+
for (const record of insights.results) {
|
|
74
|
+
console.log(`Bushfire: ${record.bushfire}, Flood: ${record.flood}`);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
### Address Search
|
|
81
|
+
|
|
82
|
+
Find addresses by free-text query with fuzzy matching.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
const results = await client.address.search({
|
|
86
|
+
q: '100 Hickox St Traralgon',
|
|
87
|
+
threshold: 0.3, // minimum match score (0.1 - 1.0)
|
|
88
|
+
limit: 5, // max results (1 - 50)
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
for (const match of results.results) {
|
|
92
|
+
console.log(match.address_label);
|
|
93
|
+
console.log(` Key: ${match.address_key}`);
|
|
94
|
+
console.log(` Score: ${match.score.toFixed(2)}`);
|
|
95
|
+
console.log(` Location: ${match.lat}, ${match.lon}`);
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Address Insights
|
|
100
|
+
|
|
101
|
+
Retrieve enriched data for addresses including risk flags, SEIFA scores, and zoning.
|
|
102
|
+
|
|
103
|
+
Provide exactly one of `address`, `addressKeys`, or `legalParcelId`:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// By address string
|
|
107
|
+
const insights = await client.address.insights({
|
|
108
|
+
address: '15 Miranda Court, Noble Park VIC 3174',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// By GNAF address keys (up to 50)
|
|
112
|
+
const insights2 = await client.address.insights({
|
|
113
|
+
addressKeys: ['100102HICKOXSTREETTRARALGONVIC3844'],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// By legal parcel ID
|
|
117
|
+
const insights3 = await client.address.insights({
|
|
118
|
+
legalParcelId: '2\\TP574754',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
for (const record of insights.results) {
|
|
122
|
+
console.log(`Address: ${record.address_label}`);
|
|
123
|
+
console.log(` Bushfire risk: ${record.bushfire}`);
|
|
124
|
+
console.log(` Flood risk: ${record.flood}`);
|
|
125
|
+
console.log(` Heritage: ${record.heritage}`);
|
|
126
|
+
console.log(` SEIFA (IRSAD): ${record.IRSAD}`);
|
|
127
|
+
console.log(` Zoning: ${record.zoning}`);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Address Standardisation
|
|
132
|
+
|
|
133
|
+
Standardise raw address strings into structured, canonical components.
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const result = await client.address.standardise({
|
|
137
|
+
addresses: [
|
|
138
|
+
'12 / 100-102 HICKOX STR TRARALGON, VIC 3844',
|
|
139
|
+
'15a smith st fitzroy vic 3065',
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
for (const item of result.results) {
|
|
144
|
+
if (item.error) {
|
|
145
|
+
console.log(`Failed: ${item.input_address} — ${item.error}`);
|
|
146
|
+
} else {
|
|
147
|
+
const addr = item.standardised_address!;
|
|
148
|
+
console.log(item.input_address);
|
|
149
|
+
console.log(` -> ${addr.street_number} ${addr.street_name} ${addr.street_type}`);
|
|
150
|
+
console.log(` ${addr.suburb_or_locality} ${addr.state} ${addr.postcode}`);
|
|
151
|
+
console.log(` Key: ${item.address_key}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Sold Property Search
|
|
157
|
+
|
|
158
|
+
Search for recently sold properties near an address or coordinates.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
const sold = await client.property.soldSearch({
|
|
162
|
+
address: '100 George St, Sydney NSW 2000',
|
|
163
|
+
radius: 2000, // metres
|
|
164
|
+
propertyType: 'house',
|
|
165
|
+
saleValueMin: 500_000,
|
|
166
|
+
saleValueMax: 2_000_000,
|
|
167
|
+
bedroomsMin: 3,
|
|
168
|
+
startDate: '2024-01-01',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
console.log(`${sold.total} properties found`);
|
|
172
|
+
for (const prop of sold.results) {
|
|
173
|
+
const price = prop.sold_price ? `$${prop.sold_price.toLocaleString()}` : 'undisclosed';
|
|
174
|
+
console.log(` ${prop.street_address}, ${prop.suburb} — ${price} (${prop.sold_date})`);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
All filter parameters are optional:
|
|
179
|
+
|
|
180
|
+
| Parameter | Type | Description |
|
|
181
|
+
|-----------|------|-------------|
|
|
182
|
+
| `address` | string | Free-text address to centre the search on |
|
|
183
|
+
| `addressKey` | string | GNAF address key |
|
|
184
|
+
| `lat`, `lon` | number | Coordinates for point-based search |
|
|
185
|
+
| `radius` | number | Search radius in metres (default 2000, max 5000) |
|
|
186
|
+
| `proximity` | string | `'any'`, `'sameStreet'`, or `'sameSuburb'` |
|
|
187
|
+
| `propertyType` | string | `'house'`, `'unit'`, `'townhouse'`, `'land'`, `'rural'` |
|
|
188
|
+
| `saleValueMin`, `saleValueMax` | number | Price range filter (AUD) |
|
|
189
|
+
| `bedroomsMin`, `bedroomsMax` | number | Bedroom count range |
|
|
190
|
+
| `bathroomsMin`, `bathroomsMax` | number | Bathroom count range |
|
|
191
|
+
| `carSpacesMin`, `carSpacesMax` | number | Car space range |
|
|
192
|
+
| `startDate`, `endDate` | string | Date range (ISO 8601, e.g. `'2024-01-01'`) |
|
|
193
|
+
| `landAreaMin`, `landAreaMax` | number | Land area in sqm |
|
|
194
|
+
|
|
195
|
+
> Parameters use camelCase in TypeScript and are automatically converted to snake_case for the API.
|
|
196
|
+
|
|
197
|
+
### Market Snapshots
|
|
198
|
+
|
|
199
|
+
Get current market metrics at suburb or LGA level.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const snapshots = await client.markets.snapshots({
|
|
203
|
+
level: 'suburb',
|
|
204
|
+
propertyType: ['house'],
|
|
205
|
+
areaId: ['SAL10001'],
|
|
206
|
+
limit: 10,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
for (const snap of snapshots.results) {
|
|
210
|
+
console.log(`${snap.suburb} (${snap.state_name})`);
|
|
211
|
+
console.log(` Typical price: $${snap.typical_price?.toLocaleString()}`);
|
|
212
|
+
console.log(` Rent: $${snap.rent}/wk`);
|
|
213
|
+
if (snap.yield != null) console.log(` Yield: ${(snap.yield * 100).toFixed(1)}%`);
|
|
214
|
+
if (snap.one_y_price_growth != null) {
|
|
215
|
+
console.log(` 1Y growth: ${(snap.one_y_price_growth * 100).toFixed(1)}%`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Market Query (Advanced)
|
|
221
|
+
|
|
222
|
+
Run complex market searches with filter logic using AND/OR/NOT operators.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
const results = await client.markets.query({
|
|
226
|
+
level: 'suburb',
|
|
227
|
+
mode: 'search',
|
|
228
|
+
property_types: ['house'],
|
|
229
|
+
typical_price_min: 500_000,
|
|
230
|
+
typical_price_max: 1_500_000,
|
|
231
|
+
logic: {
|
|
232
|
+
and: [
|
|
233
|
+
{ field: 'one_y_price_growth', gte: 0.05 },
|
|
234
|
+
{ field: 'vacancy_rate', lte: 0.03 },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
limit: 20,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
for (const snap of results.results) {
|
|
241
|
+
console.log(`${snap.suburb}: $${snap.typical_price?.toLocaleString()}`);
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Market Trends
|
|
246
|
+
|
|
247
|
+
Access historical trend data via `client.markets.trends`. All trend methods share the same parameter signature:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// Price history
|
|
251
|
+
const prices = await client.markets.trends.price({
|
|
252
|
+
level: 'suburb',
|
|
253
|
+
areaId: ['SAL10001'],
|
|
254
|
+
propertyType: ['house'],
|
|
255
|
+
periodEndMin: '2020-01-01',
|
|
256
|
+
limit: 50,
|
|
257
|
+
});
|
|
258
|
+
for (const p of prices.results) {
|
|
259
|
+
console.log(`${p.period_end}: $${p.typical_price?.toLocaleString()} (${p.sales} sales)`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Rent history
|
|
263
|
+
const rents = await client.markets.trends.rent({
|
|
264
|
+
level: 'suburb',
|
|
265
|
+
areaId: ['SAL10001'],
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Yield history
|
|
269
|
+
const yields = await client.markets.trends.yieldHistory({
|
|
270
|
+
level: 'suburb',
|
|
271
|
+
areaId: ['SAL10001'],
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Supply & demand (inventory, vacancies, clearance rate)
|
|
275
|
+
const supply = await client.markets.trends.supplyDemand({
|
|
276
|
+
level: 'suburb',
|
|
277
|
+
areaId: ['SAL10001'],
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Search interest index (buy/rent search indices)
|
|
281
|
+
const search = await client.markets.trends.searchIndex({
|
|
282
|
+
level: 'suburb',
|
|
283
|
+
areaId: ['SAL10001'],
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Hold period
|
|
287
|
+
const hold = await client.markets.trends.holdPeriod({
|
|
288
|
+
level: 'suburb',
|
|
289
|
+
areaId: ['SAL10001'],
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Performance essentials (price, rent, sales, rentals, yield)
|
|
293
|
+
const perf = await client.markets.trends.performance({
|
|
294
|
+
level: 'suburb',
|
|
295
|
+
areaId: ['SAL10001'],
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Growth rates (price, rent, yield changes)
|
|
299
|
+
const growth = await client.markets.trends.growthRates({
|
|
300
|
+
level: 'suburb',
|
|
301
|
+
areaId: ['SAL10001'],
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Demand profile (sales by dwelling type and bedrooms)
|
|
305
|
+
const demand = await client.markets.trends.demandProfile({
|
|
306
|
+
level: 'suburb',
|
|
307
|
+
areaId: ['SAL10001'],
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Common trend parameters:
|
|
312
|
+
|
|
313
|
+
| Parameter | Type | Description |
|
|
314
|
+
|-----------|------|-------------|
|
|
315
|
+
| `level` | `'suburb'` \| `'lga'` | Geographic level (required) |
|
|
316
|
+
| `areaId` | string[] | Area identifiers (required) |
|
|
317
|
+
| `propertyType` | string[] | `['house']`, `['unit']`, etc. |
|
|
318
|
+
| `periodEndMin` | string | Filter from this date |
|
|
319
|
+
| `periodEndMax` | string | Filter up to this date |
|
|
320
|
+
| `bedrooms` | string \| string[] | Bedroom filter |
|
|
321
|
+
| `limit` | number | Max results (default 100, max 1000) |
|
|
322
|
+
| `offset` | number | Pagination offset |
|
|
323
|
+
|
|
324
|
+
## Request Cancellation
|
|
325
|
+
|
|
326
|
+
All methods accept an `AbortSignal` for cancellation:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
const controller = new AbortController();
|
|
330
|
+
|
|
331
|
+
// Cancel after 5 seconds
|
|
332
|
+
setTimeout(() => controller.abort(), 5000);
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const results = await client.address.search({
|
|
336
|
+
q: '100 George St',
|
|
337
|
+
signal: controller.signal,
|
|
338
|
+
});
|
|
339
|
+
} catch (err) {
|
|
340
|
+
if (err instanceof HtAgError && err.message === 'Request aborted') {
|
|
341
|
+
console.log('Request was cancelled');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Error Handling
|
|
347
|
+
|
|
348
|
+
The SDK raises typed errors for API failures:
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import {
|
|
352
|
+
HtAgApiClient,
|
|
353
|
+
HtAgError,
|
|
354
|
+
AuthenticationError,
|
|
355
|
+
RateLimitError,
|
|
356
|
+
ValidationError,
|
|
357
|
+
ServerError,
|
|
358
|
+
} from 'htag-sdk';
|
|
359
|
+
|
|
360
|
+
const client = new HtAgApiClient({ apiKey: 'sk-...' });
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const results = await client.address.search({ q: 'Syd' });
|
|
364
|
+
} catch (err) {
|
|
365
|
+
if (err instanceof AuthenticationError) {
|
|
366
|
+
// 401 or 403 — bad API key
|
|
367
|
+
console.error(`Auth failed (HTTP ${err.status})`);
|
|
368
|
+
} else if (err instanceof RateLimitError) {
|
|
369
|
+
// 429 — throttled (after exhausting retries)
|
|
370
|
+
console.error('Rate limited, try again later');
|
|
371
|
+
} else if (err instanceof ValidationError) {
|
|
372
|
+
// 400 or 422 — bad request params
|
|
373
|
+
console.error(`Invalid request: ${err.message}`);
|
|
374
|
+
console.error('Details:', err.body);
|
|
375
|
+
} else if (err instanceof ServerError) {
|
|
376
|
+
// 5xx — upstream failure (after exhausting retries)
|
|
377
|
+
console.error(`Server error (HTTP ${err.status})`);
|
|
378
|
+
} else if (err instanceof HtAgError) {
|
|
379
|
+
// Network/timeout/other
|
|
380
|
+
console.error(`Request failed: ${err.message}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
All errors carry:
|
|
386
|
+
- `message` — human-readable description
|
|
387
|
+
- `status` — HTTP status code (if applicable)
|
|
388
|
+
- `body` — raw response body
|
|
389
|
+
- `url` — the request URL that failed
|
|
390
|
+
- `cause` — the underlying error (for network failures)
|
|
391
|
+
|
|
392
|
+
## Retries
|
|
393
|
+
|
|
394
|
+
The SDK automatically retries transient failures:
|
|
395
|
+
|
|
396
|
+
- **Retried statuses**: 429, 500, 502, 503, 504
|
|
397
|
+
- **Network errors**: connection failures, timeouts
|
|
398
|
+
- **Max retries**: 3 (configurable)
|
|
399
|
+
- **Backoff**: exponential (0.5s base, 2x multiplier, random jitter)
|
|
400
|
+
|
|
401
|
+
Configure retry behaviour:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
const client = new HtAgApiClient({
|
|
405
|
+
apiKey: 'sk-...',
|
|
406
|
+
maxRetries: 5, // default is 3
|
|
407
|
+
timeout: 120_000, // request timeout in ms (default 30000)
|
|
408
|
+
});
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Configuration Reference
|
|
412
|
+
|
|
413
|
+
| Option | Type | Default | Description |
|
|
414
|
+
|--------|------|---------|-------------|
|
|
415
|
+
| `apiKey` | string | required | Your HtAG API key |
|
|
416
|
+
| `environment` | `'dev'` \| `'prod'` | `'prod'` | API environment |
|
|
417
|
+
| `baseUrl` | string | — | Custom base URL (overrides environment) |
|
|
418
|
+
| `timeout` | number | `30000` | Request timeout in milliseconds |
|
|
419
|
+
| `maxRetries` | number | `3` | Maximum retry attempts |
|
|
420
|
+
| `retryBaseDelay` | number | `500` | Base delay between retries in ms |
|
|
421
|
+
|
|
422
|
+
## CommonJS
|
|
423
|
+
|
|
424
|
+
The package ships with both ESM and CommonJS builds:
|
|
425
|
+
|
|
426
|
+
```javascript
|
|
427
|
+
// ESM (recommended)
|
|
428
|
+
import { HtAgApiClient } from 'htag-sdk';
|
|
429
|
+
|
|
430
|
+
// CommonJS
|
|
431
|
+
const { HtAgApiClient } = require('htag-sdk');
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## TypeScript
|
|
435
|
+
|
|
436
|
+
All types are exported for use in your application:
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
import type {
|
|
440
|
+
AddressRecord,
|
|
441
|
+
AddressSearchResult,
|
|
442
|
+
SoldPropertyRecord,
|
|
443
|
+
MarketSnapshot,
|
|
444
|
+
PriceHistoryOut,
|
|
445
|
+
RentHistoryOut,
|
|
446
|
+
BaseResponse,
|
|
447
|
+
LevelEnum,
|
|
448
|
+
PropertyTypeEnum,
|
|
449
|
+
} from 'htag-sdk';
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## Requirements
|
|
453
|
+
|
|
454
|
+
- Node.js >= 18 (for native `fetch`)
|
|
455
|
+
- No runtime dependencies
|
|
456
|
+
|
|
457
|
+
## License
|
|
458
|
+
|
|
459
|
+
MIT
|