{
  "openapi": "3.1.0",
  "info": {
    "title": "WattleAddr Address API",
    "version": "1.0.0",
    "summary": "Australian address autocomplete, retrieve and verification on the G-NAF dataset.",
    "description": "Australian address autocomplete, retrieve and verification, built on the Geocoded National Address File (G-NAF) and served entirely from Australian infrastructure.\n\n## Billing model\n\nBilling is per **session**, not per request — the same unit Google Places uses.\n\n1. Generate a session token (any opaque string; a UUID is ideal) when the user starts typing.\n2. Pass it as `session` on every `autocomplete` call. **Autocomplete is never billed**, however many keystrokes it takes.\n3. Pass the same token to `retrieve` (or `verify`). That call bills **once**. Repeating it with the same token does not bill again.\n\nCalling `retrieve` with **no** `session` always bills, because there is nothing to deduplicate against. Always send a session token.\n\n## Key types\n\n- **Secret keys** (`waddr_sk_…`) are for server-side use. They may carry an IP allowlist.\n- **Publishable keys** (`waddr_pk_…`) are safe in a browser. They are restricted to the domains you configure, checked against the request `Origin`/`Referer`.\n\n## Coverage boundary\n\nG-NAF is a register of **physical** addresses. It contains no PO Boxes, GPO Boxes, Locked Bags or Private Bags, and it says nothing about whether Australia Post delivers to an address. Queries for postal addresses return `200` with an empty result and a `postal_address_unsupported` notice — see the `Notice` schema.",
    "contact": {
      "name": "WattleAddr",
      "url": "https://wattleaddr.com.au",
      "email": "info@rrsols.com.au"
    },
    "termsOfService": "https://wattleaddr.com.au/legal/terms",
    "license": {
      "name": "WattleAddr API Terms of Service",
      "url": "https://wattleaddr.com.au/legal/terms"
    }
  },
  "servers": [
    {
      "url": "https://api.wattleaddr.com.au",
      "description": "Production (Australia)"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "apiKeyHeader": []
    },
    {
      "apiKeyQuery": []
    }
  ],
  "tags": [
    {
      "name": "Addresses",
      "description": "Autocomplete, retrieve and verify."
    },
    {
      "name": "Service",
      "description": "Service health and dataset vintage."
    }
  ],
  "paths": {
    "/v1/addresses/autocomplete": {
      "get": {
        "tags": [
          "Addresses"
        ],
        "operationId": "autocompleteAddresses",
        "summary": "Suggest addresses as the user types",
        "description": "Type-ahead suggestions. **Never billed**, so call it on every keystroke. Pass a `session` token and reuse it on the `retrieve` call that follows.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "What the user has typed. Minimum 3 characters.",
            "schema": {
              "type": "string",
              "minLength": 3
            },
            "example": "1 martin pl sydney"
          },
          {
            "name": "session",
            "in": "query",
            "required": false,
            "description": "Opaque session token tying these keystrokes to the eventual billable retrieve.",
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "example": "0f8c1c2e-6a3f-4a1b-9a1e-3c9f5b2d7e41"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum suggestions to return.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20,
              "default": 8
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "Restrict to one state or territory.",
            "schema": {
              "type": "string",
              "enum": [
                "NSW",
                "VIC",
                "QLD",
                "WA",
                "SA",
                "TAS",
                "ACT",
                "NT"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Suggestions, possibly empty. An empty list with a `notice` means the query can never match.",
            "headers": {
              "X-WattleAddr-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-WattleAddr-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutocompleteResponse"
                },
                "examples": {
                  "suggestions": {
                    "summary": "Normal result",
                    "value": {
                      "session": "0f8c1c2e-6a3f-4a1b-9a1e-3c9f5b2d7e41",
                      "query": "1 martin pl sydney",
                      "suggestions": [
                        {
                          "id": "GANSW706124693",
                          "formatted": "1 Martin Pl, Sydney NSW 2000",
                          "score": 42.66
                        }
                      ]
                    }
                  },
                  "postalAddress": {
                    "summary": "PO Box — absent from G-NAF by design",
                    "value": {
                      "session": "0f8c1c2e-6a3f-4a1b-9a1e-3c9f5b2d7e41",
                      "query": "PO Box 123 Parkes NSW",
                      "suggestions": [],
                      "notice": {
                        "code": "postal_address_unsupported",
                        "kind": "PO Box",
                        "message": "PO Box addresses aren’t in the G-NAF dataset, which covers physical street addresses only. Enter a street address instead."
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/addresses/{id}": {
      "get": {
        "tags": [
          "Addresses"
        ],
        "operationId": "retrieveAddress",
        "summary": "Retrieve the full record for a suggestion",
        "description": "Returns the structured address for an id returned by `autocomplete`.\n\n**This is the billable call.** With a `session` token it bills once per session; without one it bills every time.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "G-NAF address detail PID, taken from a suggestion's `id`.",
            "schema": {
              "type": "string"
            },
            "example": "GANSW706124693"
          },
          {
            "name": "session",
            "in": "query",
            "required": false,
            "description": "The same token used during autocomplete. Omitting it means every call bills.",
            "schema": {
              "type": "string",
              "maxLength": 64
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The address record.",
            "headers": {
              "X-WattleAddr-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-WattleAddr-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetrieveResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/addresses/verify": {
      "post": {
        "tags": [
          "Addresses"
        ],
        "operationId": "verifyAddress",
        "summary": "Match a free-text address to a canonical record",
        "description": "Takes an address as a single string and returns the best canonical G-NAF match with structured components. Useful for cleaning addresses you already hold.\n\nBillable on the same session rules as `retrieve`, and **only when it matches** — a miss is never charged.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifyRequest"
              },
              "example": {
                "address": "100 collins st melbourne",
                "session": "0f8c1c2e-6a3f-4a1b-9a1e-3c9f5b2d7e41"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Match result. `matched: false` is a normal outcome, not an error.",
            "headers": {
              "X-WattleAddr-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-WattleAddr-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/status": {
      "get": {
        "tags": [
          "Service"
        ],
        "operationId": "getStatus",
        "summary": "Service health and dataset vintage",
        "description": "Unauthenticated. Reports engine and database health, which G-NAF release is being served, and which Australian region is serving it.",
        "security": [],
        "responses": {
          "200": {
            "description": "Service status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusResponse"
                },
                "example": {
                  "status": "operational",
                  "engine": {
                    "name": "addressr",
                    "ok": true
                  },
                  "database": true,
                  "gnaf_release": "2026-05",
                  "region": "au-syd-1"
                }
              }
            }
          },
          "503": {
            "description": "`engine_unavailable` — the address engine or database is unreachable. The body still parses as an Error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "engine_unavailable",
                    "message": "Address engine unavailable."
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer waddr_sk_live_…`. Preferred for server-side calls."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Alternative to the bearer header."
      },
      "apiKeyQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "key",
        "description": "Query-string key. Intended for **publishable** keys in the browser: it avoids a CORS preflight. Never put a secret key in a URL."
      }
    },
    "headers": {
      "QuotaLimit": {
        "description": "Billable lookups included in the plan this billing period. Absent on plans with no cap.",
        "schema": {
          "type": "integer"
        },
        "example": 50000
      },
      "QuotaRemaining": {
        "description": "Billable lookups left this period.",
        "schema": {
          "type": "integer"
        },
        "example": 49873
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer"
        },
        "example": 1
      }
    },
    "schemas": {
      "Suggestion": {
        "type": "object",
        "required": [
          "id",
          "formatted"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "G-NAF address detail PID. Pass to `retrieve`.",
            "example": "GANSW706124693"
          },
          "formatted": {
            "type": "string",
            "description": "Single-line address for display.",
            "example": "1 Martin Pl, Sydney NSW 2000"
          },
          "score": {
            "type": "number",
            "description": "Relative relevance within this response. Not comparable across queries.",
            "example": 42.66
          }
        }
      },
      "AddressComponents": {
        "type": "object",
        "description": "Structured parts. Every field may be null — most addresses have no flat or level.",
        "properties": {
          "flat_number": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "level_number": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "street_number": {
            "type": [
              "string",
              "null"
            ],
            "example": "1"
          },
          "street_name": {
            "type": [
              "string",
              "null"
            ],
            "example": "Martin"
          },
          "street_type": {
            "type": [
              "string",
              "null"
            ],
            "example": "Pl"
          },
          "locality": {
            "type": [
              "string",
              "null"
            ],
            "example": "Sydney"
          },
          "state": {
            "type": [
              "string",
              "null"
            ],
            "example": "NSW"
          },
          "postcode": {
            "type": [
              "string",
              "null"
            ],
            "example": "2000"
          }
        }
      },
      "AddressRecord": {
        "type": "object",
        "required": [
          "id",
          "formatted",
          "components",
          "geo",
          "confidence"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "GANSW706124693"
          },
          "formatted": {
            "type": "string",
            "example": "1 Martin Pl, Sydney NSW 2000"
          },
          "components": {
            "$ref": "#/components/schemas/AddressComponents"
          },
          "geo": {
            "type": [
              "object",
              "null"
            ],
            "description": "Coordinates (GDA2020). Null when the record has no geocode.",
            "required": [
              "lat",
              "lng"
            ],
            "properties": {
              "lat": {
                "type": "number",
                "example": -33.86785
              },
              "lng": {
                "type": "number",
                "example": 151.20732
              }
            }
          },
          "confidence": {
            "type": "string",
            "enum": [
              "high",
              "medium",
              "low"
            ],
            "description": "G-NAF's own confidence in the record, bucketed. Not a match score."
          }
        }
      },
      "Notice": {
        "type": "object",
        "description": "Explains a successful-but-empty result. Not an error: the status is 200 and every usual field is present. Additive — clients that ignore it behave exactly as before.",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "postal_address_unsupported"
            ],
            "description": "`postal_address_unsupported` — the query is for a PO Box, GPO Box, Post Office Box, Locked Bag or Private Bag. G-NAF holds physical addresses only, so no match is possible."
          },
          "kind": {
            "type": "string",
            "enum": [
              "PO Box",
              "GPO Box",
              "Post Office Box",
              "Locked Bag",
              "Private Bag"
            ],
            "description": "Which postal form was detected."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation, safe to show to an end user."
          }
        }
      },
      "AutocompleteResponse": {
        "type": "object",
        "required": [
          "session",
          "query",
          "suggestions"
        ],
        "properties": {
          "session": {
            "type": [
              "string",
              "null"
            ],
            "description": "Echo of the session token you sent."
          },
          "query": {
            "type": "string",
            "description": "Echo of the trimmed query."
          },
          "suggestions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Suggestion"
            }
          },
          "notice": {
            "$ref": "#/components/schemas/Notice"
          }
        }
      },
      "RetrieveResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/AddressRecord"
          },
          {
            "type": "object",
            "required": [
              "source",
              "gnaf_release",
              "billed"
            ],
            "properties": {
              "source": {
                "type": "string",
                "enum": [
                  "gnaf"
                ]
              },
              "gnaf_release": {
                "type": "string",
                "description": "Which quarterly G-NAF release answered this.",
                "example": "2026-05"
              },
              "billed": {
                "type": "boolean",
                "description": "Whether this call consumed a billable lookup."
              },
              "test": {
                "type": "boolean",
                "description": "Present and true only for test-mode keys, which never bill."
              }
            }
          }
        ]
      },
      "VerifyRequest": {
        "type": "object",
        "required": [
          "address"
        ],
        "properties": {
          "address": {
            "type": "string",
            "minLength": 3,
            "description": "Free-text address.",
            "example": "100 collins st melbourne"
          },
          "session": {
            "type": "string",
            "maxLength": 64,
            "description": "Session token, same semantics as retrieve."
          }
        }
      },
      "VerifyResponse": {
        "type": "object",
        "required": [
          "matched",
          "match",
          "confidence",
          "alternatives",
          "source",
          "gnaf_release",
          "billed"
        ],
        "properties": {
          "matched": {
            "type": "boolean"
          },
          "match": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/AddressRecord"
              },
              {
                "type": "null"
              }
            ]
          },
          "confidence": {
            "type": "string",
            "enum": [
              "high",
              "medium",
              "low"
            ]
          },
          "alternatives": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Suggestion"
            },
            "description": "Runner-up candidates."
          },
          "source": {
            "type": "string",
            "enum": [
              "gnaf"
            ]
          },
          "gnaf_release": {
            "type": "string",
            "example": "2026-05"
          },
          "billed": {
            "type": "boolean",
            "description": "False on a miss and on a postal-address notice — neither is charged."
          },
          "notice": {
            "$ref": "#/components/schemas/Notice"
          },
          "test": {
            "type": "boolean"
          }
        }
      },
      "StatusResponse": {
        "type": "object",
        "required": [
          "status",
          "engine",
          "database",
          "gnaf_release",
          "region"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "operational",
              "degraded"
            ]
          },
          "engine": {
            "type": "object",
            "required": [
              "name",
              "ok"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "ok": {
                "type": "boolean"
              }
            }
          },
          "database": {
            "type": "boolean"
          },
          "gnaf_release": {
            "type": "string",
            "example": "2026-05"
          },
          "region": {
            "type": "string",
            "description": "Australian region actually serving the request.",
            "example": "au-syd-1"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "bad_request",
                  "unauthorized",
                  "invalid_key",
                  "forbidden_referrer",
                  "forbidden_ip",
                  "test_mode_expired",
                  "feature_unavailable",
                  "quota_exceeded",
                  "rate_limited",
                  "not_found",
                  "engine_unavailable",
                  "internal"
                ],
                "description": "Stable, safe to switch on."
              },
              "message": {
                "type": "string",
                "description": "Human-readable. Wording may change; the code will not."
              }
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "`bad_request` — a parameter is missing or too short (`q` and `address` need at least 3 characters).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "bad_request",
                "message": "Query `q` must be at least 3 characters."
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "`unauthorized` — no key supplied. `invalid_key` — the key is unknown or revoked.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_key",
                "message": "Invalid or revoked API key."
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "`forbidden_referrer` — the request `Origin`/`Referer` is not in this publishable key's allowed domains. `forbidden_ip` — the caller's IP is not in this secret key's allowlist.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "forbidden_referrer",
                "message": "This origin is not allowed for this publishable key."
              }
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "`quota_exceeded` — the plan's monthly lookup quota is used up. `feature_unavailable` — the endpoint is not in this plan. `test_mode_expired` — the workspace's free test-mode window has closed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "quota_exceeded",
                "message": "Monthly lookup quota reached on the Starter plan. Upgrade to keep serving lookups."
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "`not_found` — no address with that id.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "not_found",
                "message": "No address with that id."
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "`rate_limited` — the per-key burst limit was exceeded. Honour `Retry-After`.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "Too many requests, slow down."
              }
            }
          }
        }
      }
    }
  }
}
