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 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