Developer docs
Resolve an address, classify a zone, run a bulk job, verify a webhook. Every call below is a real route of the AddressIQ service.
Alpha, and synthetic data. The routes are checked against the OpenAPI spec by an automated contract test, run on a test database. The bundled data is eight made-up Australian addresses and five zones; every response says "dataMode": "synthetic" or "live". No live provider is connected yet. Examples assume a running AddressIQ service. The public demo on this site serves a subset of the routes with the same shapes, and has no bulk jobs, quotas, request ids or webhooks.
Quick start
Set the base URL and your key. Keys are issued by an administrator today; there is no self-serve sign-up yet.
# your instance and key
export ADDRESSIQ_URL=https://your-addressiq-host
export ADDRESSIQ_KEY=your-api-key
1. Check it is up
No key needed. This is a liveness check and does not query the database. /v1/ready is the deeper readiness check.
curl "$ADDRESSIQ_URL/v1/health"
2. Resolve one address
The answer is one normalised address with suburb, state, postcode, coordinates, precision and validationStatus, plus attempts (the provider hops tried), requestId and dataMode. Send latitude and longitude instead for a reverse lookup. An address nothing matches returns 404 unresolvable with suggestions.
curl -s "$ADDRESSIQ_URL/v1/address/resolve" \
-H "x-api-key: $ADDRESSIQ_KEY" \
-H "content-type: application/json" \
-d '{"address": "18 Beach Rd, Bondi NSW 2026"}'
3. Classify a point into zones
The default mode, single-winner, returns the heaviest zone as winner. Add "mode": "multi-tag" to get every zone that contains the point, ranked by weight, with no single winner.
curl -s "$ADDRESSIQ_URL/v1/zones/classify" \
-H "x-api-key: $ADDRESSIQ_KEY" \
-H "content-type: application/json" \
-d '{"latitude": -33.8915, "longitude": 151.2767, "mode": "multi-tag"}'
Bulk jobs
For up to 100 addresses in one call use POST /v1/address/bulk-resolve. For larger sets, start an asynchronous job: up to 50,000 rows as JSON or CSV, depending on your plan. Both need the bulk scope, and every row counts against your quota.
Create a job with an idempotency key
Send an Idempotency-Key (8 to 128 characters of A-Z a-z 0-9 . _ : -) so a network retry never starts a second job. The reply is 202 with the job in queued state.
curl -s "$ADDRESSIQ_URL/v1/bulk/jobs" \
-H "x-api-key: $ADDRESSIQ_KEY" \
-H "content-type: application/json" \
-H "Idempotency-Key: my-batch-2026-11-05" \
-d '{"addresses": ["18 Beach Rd, Bondi NSW 2026", "nowhere at all 99999"]}'
# CSV works too: a header row with an "address" column
curl -s "$ADDRESSIQ_URL/v1/bulk/jobs" \
-H "x-api-key: $ADDRESSIQ_KEY" \
-H "content-type: text/csv" \
-H "Idempotency-Key: my-batch-2026-11-05-csv" \
--data-binary @addresses.csv
- Same key and same body: you get the first response again, with
Idempotent-Replayed: true, and no second job. - Same key and a different body: 422. Same key while the first request is still running: 409.
- Keys are per API key and expire after 24 hours.
Poll, then download
The status is queued, running, done, failed or cancelled, with counts for total, resolved, unresolved and pending. A job that stops because the quota ran out is failed with error set to quota_exceeded. A worker that restarts mid-job resumes it.
JOB=<id from the 202 reply> # poll every 5 seconds until the job stops (jq is optional) while true; do S=$(curl -s "$ADDRESSIQ_URL/v1/bulk/jobs/$JOB" -H "x-api-key: $ADDRESSIQ_KEY" | jq -r .status) echo "status: $S" case "$S" in done|failed|cancelled) break;; esac sleep 5 done # results: ndjson (default) or csv curl -s "$ADDRESSIQ_URL/v1/bulk/jobs/$JOB/results?format=csv" -H "x-api-key: $ADDRESSIQ_KEY" -o results.csv # rows that did not resolve, with the reason curl -s "$ADDRESSIQ_URL/v1/bulk/jobs/$JOB/exceptions" -H "x-api-key: $ADDRESSIQ_KEY" # stop a running job curl -s -X POST "$ADDRESSIQ_URL/v1/bulk/jobs/$JOB/cancel" -H "x-api-key: $ADDRESSIQ_KEY"
Tested with 1,000 rows (including bad rows, a restart mid-job, cancel and tenant isolation) on a test database. 10,000 rows and several service instances have not been run. A job id that belongs to another tenant answers 404, the same as an unknown id.
Webhooks
A tenant administrator registers an HTTPS endpoint with the admin action createWebhook(url, events). The events are bulk.job.completed and bulk.job.failed. The signing secret is returned once and is stored encrypted on our side; rotateWebhookSecret issues a new one, disableWebhook stops deliveries, and testWebhook sends one signed webhook.test. Registration is an admin API action; this page does not claim a console screen for it.
Each delivery is a JSON POST with these headers:
| Header | Value |
|---|---|
X-AddressIQ-Event | The event name. |
X-AddressIQ-Delivery | A delivery id. |
X-AddressIQ-Timestamp | Unix seconds. |
X-AddressIQ-Signature | v1= and the hex HMAC-SHA256 of <timestamp>.<raw body>, keyed with your secret. |
Answer 2xx within 5 seconds. Anything else, redirects included, is retried with exponential backoff, up to 8 attempts. Only public addresses on ports 443 and 8443 are allowed. Always verify the signature on the raw body, and reject old timestamps:
const crypto = require('crypto');
const express = require('express');
function verify(secret, ts, rawBody, sig) {
// reject old or replayed deliveries
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const want = 'v1=' + crypto.createHmac('sha256', secret)
.update(`${ts}.${rawBody}`).digest('hex');
return want.length === sig.length &&
crypto.timingSafeEqual(Buffer.from(want), Buffer.from(sig));
}
const app = express();
// express.raw keeps the exact bytes that were signed
app.post('/addressiq-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const ok = verify(
process.env.ADDRESSIQ_WEBHOOK_SECRET,
req.get('x-addressiq-timestamp'),
req.body.toString('utf8'),
req.get('x-addressiq-signature') || ''
);
if (!ok) return res.sendStatus(401);
// handle the event, then answer fast
res.sendStatus(204);
});
app.listen(3000);
Signing, retries, the delivery log and the block on private addresses are tested against local receivers. They have not been tested against real internet receivers.
MCP tools for AI agents
The MCP server gives any MCP-capable assistant eight read-only tools: validate_address, autocomplete_address, reverse_geocode, zone_for_point, zone_for_address, enrich_address, bulk_validate and drive_distance. It runs over stdio on the machine that runs the assistant and needs Node 18 or later, with no npm packages.
{
"mcpServers": {
"addressiq": {
"command": "node",
"args": ["/path/to/addressiq/mcp/server.js"],
"env": {
"ADDRESSIQ_URL": "https://your-addressiq-host",
"ADDRESSIQ_KEY": "your-api-key"
}
}
}
}
- The key comes from the environment only and is never logged or returned. A plain-http URL is refused, except for localhost.
- The key’s tenant, scopes and limits apply as for the REST API. “Not found” is a normal answer (
found: false), never a guess. - No tool can create, change or delete anything.
Tested against a mock API. Not yet run against the live service. There is no hosted transport; that would need per-tenant sign-in.
Authentication and scopes
Send x-api-key: <key> on every call except GET /v1/health and GET /v1/ready. A key belongs to one tenant and sees only that tenant’s data. The raw key is shown once when issued; only a hash is stored. A key can be pinned to web origins, so a browser call from another site gets 403. Server-to-server calls carry no origin and pass.
Each key carries a list of scopes. The scope each operation needs is in the spec as x-required-scope.
| Route | Scope |
|---|---|
POST /v1/address/autocomplete | autocomplete |
POST /v1/address/resolve | resolve |
POST /v1/address/bulk-resolve | bulk |
/v1/bulk/jobs and its status, results, exceptions and cancel routes | bulk |
POST /v1/address/enrich | enrich |
POST /v1/zones/classify | classify |
POST /v1/route/distance, /directions, /matrix | route |
POST /v1/connect/verify | resolve, plus bulk for more than one record |
GET /v1/whoami, /v1/metrics, /v1/connect/systems | Any valid key |
GET /v1/health, /v1/ready | None |
GET /v1/whoami shows the key’s tenant, scopes, plan and quota. GET /v1/metrics shows your own tenant’s counters only. The Prometheus /metrics route is for the platform operator and refuses tenant keys.
Request ids
Send x-request-id (8 to 128 characters of A-Z a-z 0-9 . _ : -) to trace a call. It is echoed in the response header and recorded in the id-only access log. Without one, or with anything else, the service issues a UUID. Quote it when you ask for help, and look it up in the console trace.
Errors
Every error body is { "error": "<message>" }. An unexpected internal error is always { "error": "internal error", "requestId": "<id>" } with nothing else; quote that id when you ask for help. The 404 on resolve adds attempts, dataMode and suggestions. The rate-limit 429 adds limitPerMin. The quota 429 is { "error": "quota_exceeded", "resetsAt": "<ISO time>" }.
| Status | Meaning |
|---|---|
| 400 | Bad input: a missing field, an out-of-range value, or more than 100 items. The message says which. |
| 401 | No key, an unknown key, or a suspended key. |
| 403 | Scope not granted, origin not allowed, or tenant suspended. The body never names the missing capability. |
| 404 | An unresolvable address, an unknown id, or a bulk job that is not yours (the same answer as an unknown id). |
| 409 | Idempotency-Key: the first request with this key is still running. |
| 413 | Body larger than the limit (default 1 MB). |
| 422 | Idempotency-Key reused with a different body. |
| 429 | Per-key rate limit (wait for the 60-second window) or the tenant’s monthly quota is used up (quota_exceeded, wait for resetsAt). |
| 500 | An unexpected internal error. The body is the generic text above and the request id; the detail is only in our server log. In a batch, the failed item says internal error. |
| 502 | The road-routing provider is unavailable. Routes only; the message gives no upstream detail. |
| 503 | /v1/ready only: database unreachable, schema missing, or the pod is draining. |
Rate limits and quotas
Rate limit. Each key has a per-minute limit on a 60-second sliding window. Over the limit returns 429 and the request is not metered. Row limits: 100 addresses or records per call, and 10 by 10 for the route matrix.
Quota. Each tenant is on a plan with a monthly call quota. Every metered call counts, bulk job rows included, across all of the tenant’s keys. The month runs on UTC calendar months. Every authenticated response carries X-Quota-Limit, X-Quota-Remaining and X-Quota-Reset (Unix seconds, the first day of next month UTC). Reads that meter nothing (whoami, metrics, connect/systems, job status and downloads) still answer over quota.
The plan shape in the product today. These are default limits that an administrator can override per tenant. They are the structure of the plans, not prices and not a commitment.
Sandbox
- Calls a month
- 2,500
- Requests a minute
- 60
- Rows per bulk job
- 1,000
Standard
- Calls a month
- 100,000
- Requests a minute
- 600
- Rows per bulk job
- 50,000
Enterprise
- Calls a month
- 5,000,000
- Requests a minute
- 3,000
- Rows per bulk job
- 50,000
Limits: the quota is counted from the database and cached for 5 seconds in each instance, so with several instances a tenant can overshoot its quota by about 5 seconds of traffic. The per-key rate limit is counted in memory in each instance, so with several instances the effective limit is the instance count times the limit; the monthly quota is the real cap. A key cannot be issued with a per-minute limit above its plan. Tested on a test database with one process; no test runs several instances.
OpenAPI and Postman
The OpenAPI 3.0.3 spec describes every public /v1 route and is the source of truth. A contract test starts the real service and calls every operation. It fails if the code returns a field the spec does not declare, stops returning a required field, registers a route the spec lacks, or the spec lists one the code does not register. The Postman collection is generated from the spec.
- Swagger UI, Redoc, Stoplight, Insomnia and Bruno: import
openapi.json. - Postman: import the collection, then set the variables
baseUrlandapiKey. Each request has a status check.
The spec documents the service. The public edge demo ignores scopes, has no request ids or metrics, and serves only a subset of the routes.
Questions about the API or a security review? See the trust page, or write to addressiq@vrdigital.solutions.