@wattleaddr/js
v0.4.0
Published
JavaScript/TypeScript SDK for the WattleAddr address API: Australian address autocomplete and verification, served from Australia. Headless client + drop-in autocomplete widget, zero dependencies.
Maintainers
Readme
@wattleaddr/js
Official JavaScript/TypeScript SDK for the WattleAddr address API. It does Australian address autocomplete and verification, served from Australia on G-NAF.
- Zero dependencies. One small module, ESM + CommonJS, full TypeScript types.
- Runs anywhere
fetchdoes: browsers and Node 18+. - Two ways in: a headless
WattleAddrClientfor full control, or a drop-inWattleAddrwidget for any<input>.
npm install @wattleaddr/jsUsing React? Use @wattleaddr/react instead.
Keys
Use a publishable key (waddr_pk_…), locked to your domains in the console, anywhere the code
runs in a browser. A secret key (waddr_sk_…) is server-only. Never ship it to a browser.
Drop-in widget
Attach autocomplete to any text input. Handles debouncing, keyboard navigation, accessibility, and PO-Box notices for you, and bills one lookup per completed session (not per keystroke).
import { WattleAddr } from '@wattleaddr/js';
const wa = new WattleAddr('waddr_pk_live_…', { limit: 6, theme: 'light' });
wa.attach('#address', {
onSelect(address) {
console.log(address.formatted); // "1 Martin Place, Sydney NSW 2000"
console.log(address.components.postcode); // "2000"
console.log(address.geo); // { lat, lng } | null
},
});attach() also fires a native change event on the input, so it works inside most form libraries.
Call wa.destroy() to tear everything down.
Fill your form
Pass fill to put the picked address into your own form fields. It maps address parts to a field id, a name,
a CSS selector, or the element itself.
wa.attach('#address1', {
fill: { address_line1: 'address1', address_line2: 'address2', suburb: 'city', state: 'state', postcode: 'zip' },
});Units, shops, levels and lots go to the right place. With no line 2 or unit field, the unit joins line 1. Text the
person typed in line 2 is never overwritten or cleared. A drop-down is set only to an option it already offers. An unknown
part name throws when you call attach(). onSelect still runs after the fill. A form inside another iframe is not
filled, and values are not shortened to a field's maxlength.
The parts are address_line1, address_line2, unit, building_name, street_number, street_name, suburb,
state, state_name, postcode, country, full_address, latitude, longitude and address_id. The table with
examples is at https://wattleaddr.com.au/docs#widget.
To get the parts without filling anything, call addressParts(record). It returns every part as a string, empty when
the record has none. Pass { foldUnit: true } to put the unit in line 1.
Headless client
When you want to render your own UI, or call the API from a server, use the client directly.
import { WattleAddrClient, createSession } from '@wattleaddr/js';
const client = new WattleAddrClient('waddr_pk_live_…');
const session = createSession(); // ties a lookup together for billing
const { suggestions } = await client.autocomplete('1 mart', { session });
const address = await client.retrieve(suggestions[0].id, { session }); // the billable call
// Start a fresh session for the next lookup.Verify a free-text address
verify takes an address you already hold and matches it against G-NAF. Branch on verdict.
It is the field that tells you what to do:
Requires
@wattleaddr/js0.3.0 or later. 0.3.0 is the first release that types the verify assessment fields (verdict,match_score,match_level,elements,changed_elements,unmatched_tokens,dropped_tokens). The API returns them to every caller regardless of SDK version; older SDKs do not describe them.@wattleaddr/react0.2.0 depends on this release and re-exports the same types.
const r = await client.verify('100 collins st melbourne vic 3000', { session: createSession() });
switch (r.verdict) {
case 'verified': // everything you supplied agrees, and no other candidate fits
case 'corrected': // agrees as far as it goes; you abbreviated or omitted something, or, with extended_corrections: true, one part was fixed (see r.changed_elements)
save(r.match!); // r.match.formatted is the canonical form
break;
case 'ambiguous': // an element was contradicted, input was discarded, or a runner-up fits too
review(r.match, r.alternatives); // never auto-accept
break;
case 'unverified': // nothing usable matched: let the customer proceed, flag the row
flagForReview();
break;
}match_level says how precisely we identified it (subpremise → premise → thoroughfare →
locality → none) and is independent of verdict. premise + ambiguous can happen, and means
we found a building on a street whose suburb you contradicted. match_score (0–100) is there for
your own ranking. We publish no accept/reject line, because the right one depends on what a wrong
address costs you.
Re-prompt for one field, not the whole address. elements carries a per-field status, so the
form can ask again for exactly what is wrong:
// `changed` = you asserted a different value. This is why the address may be wrong.
// e.g. ['postcode'] for "1 Martin Place Sydney NSW 2001", where the record says 2000.
for (const field of r.changed_elements) askAgain(field);
// `missing` = not corroborated: either you didn't supply it, or the record hasn't got one.
// So check the record before prompting: a freestanding house always reports `subdwelling: missing`.
if (r.elements.subdwelling === 'missing' && r.match?.components.flat_number) {
askAgain('unit'); // the matched record IS a unit and you named none, so never `verified`
}That last case is why a scalar is not enough. "100 Collins St Melbourne VIC 3000" against
Unit 5, 100 Collins Street scores 92 with changed_elements: []. Every element you gave is
correct, but the verdict is ambiguous. In Australian delivery the missing unit is the
difference between a delivered parcel and a returned one.
Check dropped_tokens too. Non-empty means the match answers a shorter query than the
one you sent, and such a result is never verified. When the dropped token is the street
number, verify returns match: null. G-NAF has no such number on that street, and the house next
door is not the one you asked about. (Autocomplete is looser. It suggests the nearest numbers it
finds and flags it with a street_number_not_found notice. When the building matches but the unit you typed
has no exact match, it leads with the building and flags it with a unit_not_found notice.) unmatched_tokens holds words whose role in
the address could not be determined. They are reported rather than guessed into a field.
Migrating from
confidence. Top-levelconfidenceon a verify response is deprecated. It is now a bucketing ofmatch_score. Before August 2026 it was hardcoded to"high"on any match, including one obtained by discarding part of your query, so code branching on it was branching on a constant. It is still returned, and still'high' | 'medium' | 'low', because it is a required field in the published contract. Switch toverdictandelements. (Theconfidenceinsideresult.matchis a different field. It is G-NAF's own confidence in the record, not a match score, and is not deprecated.)
Server-side use
Set keyTransport: 'header' to send a secret key as Authorization: Bearer and keep it out of
request URLs and logs:
const client = new WattleAddrClient(process.env.WATTLEADDR_SECRET_KEY, {
keyTransport: 'header',
});In older runtimes without a global fetch, pass one: new WattleAddrClient(key, { fetch }).
API
new WattleAddrClient(key, options?)
| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| baseUrl | string | https://api.wattleaddr.com.au | Override for self-host/testing. |
| limit | number | 8 | Default autocomplete result count (1–20). |
| state | AuState | none | Default state filter (NSW, VIC, …). |
| keyTransport | 'query' \| 'header' | 'query' | query avoids a CORS preflight (browser); header keeps secret keys out of URLs. |
| fetch | typeof fetch | global | Inject a fetch implementation. |
client.autocomplete(query, { session?, limit?, state?, signal? })→AutocompleteResponseclient.retrieve(id, { session?, signal? })→AddressRecord(billable)client.verify(address, { session?, signal? })→VerifyResponse(billable on a match; requires a plan with verify)client.status({ signal? })→StatusResponse
Errors
Any non-2xx response throws a WattleAddrError with .code (e.g. rate_limited, quota_exceeded,
invalid_key), .status, .docs, and .retryAfter (on 429). Aborted requests reject with the
native AbortError.
import { WattleAddrError } from '@wattleaddr/js';
try {
await client.retrieve(id, { session });
} catch (e) {
if (e instanceof WattleAddrError && e.code === 'quota_exceeded') showUpgradePrompt();
}Using this with a coding agent
This package ships an agent skill, skills/wattleaddr/SKILL.md. It covers the things an assistant
otherwise gets wrong: session tokens and what they cost, which key belongs in the browser, PO Boxes
being absent from G-NAF by design, branching a verify result on verdict rather than the deprecated
confidence, and how to store the components. It contains no telemetry and phones nothing home.
Claude Code / Claude Desktop
mkdir -p .claude/skills/wattleaddr
cp node_modules/@wattleaddr/js/skills/wattleaddr/SKILL.md .claude/skills/wattleaddr/Cursor, Copilot, Windsurf or any other assistant: point it at the file, or fetch it directly:
curl -o WATTLEADDR.md https://wattleaddr.com.au/skills/wattleaddr/SKILL.mdLicense
MIT © RR Sols Pty Ltd t/a WattleAddr. Address data is © the Commonwealth of Australia (G-NAF), used under licence and served by the API. This package only talks to the API.
Links
- Documentation and quickstart: https://wattleaddr.com.au/docs
- Interactive API reference (no key needed): https://wattleaddr.com.au/docs/reference
- Error codes: https://wattleaddr.com.au/docs/errors
- Pricing (AUD, GST included; free tier, no card): https://wattleaddr.com.au/pricing
- What G-NAF is, and what it can and cannot do: https://wattleaddr.com.au/blog/gnaf-explained-what-it-can-and-cant-do
- Shopify app: https://wattleaddr.com.au/integrations/shopify
- Status: https://wattleaddr.com.au/status
- Support: [email protected]
Australian addresses only. Built on the open G-NAF dataset, served entirely from Australia. Not a postal-deliverability or AMAS/PAF service, and G-NAF holds no PO Boxes.
