The new plan ladder includes Supporter. Plans set per-key rates and scopes. Browser keys restrict lookups to allowed websites. The MCP server, address widget and integration guides help you connect your application. Every response now includes a request id for support.
Docs
Documentation
One API for Australian addresses. Everything is reachable in two clicks. Start with one request, then follow the reference in the rail’s order.
Every endpoint has a copyable request and the shape of its response. The same reference is available as OpenAPI 3.1, and a test fails the build when the document and the running routes disagree.
First sixty seconds
Get a free key
Sign up for a free key, verify your email and copy your server key. Replace
YOUR_KEYin the request with that key.Send one request
Send your key in the
X-API-Keyheader.curl "https://yepthere.au/v1/addresses?q=9+george+st+syd" -H "X-API-Key: YOUR_KEY"
const response = await fetch('https://yepthere.au/v1/addresses?q=9+george+st+syd', { headers: { 'X-API-Key': 'YOUR_KEY' } }); const data = await response.json(); console.log(data.results[0]?.uprn);import json from urllib.request import Request, urlopen request = Request('https://yepthere.au/v1/addresses?q=9+george+st+syd', headers={'X-API-Key': 'YOUR_KEY', 'User-Agent': 'yepthere-python/0.1.0 (+https://yepthere.au)'}) with urlopen(request) as response: data = json.load(response) print(data['results'][0]['uprn'] if data['results'] else None)claude mcp add --transport http yepthere 'https://yepthere.au/mcp' --header "Authorization: Bearer YOUR_KEY"
For Claude, use the MCP command. For Cursor, use the same endpoint and header in AI assistant setup.
The free plan includes up to 500 MCP lookups a month. Paid plans use their full allowance. Upgrade your plan.
Read the chosen result’s uprn
In
results[0],uprnis the stable address id,line1is the street address andline2is the building name, or null. Keep the chosen result’suprnfor later lookups. This abbreviated example keeps the response’s field names:{ "ok": true, "results": [ { "uprn": "GANSW718028803", "line1": "9 George Street", "line2": null, "suburb": "Sydney", "state": "NSW", "postcode": "2000", "latitude": -33.86580248, "longitude": 151.20700223, "boundaries": { "edition": "ASGS Edition 3 (2021)", "sa2": { "code": "117031644", "name": "Sydney (North) - Millers Point" }, "lga": { "code": "17200", "name": "Sydney" }, "mmm": { "category": 1, "name": "Metropolitan" } } } ] }
What to do when it fails
401: check that you replaced YOUR_KEY with your current key and sent the key header. 403: check the browser key’s allowed website and lookup-only rule, or the server key’s plan scope. 429: wait the number of seconds in Retry-After. A monthly quota error means waiting for the reset or changing plan. Find the exact error below.
Find your way
Endpoints
Copy a request and see the response for every endpoint.
The address record
Understand the fields returned for an Australian address.
Boundaries
Find the statistical, electoral and local government areas for an address.
Keys and limits
Choose a key and check its scopes, allowances and rate limits.
Errors
Find the exact error message and what to do next.
Data and changes
Check the loaded release, attribution and what changed.
Integrations
Connect your platform or use the JavaScript and Python clients.
The widget
Add address autocomplete to a form on your website.
AI assistants
Connect Claude or Cursor through MCP.
Base https://yepthere.au/v1. JSON. GDA2020.
Endpoints
Address lookups return the same record shape. GET lookup responses carry ETag and Cache-Control headers. Conditional requests compare weak validators too; they still run the lookup, but a 304 response consumes no monthly allowance but does consume burst capacity. Browser keys allow GET and HEAD lookups only. Examples use sample records.
For JavaScript, use Node 22 ESM. For Python, run with PYTHONPATH=sdk/python. Copy the vendored clients from Integrations to the named paths and keep their MIT licence. Both clients read SESSIONAL_ADDRESSES_KEY in these samples. Both clients provide boundaries(layer, code) and jobs.download(id). Downloads require a jobs-scoped account key and follow a five-minute storage redirect without forwarding credentials. Python parse() includes response.parsed. Raw HTTP examples are also shown for jobs and health checks. Display the G-NAF attribution wherever you publish results.
GET/v1/addresses?q=<text>&limit=8
Autocomplete. Word-level AND match on at least three characters. Returns eight by default, up to twenty.
curl "https://yepthere.au/v1/addresses?q=9+George+Street+Sydney&limit=5" \ -H "x-api-key: $SESSIONAL_ADDRESSES_KEY"
// Attribution: display the G-NAF attribution line with published results.
import { Addresses } from './sdk/js/index.js';
const client = new Addresses({ apiKey: process.env.SESSIONAL_ADDRESSES_KEY, baseUrl: "https://yepthere.au" });
const response = await client.search('9 George Street Sydney', { limit: 5 });
console.log(response.results, response.rateLimit.remaining);# Attribution: display the G-NAF attribution line with published results.
import os
from sessional_addresses import Addresses
client = Addresses(api_key=os.environ["SESSIONAL_ADDRESSES_KEY"], base_url="https://yepthere.au")
response = client.search("9 George Street Sydney", limit=5)
print(response.results, response.rate_limit.remaining)Response shape
{
"ok": true,
"release": "AUG 2026 - Geoscape G-NAF - GDA2020",
"results": [
{
"uprn": "GANSW718028803",
"line1": "9 George Street",
"line2": null,
"suburb": "Sydney",
"state": "NSW",
"postcode": "2000",
"latitude": -33.86580248,
"longitude": 151.20700223,
"boundaries": {
"edition": "ASGS Edition 3 (2021)",
"sa1": "11703164421",
"sa2": {
"code": "117031644",
"name": "Sydney (North) - Millers Point"
},
"sa3": {
"code": "11703",
"name": "Sydney Inner City"
},
"sa4": {
"code": "117",
"name": "Sydney - City and Inner South"
},
"gccsa": {
"code": "1GSYD",
"name": "Greater Sydney"
},
"remoteness": {
"code": "10",
"name": "Major Cities of Australia"
},
"lga": {
"code": "17200",
"name": "Sydney"
},
"electorate": {
"federal": {
"code": "141",
"name": "Sydney"
},
"state": {
"code": "10078",
"name": "Sydney"
}
},
"postalArea": "2000",
"suburb": {
"code": "13730",
"name": "Sydney"
},
"mmm": {
"category": 1,
"name": "Metropolitan"
}
}
}
]
}GET/v1/addresses?postcode=2000&limit=100
Every address at a postcode. Default 100, maximum 500.
curl "https://yepthere.au/v1/addresses?postcode=2000&limit=500" \ -H "x-api-key: $SESSIONAL_ADDRESSES_KEY"
// Attribution: display the G-NAF attribution line with published results.
import { Addresses } from './sdk/js/index.js';
const client = new Addresses({ apiKey: process.env.SESSIONAL_ADDRESSES_KEY, baseUrl: "https://yepthere.au" });
const response = await client.byPostcode('2000', { limit: 500 });
console.log(response.results, response.rateLimit.remaining);# Attribution: display the G-NAF attribution line with published results.
import os
from sessional_addresses import Addresses
client = Addresses(api_key=os.environ["SESSIONAL_ADDRESSES_KEY"], base_url="https://yepthere.au")
response = client.by_postcode("2000", limit=500)
print(response.results, response.rate_limit.remaining)Response shape
{
"ok": true,
"release": "AUG 2026 - Geoscape G-NAF - GDA2020",
"results": [
{
"uprn": "GANSW718028803",
"line1": "9 George Street",
"line2": null,
"suburb": "Sydney",
"state": "NSW",
"postcode": "2000",
"latitude": -33.86580248,
"longitude": 151.20700223,
"boundaries": {
"edition": "ASGS Edition 3 (2021)",
"sa1": "11703164421",
"sa2": {
"code": "117031644",
"name": "Sydney (North) - Millers Point"
},
"sa3": {
"code": "11703",
"name": "Sydney Inner City"
},
"sa4": {
"code": "117",
"name": "Sydney - City and Inner South"
},
"gccsa": {
"code": "1GSYD",
"name": "Greater Sydney"
},
"remoteness": {
"code": "10",
"name": "Major Cities of Australia"
},
"lga": {
"code": "17200",
"name": "Sydney"
},
"electorate": {
"federal": {
"code": "141",
"name": "Sydney"
},
"state": {
"code": "10078",
"name": "Sydney"
}
},
"postalArea": "2000",
"suburb": {
"code": "13730",
"name": "Sydney"
},
"mmm": {
"category": 1,
"name": "Metropolitan"
}
}
}
]
}GET/v1/addresses/:id
Resolve a stored G-NAF address ID back to the full record. Zero or one result.
curl "https://yepthere.au/v1/addresses/GANSW718028803" \ -H "x-api-key: $SESSIONAL_ADDRESSES_KEY"
// Attribution: display the G-NAF attribution line with published results.
import { Addresses } from './sdk/js/index.js';
const client = new Addresses({ apiKey: process.env.SESSIONAL_ADDRESSES_KEY, baseUrl: "https://yepthere.au" });
const response = await client.get('GANSW718028803');
console.log(response.results, response.rateLimit.remaining);# Attribution: display the G-NAF attribution line with published results.
import os
from sessional_addresses import Addresses
client = Addresses(api_key=os.environ["SESSIONAL_ADDRESSES_KEY"], base_url="https://yepthere.au")
response = client.get("GANSW718028803")
print(response.results, response.rate_limit.remaining)Response shape
{
"ok": true,
"release": "AUG 2026 - Geoscape G-NAF - GDA2020",
"results": [
{
"uprn": "GANSW718028803",
"line1": "9 George Street",
"line2": null,
"suburb": "Sydney",
"state": "NSW",
"postcode": "2000",
"latitude": -33.86580248,
"longitude": 151.20700223,
"boundaries": {
"edition": "ASGS Edition 3 (2021)",
"sa1": "11703164421",
"sa2": {
"code": "117031644",
"name": "Sydney (North) - Millers Point"
},
"sa3": {
"code": "11703",
"name": "Sydney Inner City"
},
"sa4": {
"code": "117",
"name": "Sydney - City and Inner South"
},
"gccsa": {
"code": "1GSYD",
"name": "Greater Sydney"
},
"remoteness": {
"code": "10",
"name": "Major Cities of Australia"
},
"lga": {
"code": "17200",
"name": "Sydney"
},
"electorate": {
"federal": {
"code": "141",
"name": "Sydney"
},
"state": {
"code": "10078",
"name": "Sydney"
}
},
"postalArea": "2000",
"suburb": {
"code": "13730",
"name": "Sydney"
},
"mmm": {
"category": 1,
"name": "Metropolitan"
}
}
}
]
}GET/v1/nearest?lat=&lng=&limit=5
Reverse geocode. The closest addresses to a point, with distance in metres. Maximum 20.
curl "https://yepthere.au/v1/nearest?lat=-33.8658&lng=151.2070" \ -H "x-api-key: $SESSIONAL_ADDRESSES_KEY"
// Attribution: display the G-NAF attribution line with published results.
import { Addresses } from './sdk/js/index.js';
const client = new Addresses({ apiKey: process.env.SESSIONAL_ADDRESSES_KEY, baseUrl: "https://yepthere.au" });
const response = await client.nearest(-33.8658, 151.2070);
console.log(response.results, response.rateLimit.remaining);# Attribution: display the G-NAF attribution line with published results. import os from sessional_addresses import Addresses client = Addresses(api_key=os.environ["SESSIONAL_ADDRESSES_KEY"], base_url="https://yepthere.au") response = client.nearest(-33.8658, 151.2070) print(response.results, response.rate_limit.remaining)
Response shape
{
"ok": true,
"release": "AUG 2026 - Geoscape G-NAF - GDA2020",
"results": [
{
"uprn": "GANSW718028803",
"line1": "9 George Street",
"line2": null,
"suburb": "Sydney",
"state": "NSW",
"postcode": "2000",
"latitude": -33.86580248,
"longitude": 151.20700223,
"distanceMetres": 12.4
}
]
}GET/v1/postcodes/:postcode
The postcode itself: suburbs it covers, state, and a centroid. One aggregate per state. suburbRanking lists name, state and addressCount per suburb, most addresses first; suburbs remains a string array. Unknown postcodes return 404 with postcode-not-found.
curl "https://yepthere.au/v1/postcodes/3000" \ -H "x-api-key: $SESSIONAL_ADDRESSES_KEY"
// Attribution: display the G-NAF attribution line with published results.
import { Addresses } from './sdk/js/index.js';
const client = new Addresses({ apiKey: process.env.SESSIONAL_ADDRESSES_KEY, baseUrl: "https://yepthere.au" });
const response = await client.postcode('3000');
console.log(response.results, response.rateLimit.remaining);# Attribution: display the G-NAF attribution line with published results.
import os
from sessional_addresses import Addresses
client = Addresses(api_key=os.environ["SESSIONAL_ADDRESSES_KEY"], base_url="https://yepthere.au")
response = client.postcode("3000")
print(response.results, response.rate_limit.remaining)Response shape
{
"ok": true,
"release": "AUG 2026 - Geoscape G-NAF - GDA2020",
"results": [
{
"postcode": "3000",
"state": "VIC",
"suburbs": [
"Melbourne"
],
"addressCount": 100,
"latitude": -37.81,
"longitude": 144.96
}
]
}GET/v1/postcodes?q=<prefix-or-suburb>&limit=8
Postcode and suburb autocomplete. Supply a postcode prefix of one to four digits or a suburb name. Returns eight by default, up to twenty.
curl "https://yepthere.au/v1/postcodes?q=melbourne" \ -H "x-api-key: $SESSIONAL_ADDRESSES_KEY"
// Attribution: display the G-NAF attribution line with published results.
import { Addresses } from './sdk/js/index.js';
const client = new Addresses({ apiKey: process.env.SESSIONAL_ADDRESSES_KEY, baseUrl: "https://yepthere.au" });
const response = await client.searchPostcodes('melbourne');
console.log(response.results, response.rateLimit.remaining);# Attribution: display the G-NAF attribution line with published results.
import os
from sessional_addresses import Addresses
client = Addresses(api_key=os.environ["SESSIONAL_ADDRESSES_KEY"], base_url="https://yepthere.au")
response = client.search_postcodes("melbourne")
print(response.results, response.rate_limit.remaining)Response shape
{
"ok": true,
"release": "AUG 2026 - Geoscape G-NAF - GDA2020",
"results": [
{
"postcode": "3000",
"state": "VIC",
"suburbs": [
"Melbourne"
],
"addressCount": 100,
"latitude": -37.81,
"longitude": 144.96
}
]
}GET/v1/boundaries/:layer/:code
A boundary by layer (mmm, remoteness, lga, sa2, sa3, sa4, federal, state, poa, sal) and code: its name and the postcodes it touches with address counts. Returns 404 boundary-not-found for unknown codes or when no boundary set is loaded.
curl "https://yepthere.au/v1/boundaries/lga/17200" \ -H "x-api-key: $SESSIONAL_ADDRESSES_KEY"
// Attribution: display the G-NAF attribution line with published results.
import { Addresses } from './sdk/js/index.js';
const client = new Addresses({ apiKey: process.env.SESSIONAL_ADDRESSES_KEY, baseUrl: "https://yepthere.au" });
const response = await client.boundaries('poa', '2000');
console.log(response.results, response.rateLimit.remaining);# Attribution: display the G-NAF attribution line with published results.
import os
from sessional_addresses import Addresses
client = Addresses(api_key=os.environ["SESSIONAL_ADDRESSES_KEY"], base_url="https://yepthere.au")
response = client.boundaries("poa", "2000")
print(response.results, response.rate_limit.remaining)Response shape
{
"ok": true,
"release": "AUG 2026 - Geoscape G-NAF - GDA2020",
"results": [
{
"layer": "lga",
"code": "17200",
"name": "Sydney",
"postcodes": [
{
"postcode": "2000",
"state": "NSW",
"addresses": 100
}
]
}
]
}POST/v1/parse
Messy address text in, structured components and ranked G-NAF matches out, with a confidence and what matched. Body { "q": "unit 3/42 wallaby way syd 2000", "limit": 3 }. PO boxes are recognised and never matched. When the rule-based parser is unsure the text is passed to a language model. The privacy page explains what is sent. Needs the parse scope.
curl -X POST "https://yepthere.au/v1/parse" \
-H "x-api-key: $SESSIONAL_ADDRESSES_KEY" \
-H "content-type: application/json" \
-d '{"q":"unit 3/42 wallaby way syd 2000","limit":3}'// Attribution: display the G-NAF attribution line with published results.
import { Addresses } from './sdk/js/index.js';
const client = new Addresses({ apiKey: process.env.SESSIONAL_ADDRESSES_KEY, baseUrl: "https://yepthere.au" });
const response = await client.parse('unit 3/42 wallaby way syd 2000', { limit: 3 });
console.log(response.results, response.rateLimit.remaining);# Attribution: display the G-NAF attribution line with published results.
import os
from sessional_addresses import Addresses
client = Addresses(api_key=os.environ["SESSIONAL_ADDRESSES_KEY"], base_url="https://yepthere.au")
response = client.parse("unit 3/42 wallaby way syd 2000", limit=3)
print(response.results, response.rate_limit.remaining)Response shape
{
"ok": true,
"release": "AUG 2026 - Geoscape G-NAF - GDA2020",
"parsed": {
"unit": "3",
"number": "42",
"numberTo": null,
"street": "WALLABY",
"streetType": "WAY",
"suburb": "SYDNEY",
"state": "NSW",
"postcode": "2000",
"kind": "street",
"method": "rules",
"confidence": 0.94
},
"results": [
{
"uprn": "GANSW718028803",
"line1": "9 George Street",
"line2": null,
"suburb": "Sydney",
"state": "NSW",
"postcode": "2000",
"latitude": -33.86580248,
"longitude": 151.20700223,
"confidence": 0.94,
"matchedOn": [
"number",
"street",
"suburb"
]
}
]
}POST/v1/addresses/bulk
Up to 100 queries in one request. Each counts as one lookup. Results come back in the order sent. Each query is an object with q and an optional limit, default 1, maximum 5.
curl -X POST "https://yepthere.au/v1/addresses/bulk" \
-H "x-api-key: $SESSIONAL_ADDRESSES_KEY" \
-H "content-type: application/json" \
-d '{"queries":[{"q":"1 george st sydney"},{"q":"42 wallaby way"}]}'// Attribution: display the G-NAF attribution line with published results.
import { Addresses } from './sdk/js/index.js';
const client = new Addresses({ apiKey: process.env.SESSIONAL_ADDRESSES_KEY, baseUrl: "https://yepthere.au" });
const response = await client.bulk([{ q: '1 george st sydney' }, { q: '42 wallaby way' }]);
console.log(response.results, response.rateLimit.remaining);# Attribution: display the G-NAF attribution line with published results.
import os
from sessional_addresses import Addresses
client = Addresses(api_key=os.environ["SESSIONAL_ADDRESSES_KEY"], base_url="https://yepthere.au")
response = client.bulk([{"q": "1 george st sydney"}, {"q": "42 wallaby way"}])
print(response.results, response.rate_limit.remaining)Response shape
{
"ok": true,
"release": "AUG 2026 - Geoscape G-NAF - GDA2020",
"results": [
{
"q": "1 george st sydney",
"results": [
{
"uprn": "GANSW718028803",
"line1": "9 George Street",
"line2": null,
"suburb": "Sydney",
"state": "NSW",
"postcode": "2000",
"latitude": -33.86580248,
"longitude": 151.20700223
}
]
},
{
"q": "42 wallaby way",
"results": []
}
]
}POST/v1/jobs
Submit a CSV of addresses to match and geocode as a job. Send the file as the body with Content-Type text/csv, up to 10,485,760 bytes and 50,000 rows. Headers x-job-column and x-job-filename set the address column and filename. Read job.id from the response. estimatedSecondsRemaining includes work scheduled ahead; estimatedReadyAt is an ISO 8601 UTC ready time. Both are estimates and tighten as processing advances. Every successfully processed row counts one job row; error rows cost nothing. Needs the jobs scope.
curl -X POST "https://yepthere.au/v1/jobs" \ -H "x-api-key: $SESSIONAL_ADDRESSES_KEY" \ -H "content-type: text/csv" -H "x-job-column: address" \ --data-binary @addresses.csv
// Jobs accept raw CSV and return job objects. The SDK jobs.submit and jobs.get methods use the same contract.
import { readFile } from 'node:fs/promises';
const response = await fetch("https://yepthere.au/v1/jobs", {
method: 'POST',
headers: { 'x-api-key': process.env.SESSIONAL_ADDRESSES_KEY, 'content-type': 'text/csv', 'x-job-column': 'address' },
body: await readFile('addresses.csv')
});
const data = await response.json();
if (response.ok === false) throw new Error(data.message || 'Request failed');
console.log(data);# Jobs accept raw CSV and return job objects. The SDK jobs.submit and jobs.get methods use the same contract.
import json
import os
from pathlib import Path
from urllib.request import Request, urlopen
request = Request("https://yepthere.au/v1/jobs", method="POST",
headers={"User-Agent": "yepthere-python/0.1.0 (+https://yepthere.au)", "x-api-key": os.environ["SESSIONAL_ADDRESSES_KEY"], "Content-Type": "text/csv", "x-job-column": "address"},
data=Path("addresses.csv").read_bytes())
with urlopen(request, timeout=30) as response:
print(json.load(response))Response shape
{
"ok": true,
"job": {
"id": "job_example",
"status": "queued",
"rowCount": 1200
}
}GET/v1/jobs/:id
Job state, estimatedSecondsRemaining, estimatedReadyAt (ISO 8601 UTC), counts by confidence band, errorRows for failed row lookups, and an authenticated download URL once the job is done. Send your account key to GET /v1/jobs/:id/download; it redirects to storage for five minutes (404 if unknown or expired, 409 if not done).
curl "https://yepthere.au/v1/jobs/job_example" \ -H "x-api-key: $SESSIONAL_ADDRESSES_KEY"
// Jobs accept raw CSV and return job objects. The SDK jobs.submit and jobs.get methods use the same contract.
const response = await fetch("https://yepthere.au/v1/jobs/job_example", {
method: 'GET',
headers: { 'x-api-key': process.env.SESSIONAL_ADDRESSES_KEY }
});
const data = await response.json();
if (response.ok === false) throw new Error(data.message || 'Request failed');
console.log(data);# Jobs accept raw CSV and return job objects. The SDK jobs.submit and jobs.get methods use the same contract.
import json
import os
from urllib.request import Request, urlopen
request = Request("https://yepthere.au/v1/jobs/job_example", method="GET",
headers={"User-Agent": "yepthere-python/0.1.0 (+https://yepthere.au)", "x-api-key": os.environ["SESSIONAL_ADDRESSES_KEY"]})
with urlopen(request, timeout=30) as response:
print(json.load(response))Response shape
{
"ok": true,
"job": {
"id": "job_example",
"filename": "addresses.csv",
"column": "address",
"status": "done",
"estimatedSecondsRemaining": 0,
"estimatedReadyAt": "2026-10-18T00:00:00.000Z",
"rowCount": 1200,
"matchedHigh": 1104,
"matchedLow": 71,
"unmatched": 25,
"errorRows": 0,
"downloadUrl": "https://yepthere.au/v1/jobs/job_example/download",
"expiresAt": "2026-10-25T00:00:00.000Z"
}
}GET/v1/jobs/:id/download
Download completed CSV results with the owning account key and jobs scope. Returns a 302 redirect to a five-minute storage URL, 404 for unknown or expired jobs, and 409 until done. Do not forward credentials to storage.
curl --location "https://yepthere.au/v1/jobs/job_example/download" \ -H "Authorization: Bearer $SESSIONAL_ADDRESSES_KEY"
import { Addresses } from './sdk/js/index.js';
const client = new Addresses({ apiKey: process.env.SESSIONAL_ADDRESSES_KEY, baseUrl: "https://yepthere.au" });
const stream = await client.jobs.download('job_example');
console.log(await new Response(stream).text());import os
from pathlib import Path
from sessional_addresses import Addresses
client = Addresses(api_key=os.environ["SESSIONAL_ADDRESSES_KEY"], base_url="https://yepthere.au")
Path("results.csv").write_bytes(client.jobs.download("job_example"))Response shape
{
"status": 302,
"Location": "https://storage.example/results.csv?X-Amz-Expires=300"
}GET/v1/jobs
List the last 50 jobs for your account. Needs the jobs scope.
curl "https://yepthere.au/v1/jobs" \ -H "x-api-key: $SESSIONAL_ADDRESSES_KEY"
// Jobs accept raw CSV and return job objects. The SDK jobs.submit and jobs.get methods use the same contract.
const response = await fetch("https://yepthere.au/v1/jobs", {
method: 'GET',
headers: { 'x-api-key': process.env.SESSIONAL_ADDRESSES_KEY }
});
const data = await response.json();
if (response.ok === false) throw new Error(data.message || 'Request failed');
console.log(data);# Jobs accept raw CSV and return job objects. The SDK jobs.submit and jobs.get methods use the same contract.
import json
import os
from urllib.request import Request, urlopen
request = Request("https://yepthere.au/v1/jobs", method="GET",
headers={"User-Agent": "yepthere-python/0.1.0 (+https://yepthere.au)", "x-api-key": os.environ["SESSIONAL_ADDRESSES_KEY"]})
with urlopen(request, timeout=30) as response:
print(json.load(response))Response shape
{
"ok": true,
"jobs": [
{
"id": "job_example",
"filename": "addresses.csv",
"column": "address",
"status": "done",
"estimatedSecondsRemaining": 0,
"estimatedReadyAt": "2026-10-18T00:00:00.000Z",
"rowCount": 1200,
"matchedHigh": 1104,
"matchedLow": 71,
"unmatched": 25,
"errorRows": 0,
"downloadUrl": "https://yepthere.au/v1/jobs/job_example/download",
"expiresAt": "2026-10-25T00:00:00.000Z"
}
]
}GET/health
Cheap process liveness check. It does not query PostgreSQL. No key required.
curl "https://yepthere.au/health"
// Use HTTP here: the vendored clients have no method for this endpoint.
const response = await fetch("https://yepthere.au/health", {
method: 'GET',
headers: { }
});
const data = await response.json();
if (response.ok === false) throw new Error(data.message || 'Request failed');
console.log(data);# Use HTTP here: the vendored clients have no method for this endpoint.
import json
from urllib.request import Request, urlopen
request = Request("https://yepthere.au/health", method="GET",
headers={"User-Agent": "yepthere-python/0.1.0 (+https://yepthere.au)"})
with urlopen(request, timeout=30) as response:
print(json.load(response))Response shape
{
"ok": true
}GET/ready
Readiness, row count, release ID, release name and load timestamp. Returns 503 until loaded or when the database is unavailable. No key required.
curl "https://yepthere.au/ready"
// Use HTTP here: the vendored clients have no method for this endpoint.
const response = await fetch("https://yepthere.au/ready", {
method: 'GET',
headers: { }
});
const data = await response.json();
if (response.ok === false) throw new Error(data.message || 'Request failed');
console.log(data);# Use HTTP here: the vendored clients have no method for this endpoint.
import json
from urllib.request import Request, urlopen
request = Request("https://yepthere.au/ready", method="GET",
headers={"User-Agent": "yepthere-python/0.1.0 (+https://yepthere.au)"})
with urlopen(request, timeout=30) as response:
print(json.load(response))Response shape
{
"ok": true,
"ready": true,
"rowCount": 16091511,
"releaseId": "b023544a-5836-4d43-b6d8-da2f73e8d2bf:2026-08-17T03:24:56.565849",
"releaseName": "AUG 2026 - Geoscape G-NAF - GDA2020",
"loadedAt": "2026-10-25T12:11:35.160Z"
}The address record
Fields are stable across releases. Coordinates use GDA2020 decimal degrees. Postcodes are strings so leading zeros are retained. Building names, coordinates and boundary fields can be null.
{
"uprn": "GANSW718028803",
"line1": "9 George Street",
"line2": null,
"suburb": "Sydney",
"state": "NSW",
"postcode": "2000",
"latitude": -33.86580248,
"longitude": 151.20700223,
"boundaries": {
"edition": "ASGS Edition 3 (2021)",
"sa1": "11703164421",
"sa2": {
"code": "117031644",
"name": "Sydney (North) - Millers Point"
},
"sa3": {
"code": "11703",
"name": "Sydney Inner City"
},
"sa4": {
"code": "117",
"name": "Sydney - City and Inner South"
},
"gccsa": {
"code": "1GSYD",
"name": "Greater Sydney"
},
"remoteness": {
"code": "10",
"name": "Major Cities of Australia"
},
"lga": {
"code": "17200",
"name": "Sydney"
},
"electorate": {
"federal": {
"code": "141",
"name": "Sydney"
},
"state": {
"code": "10078",
"name": "Sydney"
}
},
"postalArea": "2000",
"suburb": {
"code": "13730",
"name": "Sydney"
},
"mmm": {
"category": 1,
"name": "Metropolitan"
}
}
}- uprn
- G-NAF address detail PID. Stable across releases.
- line1
- Number and street, with the unit or flat where there is one.
- line2
- Building or site name when G-NAF carries one, otherwise null.
- suburb
- Locality name as gazetted.
- state
- NSW, VIC, QLD, SA, WA, TAS, NT, ACT or OT (Other Territories).
- postcode
- Four digits as a string.
- latitude
- GDA2020 decimal degrees. Property centroid where G-NAF has one.
- longitude
- GDA2020 decimal degrees.
- boundaries
- Present once a boundary set is loaded. Nearest results add
distanceMetres. Postcode results contain state aggregates with anaddressCount. Bulk results contain each query and its results in request order. Successful lookup envelopes include the release name andboundaries.edition, for exampleASGS Edition 4 (2026). Address boundary objects also includeedition. A null edition means no recognised ASGS set is loaded. The configured switch selects the next load; responses describe the loaded set, including after rollback.
Boundary layers
Each address is joined using its G-NAF Mesh Block and through the ABS allocation files to these areas. Codes are the official ABS and AEC codes. Use /v1/boundaries/:layer/:code to go the other way, from a boundary to the postcodes it touches.
| Field | Layer | Notes |
|---|---|---|
| sa1 | Statistical Area 1 | The smallest ABS unit with published statistics. Code only. |
| sa2 | Statistical Area 2 | The level most reporting uses. Code and name. |
| sa3 | Statistical Area 3 | Code and name. |
| sa4 | Statistical Area 4 | Code and name. |
| gccsa | Greater capital city area | Greater Sydney, Rest of NSW and so on. Code and name. |
| remoteness | Remoteness area | Major Cities of Australia through to Very Remote Australia, 2021. |
| lga | Local government area | 2025 allocation. Code and name. |
| electorate.federal | Federal electorate | Commonwealth Electoral Division, 2025 allocation. |
| electorate.state | State electorate | State Electoral Division, 2025 allocation. |
| postalArea | Postal area | The ABS postal area code, which can differ from the delivery postcode. |
| suburb | Suburb or locality | ABS suburb and locality code and name, 2021. |
| mmm | Modified Monash category | MMM 2023, category 1 for metropolitan through 7 for very remote. |
Keys and limits
Your account’s plan sets each key’s allowance and rate, and how many keys you can create. See plan prices.
Server keys
Server keys start with sak_live_. Use them in server code, scripts, CSV jobs and MCP clients. Keep them in environment variables or server secrets. Send x-api-key: $SESSIONAL_ADDRESSES_KEY or Authorization: Bearer $SESSIONAL_ADDRESSES_KEY. Keys are shown once; rotate or revoke them in your account. Health, readiness, status and docs need no key.
Browser keys
For local development, allowed websites may use http://localhost[:port], http://127.0.0.1[:port] or http://[::1][:port]. Other HTTP origins are rejected. Browser keys start with sab_live_. Use them for a widget or autocomplete in a public page. Visitors can see them. They allow lookup-only access through GET and HEAD, even on plans with parse or jobs. They cannot call parse, bulk, jobs or MCP.
List 1 to 10 allowed websites as hostnames or HTTPS origins, without a path, query or fragment. For example, shop.example.com.au and https://shop.example.com.au allow the same hostname. Names are case-insensitive, and ports do not restrict access for hostname and HTTPS entries. A normal entry matches that exact hostname.
*.example.com.au allows one subdomain level, such as shop.example.com.au. It does not allow example.com.au or au.shop.example.com.au. Add the bare domain separately if you need it. Wildcards must use your own domain; shared suffixes such as *.com.au and *.github.io are rejected.
The request must come over HTTPS, except for local development: list http://localhost:3000, http://127.0.0.1:8080 or http://[::1]:3000 exactly as your browser sends it, scheme, host and port, and that origin is allowed over plain HTTP. The service checks Origin first and uses Referer only when Origin is absent. A missing or invalid website is rejected.
Scopes by plan
Server scopes follow your account’s plan, including after a downgrade. Every plan has lookup; a non-zero parse allowance adds parse, and a non-zero CSV allowance adds jobs. Browser keys keep only lookup.
| Plan | Scopes | Lookups a month | Parses a month | CSV rows | Keys | Requests a second |
|---|---|---|---|---|---|---|
| Free | search, resolve, nearest, postcodes, bulk, parse | 5,000 | 100 | 0 | 1 | 5 |
| Supporter | search, resolve, nearest, postcodes, bulk, parse | 10,000 | 500 | 0 | 2 | 10 |
| Pro | search, resolve, nearest, postcodes, bulk, parse, jobs | 100,000 | 10,000 | 100,000 | 5 | 20 |
| Scale | search, resolve, nearest, postcodes, bulk, parse, jobs | 500,000 | 50,000 | 500,000 | 25 | 50 |
Monthly reset and quota emails
Each key’s monthly lookup allowance resets at 00:00 UTC on the first day of each month. Successful parse requests use only the parse allowance, with no lookup charge. Bulk requests count once per query and CSV jobs once per successfully processed row. We send quota emails at 80 per cent and 100 per cent of a key’s monthly lookup allowance.
Burst limits and response headers
The burst limit applies per key. The table shows the plan’s requests a second. Browser keys are capped at 5 requests a second, or the plan’s lower rate. An active throttle can lower either rate. Debounce typing and wait before retrying a 429 response.
| Header | Meaning |
|---|---|
| X-RateLimit-Burst | The effective burst rate in requests a second, including any active throttle. Internal keys are exempt. |
| X-RateLimit-Limit | The key's monthly lookup limit. |
| X-RateLimit-Remaining | Lookups remaining after a successful response. |
| X-RateLimit-Reset | The first instant of the next UTC calendar month in ISO 8601 format. |
| Retry-After | Whole seconds to wait after burst or monthly quota throttling. |
The three monthly headers are sent after a registered key passes authentication, scope, burst and quota checks. They are also present if that request then fails validation. Rejected keys and the trusted system key do not receive these headers. The system key bypasses registered-key quota and burst checks.
The JavaScript and Python clients retry transient failures with bounded exponential backoff and honour capped Retry-After delays; parse and job submission are retried only with an explicit opt-in.
Example after the first lookup on a free server key, during October 2026:
X-RateLimit-Limit: 5000 X-RateLimit-Remaining: 4999 X-RateLimit-Reset: 2026-11-01T00:00:00.000Z X-RateLimit-Burst: 5
A burst rejection can instead return Retry-After: 1, which means wait at least one second before retrying. A monthly quota rejection gives the seconds until the reset.
Request ids and support
Every response includes X-Request-Id. Please quote that request id when you write to [email protected], so we can find the request.
Errors
REST errors normally contain ok: false, an error code, a human message and empty results. The readiness response and unknown-route response omit results. MCP protocol failures use a JSON-RPC error object with a numeric code and message; tool results carry the underlying API error.
{
"ok": false,
"error": "invalid-api-key",
"message": "A valid API key is required.",
"results": []
}The table lists message variants separately. In a message template, ${host} is the request hostname, or an unknown website when it cannot be read; ${index} is the zero-based query index. Other placeholders identify request values or the limits named in the message. MCP tool errors carry the underlying API status shown here, while the MCP HTTP response can still be 200.
| Status | Code | Exact message or template | What to do |
|---|---|---|---|
| 429 | mcp-free-limit | The free plan includes 500 MCP lookups a month. | Wait until the next UTC month or upgrade for MCP within your full allowance. |
| 402 | plan-required | This capability requires a paid plan. | Upgrade for the requested capability. |
| 401 | invalid-api-key | A valid API key is required. | Send a current key in x-api-key or Authorization: Bearer. |
| 401 | key-revoked | This API key has been revoked. | Create a new key in your account and replace the old one. |
| 403 | account-suspended | This account has been suspended. | Contact support with the request id. |
| 403 | insufficient-scope | This API key does not have the required scope. | Choose a plan with the required parse or jobs allowance. |
| 403 | origin-not-allowed | This browser key is not allowed from ${host}. | Add the website hostname to the key’s allowlist. Send Origin, or Referer when Origin is absent. |
| 403 | ip-not-allowed | This key is not allowed from ${address}. | Add the caller address or CIDR range to the server key’s IP allowlist. |
| 403 | scope-denied | This browser key only allows lookups. | Use GET or HEAD lookups, or move this call to a server with a server key. |
| 429 | rate-limited | Too many requests. Please try again later. | Wait Retry-After seconds, then reduce the request rate. |
| 429 | quota-exceeded | This API key has exhausted its monthly quota. | Wait until the monthly reset or change your plan. Respect Retry-After. |
| 400 | invalid-query | Supply exactly one of q or postcode | Correct the named query parameter and send it once. |
| 400 | invalid-query | q must contain at least 3 characters | Correct the named query parameter and send it once. |
| 400 | invalid-query | Include a street or suburb name; single digits need an alphabetic word of at least 3 characters. | Correct the named query parameter and send it once. |
| 400 | invalid-query | q must contain no more than 200 characters | Correct the named query parameter and send it once. |
| 400 | invalid-query | postcode must contain exactly 4 digits | Correct the named query parameter and send it once. |
| 400 | invalid-query | limit must be a positive whole number | Correct the named query parameter and send it once. |
| 400 | invalid-query | q is required | Correct the named query parameter and send it once. |
| 400 | invalid-query | q must contain at least 1 character | Correct the named query parameter and send it once. |
| 400 | invalid-query | q must contain no more than 100 characters | Correct the named query parameter and send it once. |
| 400 | invalid-query | postcode prefix must contain between 1 and 4 digits | Correct the named query parameter and send it once. |
| 400 | invalid-query | q must be a postcode prefix or suburb name | Correct the named query parameter and send it once. |
| 400 | invalid-query | q must be supplied once | Correct the named query parameter and send it once. |
| 400 | invalid-query | postcode must be supplied once | Correct the named query parameter and send it once. |
| 400 | invalid-query | limit must be supplied once | Correct the named query parameter and send it once. |
| 400 | invalid-query | lat must be supplied once | Correct the named query parameter and send it once. |
| 400 | invalid-query | lng must be supplied once | Correct the named query parameter and send it once. |
| 400 | invalid-query | lat is required | Correct the named query parameter and send it once. |
| 400 | invalid-query | lng is required | Correct the named query parameter and send it once. |
| 400 | invalid-query | lat must be a number between -90 and 90 | Correct the named query parameter and send it once. |
| 400 | invalid-query | lng must be a number between -180 and 180 | Correct the named query parameter and send it once. |
| 414 | uri-too-long | id must contain no more than 100 characters | Use the uprn from an address record. |
| 400 | invalid-query | queries[${index}].q must not be empty | Supply address text after removing control characters. |
| 400 | invalid-id | id must contain between 1 and 100 characters | Use the uprn from an address record. |
| 400 | invalid-postcode | postcode must contain exactly 4 digits | Send a four-digit postcode as text, including leading zeros. |
| 400 | invalid-postcode | postcode must be supplied once | Send a four-digit postcode as text, including leading zeros. |
| 400 | invalid-boundary | layer must be mmm, remoteness, lga, sa2, sa3, sa4, federal, state, poa or sal | Use a listed layer and its official code. |
| 400 | invalid-boundary | MMM category must be between 1 and 7 | Use a listed layer and its official code. |
| 400 | invalid-boundary | code must contain 1 to 32 letters, digits, underscores or hyphens | Use a listed layer and its official code. |
| 400 | invalid-boundary | layer must be supplied once | Use a listed layer and its official code. |
| 400 | invalid-boundary | code must be supplied once | Use a listed layer and its official code. |
| 400 | invalid-body | body must be an object | Correct the JSON body. The index identifies the query, starting at zero. |
| 400 | invalid-body | queries must be an array | Correct the JSON body. The index identifies the query, starting at zero. |
| 400 | invalid-body | queries must contain between 1 and 100 items | Correct the JSON body. The index identifies the query, starting at zero. |
| 400 | invalid-body | queries[${index}] must be an object | Correct the JSON body. The index identifies the query, starting at zero. |
| 400 | invalid-body | queries[${index}].q must be supplied once | Correct the JSON body. The index identifies the query, starting at zero. |
| 400 | invalid-body | queries[${index}].q is required | Correct the JSON body. The index identifies the query, starting at zero. |
| 400 | invalid-body | queries[${index}].q must contain at least 3 characters | Correct the JSON body. The index identifies the query, starting at zero. |
| 400 | invalid-body | queries[${index}].q must contain no more than 200 characters | Correct the JSON body. The index identifies the query, starting at zero. |
| 400 | invalid-body | limit must be supplied once | Correct the JSON body. The index identifies the query, starting at zero. |
| 400 | invalid-body | limit must be a positive whole number | Correct the JSON body. The index identifies the query, starting at zero. |
| 400 | invalid-body | q must contain between 3 and 300 characters | Correct the JSON body. The index identifies the query, starting at zero. |
| 400 | invalid-body | limit must be a whole number between 1 and 20 | Correct the JSON body. The index identifies the query, starting at zero. |
| 400 | invalid-body | The request could not be processed. | Correct the JSON body. The index identifies the query, starting at zero. |
| 413 | invalid-body | body must contain no more than 4096 bytes | Shorten the parse body. |
| 415 | unsupported-media-type | Send application/json. | Send parse and bulk requests with Content-Type application/json. |
| 404 | postcode-not-found | Postcode was not found. | Check the postcode or search by suburb. |
| 404 | boundary-not-found | Boundary was not found. | Check the layer and code, and whether boundary data is loaded. |
| 503 | address-lookup-unavailable | Address data has not been loaded. | Check /ready and retry after the data is loaded. |
| 500 | address-lookup-error | Address lookup failed. | Retry later. Contact support with the request id if it continues. |
| 500 | address-lookup-error | Nearest address lookup failed. | Retry later. Contact support with the request id if it continues. |
| 500 | address-lookup-error | Postcode lookup failed. | Retry later. Contact support with the request id if it continues. |
| 500 | address-lookup-error | Boundary lookup failed. | Retry later. Contact support with the request id if it continues. |
| 500 | parse-error | Address parsing failed. | Retry later. Contact support with the request id if it continues. |
| 503 | jobs-unavailable | Address jobs are unavailable. | Check service status. Check your recent jobs before resubmitting. |
| 503 | jobs-unavailable | The address job could not be submitted. | Check service status. Check your recent jobs before resubmitting. |
| 415 | invalid-content-type | Send a CSV file with Content-Type text/csv. | Upload raw CSV with the text/csv header. |
| 403 | account-required | An account API key is required for address jobs. | Use a server key from an account with jobs scope. |
| 400 | invalid-job-headers | Supply a column and filename within the allowed lengths. | Use 1 to 256 characters for x-job-column and 1 to 255 for x-job-filename, without control characters in the filename. |
| 404 | job-not-found | The address job was not found. | Use a job id belonging to this account. |
| 400 | invalid-csv | A quote must start a CSV field. | Correct the CSV and the x-job-column header, then upload it again. |
| 400 | invalid-csv | Unexpected text after a quoted CSV field. | Correct the CSV and the x-job-column header, then upload it again. |
| 400 | invalid-csv | A quoted CSV field was not closed. | Correct the CSV and the x-job-column header, then upload it again. |
| 400 | invalid-csv | Send a UTF-8 CSV file. | Correct the CSV and the x-job-column header, then upload it again. |
| 400 | invalid-csv | The CSV file is empty. | Correct the CSV and the x-job-column header, then upload it again. |
| 400 | invalid-csv | The address column was not found. | Correct the CSV and the x-job-column header, then upload it again. |
| 400 | invalid-csv | The address column must have a unique name. | Correct the CSV and the x-job-column header, then upload it again. |
| 400 | invalid-csv | CSV rows must have the same number of columns. | Correct the CSV and the x-job-column header, then upload it again. |
| 400 | invalid-csv | The CSV file must contain at least one address row. | Correct the CSV and the x-job-column header, then upload it again. |
| 413 | csv-too-large | CSV files must be no larger than 10,485,760 bytes. | Split the CSV into smaller files. |
| 413 | csv-too-large | CSV files must contain no more than 50,000 rows. | Split the CSV into smaller files. |
| 503 | database-unavailable | No message field; /ready returns ok: false and ready: false. | Check service status and retry later. |
| 500 | internal-error | Something went wrong. | Contact support with the request id if retrying does not help. |
| 404 | not-found | Route not found. | Check the HTTP method and endpoint path. |
| 4xx | bad-request | ${error.message} | Correct the request described by the message. This is the fallback code when an error has no code. |
| 413 | FST_ERR_CTP_BODY_TOO_LARGE | Request body is too large | Reduce the body: bulk accepts 64 KiB and CSV accepts 10,485,760 bytes. |
| 415 | FST_ERR_CTP_INVALID_MEDIA_TYPE | Unsupported Media Type | Send the content type required by the endpoint. |
| 400 | FST_ERR_CTP_EMPTY_JSON_BODY | Body cannot be empty when content-type is set to 'application/json' | Send a JSON object. |
| 400 | FST_ERR_CTP_INVALID_JSON_BODY | Body is not valid JSON but content-type is set to 'application/json' | Fix the JSON syntax. |
| 400 | FST_ERR_CTP_INVALID_CONTENT_LENGTH | Request body size did not match Content-Length | Let your HTTP client set Content-Length. |
| 415 | -32000 | Content-Type must be application/json. | Send MCP requests as application/json. |
| 406 | -32000 | Not Acceptable: Client must accept both application/json and text/event-stream | Include application/json and text/event-stream in the MCP Accept header. |
| 400 | -32000 | Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')}) | Use a supported MCP protocol version. The message lists the accepted versions. |
| 405 | -32000 | Method not allowed. This MCP server is stateless; use POST. | Use POST /mcp. |
| 400 | -32700 | Invalid JSON body. | Fix the JSON syntax of the MCP request. |
| 400 | -32700 | Parse error: Invalid JSON-RPC message | Send a valid JSON-RPC 2.0 request. Inspect any error data for details. |
| 400 | -32700 | Parse error | Send a valid JSON-RPC 2.0 request. Inspect any error data for details. |
| 400 | -32600 | Invalid Request: Batch must not exceed ${MAX_BATCH_SIZE} messages | Split the batch and send initialization on its own. |
| 400 | -32600 | Invalid Request: Only one initialization request is allowed | Split the batch and send initialization on its own. |
| 200 | -32601 | Method not found | Use an MCP method advertised by this server. |
| 200 | -32602 | MCP error -32602: Resource ${uri} not found | List the available resources or prompts and check the supplied arguments. |
| 200 | -32602 | MCP error -32602: Prompt ${request.params.name} not found | List the available resources or prompts and check the supplied arguments. |
| 200 | -32602 | MCP error -32602: Invalid arguments for prompt ${request.params.name}: ${errorMessage} | List the available resources or prompts and check the supplied arguments. |
| 200 | -32602 | MCP error -32602: Tool ${request.params.name} not found | List the available tools and use an advertised name. |
| 200 | -32602 | MCP error -32602: Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage} | Check the tool input against its advertised schema. |
| 200 | -32603 | The OpenAPI document is unavailable. | Retry reading the MCP openapi resource later. |
| 413 | -32603 | The request body exceeds 1 MB. | Shorten the MCP request body. |
| 429 / 500 | -32603 | The MCP request could not be processed. | Respect Retry-After when present. Contact support with the request id if it continues. |
| 502 | api-unavailable | The address API could not be reached. | Retry the MCP tool later. |
| 502 | invalid-api-response | The address API returned an unexpected response. | Contact support with the request id. |
| 4xx/5xx | mcp-usage-unavailable | MCP usage is unavailable. Try again shortly. | Check the request and service status. |
| 400 | invalid-query | q must not be empty | Correct the named query parameter and send it once. |
| 429 | quota-exceeded | This API key has insufficient monthly job rows for this file. | Wait until the monthly reset or change your plan. Respect Retry-After. |
| 409 | job-not-ready | The address job is not ready to download. | Check the request and service status. |
| 414 | uri-too-long | The request path is too long. | Use the uprn from an address record. |
Data and changes
Reloaded each quarter. Release metadata below is read from the repository through the readiness check for this page request. What changed.
| Release | Loaded | Addresses |
|---|---|---|
| AUG 2026 - Geoscape G-NAF - GDA2020 | 2026-10-25T12:11:35.160Z | 16091511 |
G-NAF is published by Geoscape Australia as open data through data.gov.au under the G-NAF End User Licence Agreement. Boundaries come from the Australian Bureau of Statistics ASGS Edition 3 or Edition 4 allocation files (CC BY 4.0), as identified by boundaries.edition, and the Modified Monash Model 2023 by Mesh Block from the Department of Health and Aged Care (CC BY 2.5 AU). Read the usage policy for attribution and the mail-use condition.