{
  "openapi": "3.0.3",
  "info": {
    "title": "AddressIQ /v1 API",
    "version": "0.1.0",
    "description": "Address validation, geocoding, zone classification, enrichment, routing estimates and host-system connectors.\n\n**All data in the demo is synthetic.** The bundled dataset is eight made-up-coordinate Australian addresses and five seeded zones; nothing is looked up from a real provider unless the deployment sets `AIQ_LIVE=1` with provider keys. Every response that carries `dataMode` says `\"synthetic\"` or `\"live\"`. Do not treat demo results as real addresses.\n\nA public edge demo (Cloudflare Worker) serves most of these routes with the same shapes. It has no `/v1/metrics`, no `requestId`, ignores scopes and uses the public demo key. The differences are pinned by `test/parity.test.js`.\n\nThe contract test `test/openapi.test.js` calls every route below and fails if this file and the service disagree.",
    "contact": {
      "name": "VR Digital"
    }
  },
  "servers": [
    {
      "url": "http://localhost:4004",
      "description": "Local CAP service (npm run watch)"
    },
    {
      "url": "https://addressiq.pages.dev",
      "description": "Public edge demo (synthetic data, subset of routes)"
    }
  ],
  "tags": [
    {
      "name": "Address"
    },
    {
      "name": "Zones"
    },
    {
      "name": "Route"
    },
    {
      "name": "Connect"
    },
    {
      "name": "Bulk"
    },
    {
      "name": "Operations"
    },
    {
      "name": "Signup"
    },
    {
      "name": "Billing"
    }
  ],
  "security": [
    {
      "ApiKey": []
    }
  ],
  "paths": {
    "/v1/address/autocomplete": {
      "post": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Address"
        ],
        "operationId": "autocompleteAddress",
        "summary": "Type-ahead address suggestions",
        "description": "Provider-abstracted suggestions for a partial address. Optional `limit` (1-10, default 6) and `proximity` (nearest-first).",
        "x-required-scope": "autocomplete",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AutocompleteRequest"
              },
              "examples": {
                "street": {
                  "summary": "Street fragment",
                  "value": {
                    "query": "18 Beach"
                  }
                },
                "nearest_first": {
                  "summary": "Nearest to Parramatta, two results",
                  "value": {
                    "query": "St",
                    "limit": 2,
                    "proximity": {
                      "latitude": -33.8136,
                      "longitude": 151.0034
                    }
                  }
                },
                "too_short": {
                  "summary": "One character returns an empty list",
                  "value": {
                    "query": "x"
                  }
                },
                "bad_limit": {
                  "summary": "limit out of range",
                  "value": {
                    "query": "St",
                    "limit": 11
                  },
                  "x-expect-status": 400
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Suggestions (possibly empty).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutocompleteResponse"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "suggestions": [
                        {
                          "label": "18 Beach Rd, Bondi NSW 2026",
                          "countryCode": "AU",
                          "provider": "google"
                        }
                      ],
                      "requestId": "456c5538-a9fc-4fbc-8243-61e32e6c6a1e",
                      "dataMode": "synthetic"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "Invalid option; or a bad `x-aiq-sub-client` (empty, unknown, another client's or archived).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "limit must be an integer 1..10"
                    }
                  },
                  "bad_sub_client": {
                    "value": {
                      "error": "unknown sub-client 'north' for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "Key lacks the required scope, its origin is not allowed, or its tenant is suspended. The body never names the missing capability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "413": {
            "description": "Body larger than the configured limit (AIQ_BODY_LIMIT, default 1mb).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "error": "payload too large",
                      "limit": "1mb"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded for the current 60-second window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "internal error"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          },
          {
            "$ref": "#/components/parameters/SubClient"
          }
        ]
      }
    },
    "/v1/address/resolve": {
      "post": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Address"
        ],
        "operationId": "resolveAddress",
        "summary": "Validate and geocode one address (forward or reverse)",
        "description": "Send `address` for a forward geocode, or `latitude` + `longitude` for a reverse geocode (reverse uses the same `resolve` scope; country assumed AU). The routing chain tries providers in rank order; `attempts` says how many hops it took. Unresolvable input returns 404 with closest-match suggestions.",
        "x-required-scope": "resolve",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResolveRequest"
              },
              "examples": {
                "forward": {
                  "summary": "Forward geocode",
                  "value": {
                    "address": "18 Beach Rd, Bondi NSW 2026"
                  }
                },
                "reverse": {
                  "summary": "Reverse geocode (lat/lng)",
                  "value": {
                    "latitude": -33.8915,
                    "longitude": 151.2767
                  }
                },
                "parramatta": {
                  "summary": "Another AU address",
                  "value": {
                    "address": "3/56 George St, Parramatta NSW 2150"
                  }
                },
                "unresolvable": {
                  "summary": "Nothing matches",
                  "value": {
                    "address": "nowhere at all 99999"
                  },
                  "x-expect-status": 404
                },
                "empty": {
                  "summary": "Neither form supplied",
                  "value": {},
                  "x-expect-status": 400
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Canonical address plus routing evidence.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResolveResponse"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "address": {
                        "fullAddress": "18 Beach Rd, Bondi NSW 2026",
                        "houseNumber": "18",
                        "streetName": "Beach Rd",
                        "addressLine2": "",
                        "suburb": "Bondi",
                        "state": "NSW",
                        "postcode": "2026",
                        "countryCode": "AU",
                        "latitude": -33.8915,
                        "longitude": 151.2767,
                        "plusCode": "4RRH475G+CM4",
                        "precision": "rooftop",
                        "confidence": 0.98,
                        "validationStatus": "verified",
                        "reasonCodes": "",
                        "providerCode": "gnaf",
                        "cacheable": true
                      },
                      "requestId": "456c5538-a9fc-4fbc-8243-61e32e6c6a1e",
                      "attempts": 1,
                      "cacheHit": false,
                      "dataMode": "synthetic"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "Neither address nor latitude+longitude supplied; or a bad `x-aiq-sub-client` (empty, unknown, another client's or archived).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "address, or latitude+longitude, required"
                    }
                  },
                  "bad_sub_client": {
                    "value": {
                      "error": "unknown sub-client 'north' for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "Key lacks the required scope, its origin is not allowed, or its tenant is suspended. The body never names the missing capability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "Unresolvable. `suggestions` lists the closest known addresses when running on synthetic data.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnresolvableError"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "unresolvable",
                      "attempts": 3,
                      "dataMode": "synthetic",
                      "suggestions": [
                        {
                          "label": "18 Beach Rd, Bondi NSW 2026",
                          "countryCode": "AU",
                          "provider": "suggested"
                        }
                      ]
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "413": {
            "description": "Body larger than the configured limit (AIQ_BODY_LIMIT, default 1mb).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "error": "payload too large",
                      "limit": "1mb"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded for the current 60-second window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "internal error"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          },
          {
            "$ref": "#/components/parameters/SubClient"
          }
        ]
      }
    },
    "/v1/address/capture": {
      "post": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Address"
        ],
        "operationId": "captureAddress",
        "summary": "Capture a location with no street address (provisional)",
        "description": "For places that have no street address: send a map pin, GPS fix or parcel reference. The service stores a provisional address (`validationStatus: provisional`, `precision: pin`) carrying the coordinates and an 11-digit Plus Code, and returns it with an id. Suburb, state, postcode and country are filled from a reverse lookup when one is available; a house number or street is never invented. An admin action (`revalidateProvisional`) promotes the record to `verified` when a rooftop address is later found within 50 m. Uses the `resolve` scope.",
        "x-required-scope": "resolve",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CaptureRequest"
              },
              "examples": {
                "pin": {
                  "summary": "Map pin with a descriptor",
                  "value": {
                    "latitude": -33.8915,
                    "longitude": 151.2767,
                    "descriptor": "Shed behind the surf club, north end",
                    "source": "pin"
                  }
                },
                "gps": {
                  "summary": "GPS fix with accuracy",
                  "value": {
                    "latitude": -33.8136,
                    "longitude": 151.0034,
                    "accuracyMetres": 8.5,
                    "source": "gps"
                  }
                },
                "badLatitude": {
                  "summary": "Latitude out of range",
                  "value": {
                    "latitude": 95,
                    "longitude": 151.2767
                  },
                  "x-expect-status": 400
                },
                "badSource": {
                  "summary": "Unknown source",
                  "value": {
                    "latitude": -33.8915,
                    "longitude": 151.2767,
                    "source": "satellite"
                  },
                  "x-expect-status": 400
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored provisional address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaptureResponse"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "address": {
                        "id": "0b6f3c2e-6a43-4f6f-9d7e-6c1f2f6a8b11",
                        "fullAddress": "Shed behind the surf club, north end",
                        "houseNumber": "",
                        "streetName": "",
                        "addressLine2": "",
                        "suburb": "Bondi",
                        "state": "NSW",
                        "postcode": "2026",
                        "countryCode": "AU",
                        "latitude": -33.8915,
                        "longitude": 151.2767,
                        "plusCode": "4RRH475G+CM4",
                        "precision": "pin",
                        "confidence": 0,
                        "validationStatus": "provisional",
                        "reasonCodes": "PROVISIONAL_CAPTURE",
                        "providerCode": "capture",
                        "cacheable": false,
                        "captureSource": "pin",
                        "accuracyMetres": null,
                        "descriptor": "Shed behind the surf club, north end",
                        "parcelId": null,
                        "datum": "WGS84"
                      },
                      "requestId": "456c5538-a9fc-4fbc-8243-61e32e6c6a1e",
                      "dataMode": "synthetic"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "latitude or longitude missing or out of range, accuracyMetres negative, descriptor over 500 characters, parcelId over 60, or an unknown source.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "latitude: number between -90 and 90 required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "Key lacks the required scope, its origin is not allowed, or its tenant is suspended. The body never names the missing capability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "413": {
            "description": "Body larger than the configured limit (AIQ_BODY_LIMIT, default 1mb).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "error": "payload too large",
                      "limit": "1mb"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded for the current 60-second window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "internal error"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ]
      }
    },
    "/v1/address/match": {
      "post": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Address"
        ],
        "operationId": "matchAddresses",
        "summary": "Find duplicate or similar addresses in a list",
        "description": "Compares the addresses you send with each other and reports which are probably the same place, for example `Unit 3, 12 Smith Street, Bondi NSW 2026` and `3/12 smith st bondi`. A rule-based comparison of the text supplied: it calls no provider and stores no address. The house number is a hard gate (different numbers never score above 0.5); street and suburb tolerate typos; a field missing on one side is neutral and a conflicting field lowers the score. `level` is `duplicate` (score at or above `threshold`) or `possible` (within 0.1 below it); `groups` are the connected sets of duplicate pairs, as indexes into the request list. Pairs further apart are not listed. The threshold is the tenant's `matchThreshold`, else the AppConfig key `match.threshold` (default 0.85), clamped to 0.5 to 1; a `threshold` in the request can only raise it. 2 to `match.maxRows` addresses (default 2000), each at most 500 characters. Needs scope `resolve` for 2 addresses and `bulk` for more than 2. Writes one usage-ledger entry (operation `match`, cost 0) that holds a count, never address text. English-language street grammar only.",
        "x-required-scope": "resolve",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MatchRequest"
              },
              "examples": {
                "duplicates": {
                  "summary": "Four addresses, one duplicate group",
                  "value": {
                    "addresses": [
                      {
                        "id": "crm-1",
                        "address": "Unit 3, 12 Smith Street, Bondi NSW 2026"
                      },
                      {
                        "id": "crm-2",
                        "address": "3/12 smith st bondi"
                      },
                      {
                        "id": "crm-3",
                        "address": "12 Smith Rd, Bondi NSW 2026"
                      },
                      "402 Collins St, Melbourne VIC 3000"
                    ]
                  }
                },
                "pair": {
                  "summary": "Two addresses, raised threshold",
                  "value": {
                    "addresses": [
                      "12 Smith St Bondi NSW",
                      "12 Smyth Street, Bondi 2026"
                    ],
                    "threshold": 0.9
                  }
                },
                "tooFew": {
                  "summary": "Only one address",
                  "value": {
                    "addresses": [
                      "12 Smith St Bondi NSW"
                    ]
                  },
                  "x-expect-status": 400
                },
                "badRow": {
                  "summary": "Second row is empty",
                  "value": {
                    "addresses": [
                      "12 Smith St Bondi NSW",
                      ""
                    ]
                  },
                  "x-expect-status": 400
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pairs that are duplicates or possible duplicates, and the duplicate groups.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchResponse"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "threshold": 0.85,
                      "total": 4,
                      "pairs": [
                        {
                          "a": {
                            "index": 0,
                            "id": "crm-1"
                          },
                          "b": {
                            "index": 1,
                            "id": "crm-2"
                          },
                          "score": 0.95,
                          "level": "duplicate",
                          "reasons": [
                            "STATE_MISSING",
                            "POSTCODE_MISSING"
                          ]
                        },
                        {
                          "a": {
                            "index": 0,
                            "id": "crm-1"
                          },
                          "b": {
                            "index": 2,
                            "id": "crm-3"
                          },
                          "score": 0.8,
                          "level": "possible",
                          "reasons": [
                            "STREET_TYPE_DIFFERS",
                            "UNIT_MISSING"
                          ]
                        },
                        {
                          "a": {
                            "index": 1,
                            "id": "crm-2"
                          },
                          "b": {
                            "index": 2,
                            "id": "crm-3"
                          },
                          "score": 0.8,
                          "level": "possible",
                          "reasons": [
                            "STREET_TYPE_DIFFERS",
                            "UNIT_MISSING",
                            "STATE_MISSING",
                            "POSTCODE_MISSING"
                          ]
                        }
                      ],
                      "truncated": false,
                      "groups": [
                        [
                          0,
                          1
                        ]
                      ],
                      "dataMode": "synthetic"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "Fewer than 2 or more than `match.maxRows` addresses, a row that is not a non-empty string of at most 500 characters (or `{ id?, address }`), an `id` over 100 characters, or a `threshold` that is not a number. The message names the first bad row.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "addresses[1].address: non-empty string of at most 500 characters required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "Key lacks the required scope, its origin is not allowed, or its tenant is suspended. The body never names the missing capability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "413": {
            "description": "Body larger than the configured limit (AIQ_BODY_LIMIT, default 1mb).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "error": "payload too large",
                      "limit": "1mb"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded for the current 60-second window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "internal error"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ]
      }
    },
    "/v1/address/bulk-resolve": {
      "post": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Address"
        ],
        "operationId": "bulkResolveAddresses",
        "summary": "Resolve up to 100 addresses in one call",
        "description": "Bounded concurrency; results keep input order. A failed item carries `error` instead of `address`; the call itself still returns 200.",
        "x-required-scope": "bulk",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkResolveRequest"
              },
              "examples": {
                "mixed": {
                  "summary": "One hit and one miss",
                  "value": {
                    "addresses": [
                      "18 Beach Rd, Bondi NSW 2026",
                      "nowhere at all 99999"
                    ]
                  }
                },
                "three": {
                  "summary": "Three AU addresses",
                  "value": {
                    "addresses": [
                      "18 Beach Rd, Bondi NSW 2026",
                      "3/56 George St, Parramatta NSW 2150",
                      "402 Collins St, Melbourne VIC 3000"
                    ]
                  }
                },
                "empty": {
                  "summary": "Empty list",
                  "value": {
                    "addresses": []
                  },
                  "x-expect-status": 400
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One result per input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkResolveResponse"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "results": [
                        {
                          "input": "18 Beach Rd, Bondi NSW 2026",
                          "address": {
                            "fullAddress": "18 Beach Rd, Bondi NSW 2026",
                            "houseNumber": "18",
                            "streetName": "Beach Rd",
                            "addressLine2": "",
                            "suburb": "Bondi",
                            "state": "NSW",
                            "postcode": "2026",
                            "countryCode": "AU",
                            "latitude": -33.8915,
                            "longitude": 151.2767,
                            "precision": "rooftop",
                            "confidence": 0.98,
                            "validationStatus": "verified",
                            "reasonCodes": "",
                            "providerCode": "gnaf",
                            "cacheable": true
                          },
                          "attempts": 1,
                          "cacheHit": false
                        },
                        {
                          "input": "nowhere at all 99999",
                          "error": "unresolvable",
                          "attempts": 3
                        }
                      ],
                      "dataMode": "synthetic"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "addresses missing, empty, or more than 100; or a bad `x-aiq-sub-client` (empty, unknown, another client's or archived).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "addresses: string[] required"
                    }
                  },
                  "bad_sub_client": {
                    "value": {
                      "error": "unknown sub-client 'north' for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "Key lacks the required scope, its origin is not allowed, or its tenant is suspended. The body never names the missing capability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "413": {
            "description": "Body larger than the configured limit (AIQ_BODY_LIMIT, default 1mb).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "error": "payload too large",
                      "limit": "1mb"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded for the current 60-second window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "internal error"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          },
          {
            "$ref": "#/components/parameters/SubClient"
          }
        ]
      }
    },
    "/v1/address/enrich": {
      "post": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Address"
        ],
        "operationId": "enrichAddress",
        "summary": "Resolve an address and attach zone, LGA, flood, zoning, electorate and statistical-area attributes",
        "description": "Send `address` (resolved first) or `addressId` (a stored address in your tenant). The attribute set is the enabled enrichment steps for your tenant.",
        "x-required-scope": "enrich",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EnrichRequest"
              },
              "examples": {
                "bondi": {
                  "summary": "Bondi house",
                  "value": {
                    "address": "18 Beach Rd, Bondi NSW 2026"
                  }
                },
                "parramatta": {
                  "summary": "Parramatta unit",
                  "value": {
                    "address": "3/56 George St, Parramatta NSW 2150"
                  }
                },
                "empty": {
                  "summary": "Neither address nor addressId",
                  "value": {},
                  "x-expect-status": 400
                },
                "unresolvable": {
                  "summary": "Nothing matches",
                  "value": {
                    "address": "nowhere at all 99999"
                  },
                  "x-expect-status": 404
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Canonical address and derived attributes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnrichResponse"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "address": {
                        "fullAddress": "18 Beach Rd, Bondi NSW 2026",
                        "houseNumber": "18",
                        "streetName": "Beach Rd",
                        "addressLine2": "",
                        "suburb": "Bondi",
                        "state": "NSW",
                        "postcode": "2026",
                        "countryCode": "AU",
                        "latitude": -33.8915,
                        "longitude": 151.2767,
                        "precision": "rooftop",
                        "confidence": 0.98,
                        "validationStatus": "verified",
                        "reasonCodes": "",
                        "providerCode": "gnaf",
                        "cacheable": true
                      },
                      "attributes": [
                        {
                          "attrKey": "zone",
                          "attrValue": "Coastal Flood Overlay (drawn)",
                          "source": "zone-classify"
                        },
                        {
                          "attrKey": "lga",
                          "attrValue": "Waverley Council",
                          "source": "lga-derive"
                        },
                        {
                          "attrKey": "floodRisk",
                          "attrValue": "Low",
                          "source": "flood-risk"
                        }
                      ],
                      "dataMode": "synthetic"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "Neither address nor addressId; or a bad `x-aiq-sub-client` (empty, unknown, another client's or archived).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "address or addressId required"
                    }
                  },
                  "bad_sub_client": {
                    "value": {
                      "error": "unknown sub-client 'north' for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "Key lacks the required scope, its origin is not allowed, or its tenant is suspended. The body never names the missing capability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "Address unresolvable, or addressId not found in your tenant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "unresolvable"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "413": {
            "description": "Body larger than the configured limit (AIQ_BODY_LIMIT, default 1mb).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "error": "payload too large",
                      "limit": "1mb"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded for the current 60-second window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "internal error"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          },
          {
            "$ref": "#/components/parameters/SubClient"
          }
        ]
      }
    },
    "/v1/zones/classify": {
      "post": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Zones"
        ],
        "operationId": "classifyZone",
        "summary": "Which zones contain this point",
        "description": "`single-winner` (default) picks the heaviest zone; `multi-tag` returns every match and no winner. Weight ties break deterministically (smaller area, then id). Which zones apply depends on the caller: platform zones and the tenant's shared zones always; this client's own zones; a sub-client's zones only when `x-aiq-sub-client` names it. Another client's or another tenant's zones never apply.",
        "x-required-scope": "classify",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClassifyRequest"
              },
              "examples": {
                "bondi": {
                  "summary": "Bondi, single winner",
                  "value": {
                    "latitude": -33.8915,
                    "longitude": 151.2767
                  }
                },
                "bondi_multi": {
                  "summary": "Bondi, all matching zones",
                  "value": {
                    "latitude": -33.8915,
                    "longitude": 151.2767,
                    "mode": "multi-tag"
                  }
                },
                "outside": {
                  "summary": "Open ocean: no zones",
                  "value": {
                    "latitude": 0,
                    "longitude": 0
                  }
                },
                "missing": {
                  "summary": "longitude missing",
                  "value": {
                    "latitude": -33.8
                  },
                  "x-expect-status": 400
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Match outcome.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassifyResponse"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "mode": "single-winner",
                      "matches": [
                        {
                          "zoneId": "5ea2af43-c7c8-4982-b477-2bc8a32c2952",
                          "name": "Coastal Flood Overlay (drawn)",
                          "zoneType": "custom",
                          "weight": 45
                        },
                        {
                          "zoneId": "b9b20671-ea85-4c05-a327-1100df2ca5b8",
                          "name": "Bondi locality",
                          "zoneType": "suburb",
                          "weight": 40
                        },
                        {
                          "zoneId": "381525c3-92f2-4ca3-933e-25c1433b477b",
                          "name": "Eastern Suburbs postcodes",
                          "zoneType": "postcode",
                          "weight": 30
                        },
                        {
                          "zoneId": "30bd7ca2-f684-489f-885a-ce95edbf888c",
                          "name": "New South Wales",
                          "zoneType": "state",
                          "weight": 20
                        },
                        {
                          "zoneId": "37fff766-fcbd-4ea9-85b8-fe12e0720ac9",
                          "name": "Australia (all)",
                          "zoneType": "country",
                          "weight": 10
                        }
                      ],
                      "winner": {
                        "zoneId": "5ea2af43-c7c8-4982-b477-2bc8a32c2952",
                        "name": "Coastal Flood Overlay (drawn)",
                        "zoneType": "custom",
                        "weight": 45
                      },
                      "dataMode": "synthetic"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "latitude or longitude missing; or a bad `x-aiq-sub-client` (empty, unknown, another client's or archived).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "latitude and longitude required"
                    }
                  },
                  "bad_sub_client": {
                    "value": {
                      "error": "unknown sub-client 'north' for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "Key lacks the required scope, its origin is not allowed, or its tenant is suspended. The body never names the missing capability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "413": {
            "description": "Body larger than the configured limit (AIQ_BODY_LIMIT, default 1mb).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "error": "payload too large",
                      "limit": "1mb"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded for the current 60-second window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "internal error"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          },
          {
            "$ref": "#/components/parameters/SubClient"
          }
        ]
      }
    },
    "/v1/route/distance": {
      "post": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Route"
        ],
        "operationId": "routeDistance",
        "summary": "Distance and drive time between two points",
        "description": "Distance in km and duration in minutes. Without AIQ_OSRM_URL this is a straight-line estimate x 1.3 at 60 km/h (`method: \"estimate\"`), not road routing.",
        "x-required-scope": "route",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RouteRequest"
              },
              "examples": {
                "bondi_parramatta": {
                  "summary": "Bondi to Parramatta",
                  "value": {
                    "origin": {
                      "latitude": -33.8915,
                      "longitude": 151.2767
                    },
                    "destination": {
                      "latitude": -33.8136,
                      "longitude": 151.0034
                    }
                  }
                },
                "parramatta_bondi": {
                  "summary": "Reverse direction",
                  "value": {
                    "origin": {
                      "latitude": -33.8136,
                      "longitude": 151.0034
                    },
                    "destination": {
                      "latitude": -33.8915,
                      "longitude": 151.2767
                    }
                  }
                },
                "missing": {
                  "summary": "destination missing",
                  "value": {
                    "origin": {
                      "latitude": -33.8915,
                      "longitude": 151.2767
                    }
                  },
                  "x-expect-status": 400
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RouteStats"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "distanceKm": 34.69,
                      "durationMin": 34.7,
                      "method": "estimate",
                      "note": "Estimate only: straight-line distance x 1.3 at 60 km/h. This is not road routing.",
                      "dataMode": "synthetic"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "Invalid point or list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "destination must be { latitude: -90..90, longitude: -180..180 }"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "Key lacks the required scope, its origin is not allowed, or its tenant is suspended. The body never names the missing capability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "413": {
            "description": "Body larger than the configured limit (AIQ_BODY_LIMIT, default 1mb).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "error": "payload too large",
                      "limit": "1mb"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded for the current 60-second window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "internal error"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "502": {
            "description": "Routing provider (OSRM) unavailable or timed out. Upstream detail is never leaked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "routing provider unavailable"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ]
      }
    },
    "/v1/route/directions": {
      "post": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Route"
        ],
        "operationId": "routeDirections",
        "summary": "Directions between two points",
        "description": "Adds `steps[]`. In estimate mode the steps are a compass heading, not turn-by-turn. Without AIQ_OSRM_URL this is a straight-line estimate x 1.3 at 60 km/h (`method: \"estimate\"`), not road routing.",
        "x-required-scope": "route",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RouteRequest"
              },
              "examples": {
                "bondi_parramatta": {
                  "summary": "Bondi to Parramatta",
                  "value": {
                    "origin": {
                      "latitude": -33.8915,
                      "longitude": 151.2767
                    },
                    "destination": {
                      "latitude": -33.8136,
                      "longitude": 151.0034
                    }
                  }
                },
                "missing": {
                  "summary": "destination missing",
                  "value": {
                    "origin": {
                      "latitude": -33.8915,
                      "longitude": 151.2767
                    }
                  },
                  "x-expect-status": 400
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RouteDirections"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "distanceKm": 34.69,
                      "durationMin": 34.7,
                      "steps": [
                        {
                          "instruction": "Head west towards the destination",
                          "distanceKm": 34.69,
                          "durationMin": 34.7
                        },
                        {
                          "instruction": "Arrive at destination",
                          "distanceKm": 0,
                          "durationMin": 0
                        }
                      ],
                      "method": "estimate",
                      "note": "Estimate only: straight-line distance x 1.3 at 60 km/h. This is not road routing. Steps are a compass heading, not turn-by-turn.",
                      "dataMode": "synthetic"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "Invalid point or list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "destination must be { latitude: -90..90, longitude: -180..180 }"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "Key lacks the required scope, its origin is not allowed, or its tenant is suspended. The body never names the missing capability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "413": {
            "description": "Body larger than the configured limit (AIQ_BODY_LIMIT, default 1mb).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "error": "payload too large",
                      "limit": "1mb"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded for the current 60-second window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "internal error"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "502": {
            "description": "Routing provider (OSRM) unavailable or timed out. Upstream detail is never leaked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "routing provider unavailable"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ]
      }
    },
    "/v1/route/matrix": {
      "post": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Route"
        ],
        "operationId": "routeMatrix",
        "summary": "Distance matrix, up to 10 x 10",
        "description": "Rows are origins, columns are destinations. Without AIQ_OSRM_URL this is a straight-line estimate x 1.3 at 60 km/h (`method: \"estimate\"`), not road routing.",
        "x-required-scope": "route",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MatrixRequest"
              },
              "examples": {
                "one_by_two": {
                  "summary": "One origin, two destinations",
                  "value": {
                    "origins": [
                      {
                        "latitude": -33.8915,
                        "longitude": 151.2767
                      }
                    ],
                    "destinations": [
                      {
                        "latitude": -33.8136,
                        "longitude": 151.0034
                      },
                      {
                        "latitude": -33.8915,
                        "longitude": 151.2767
                      }
                    ]
                  }
                },
                "two_by_two": {
                  "summary": "2 x 2",
                  "value": {
                    "origins": [
                      {
                        "latitude": -33.8915,
                        "longitude": 151.2767
                      },
                      {
                        "latitude": -33.8136,
                        "longitude": 151.0034
                      }
                    ],
                    "destinations": [
                      {
                        "latitude": -33.8136,
                        "longitude": 151.0034
                      },
                      {
                        "latitude": -33.8915,
                        "longitude": 151.2767
                      }
                    ]
                  }
                },
                "empty": {
                  "summary": "Empty origins",
                  "value": {
                    "origins": [],
                    "destinations": [
                      {
                        "latitude": -33.8915,
                        "longitude": 151.2767
                      }
                    ]
                  },
                  "x-expect-status": 400
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatrixResponse"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "distancesKm": [
                        [
                          34.69,
                          0
                        ]
                      ],
                      "durationsMin": [
                        [
                          34.7,
                          0
                        ]
                      ],
                      "method": "estimate",
                      "note": "Estimate only: straight-line distance x 1.3 at 60 km/h. This is not road routing.",
                      "dataMode": "synthetic"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "Invalid point or list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "origins must be a non-empty array of points"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "Key lacks the required scope, its origin is not allowed, or its tenant is suspended. The body never names the missing capability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "413": {
            "description": "Body larger than the configured limit (AIQ_BODY_LIMIT, default 1mb).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "error": "payload too large",
                      "limit": "1mb"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded for the current 60-second window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "internal error"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "502": {
            "description": "Routing provider (OSRM) unavailable or timed out. Upstream detail is never leaked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "routing provider unavailable"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ]
      }
    },
    "/v1/connect/systems": {
      "get": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Connect"
        ],
        "operationId": "listConnectSystems",
        "summary": "Host systems and their field maps",
        "description": "The systems `POST /v1/connect/verify` understands (salesforce, sap, s4hana, servicenow, ...) and the canonical-to-host field map for each. Any valid key works; no scope needed.",
        "x-required-scope": null,
        "responses": {
          "200": {
            "description": "Systems visible to your tenant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectSystemsResponse"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "systems": [
                        {
                          "system": "servicenow",
                          "name": "ServiceNow (cmn_location)",
                          "fields": {
                            "street": "street",
                            "suburb": "city",
                            "state": "state",
                            "postcode": "zip",
                            "country": "country",
                            "latitude": "latitude",
                            "longitude": "longitude"
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "Key lacks the required scope, its origin is not allowed, or its tenant is suspended. The body never names the missing capability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded for the current 60-second window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "internal error"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ]
      }
    },
    "/v1/connect/verify": {
      "post": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Connect"
        ],
        "operationId": "verifyConnectRecords",
        "summary": "Verify host-system records and get write-back fields plus zone",
        "description": "Records go in with the host system's own field names (or a free-text `address`) and come back in the same names under `results[].fields`, with `status`, `zone` and the canonical address. Needs scope `resolve`; more than one record also needs `bulk`.",
        "x-required-scope": "resolve",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConnectVerifyRequest"
              },
              "examples": {
                "salesforce": {
                  "summary": "Salesforce compound address, one verified and one free-text miss",
                  "value": {
                    "system": "salesforce",
                    "records": [
                      {
                        "id": "001A",
                        "Street": "18 Beach Rd",
                        "City": "Bondi",
                        "State": "NSW",
                        "PostalCode": "2026",
                        "Country": "AU"
                      },
                      {
                        "id": "001B",
                        "address": "nowhere at all 99999"
                      }
                    ]
                  }
                },
                "servicenow": {
                  "summary": "ServiceNow location",
                  "value": {
                    "system": "servicenow",
                    "records": [
                      {
                        "id": "loc1",
                        "street": "3/56 George St",
                        "city": "Parramatta",
                        "state": "NSW",
                        "zip": "2150",
                        "country": "AU"
                      }
                    ]
                  }
                },
                "unknown_system": {
                  "summary": "Unknown system",
                  "value": {
                    "system": "nope",
                    "records": [
                      {}
                    ]
                  },
                  "x-expect-status": 404
                },
                "empty": {
                  "summary": "No records",
                  "value": {
                    "system": "salesforce",
                    "records": []
                  },
                  "x-expect-status": 400
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One result per record, input order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectVerifyResponse"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "system": "Salesforce (Address compound field)",
                      "results": [
                        {
                          "id": "001A",
                          "status": "verified",
                          "fields": {
                            "Street": "18 Beach Rd",
                            "City": "Bondi",
                            "State": "NSW",
                            "PostalCode": "2026",
                            "Country": "AU",
                            "Latitude": -33.8915,
                            "Longitude": 151.2767
                          },
                          "address": {
                            "fullAddress": "18 Beach Rd, Bondi NSW 2026",
                            "houseNumber": "18",
                            "streetName": "Beach Rd",
                            "addressLine2": "",
                            "suburb": "Bondi",
                            "state": "NSW",
                            "postcode": "2026",
                            "countryCode": "AU",
                            "latitude": -33.8915,
                            "longitude": 151.2767,
                            "precision": "rooftop",
                            "confidence": 0.98,
                            "validationStatus": "verified",
                            "reasonCodes": "",
                            "providerCode": "gnaf",
                            "cacheable": true
                          },
                          "zone": "Coastal Flood Overlay (drawn)",
                          "zones": [
                            "Coastal Flood Overlay (drawn)",
                            "Bondi locality"
                          ]
                        },
                        {
                          "id": "001B",
                          "status": "unverifiable",
                          "fields": {},
                          "address": null,
                          "zone": null,
                          "zones": []
                        }
                      ],
                      "dataMode": "synthetic"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "records missing, empty, or more than 100; or a bad `x-aiq-sub-client` (empty, unknown, another client's or archived).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "records: object[] required"
                    }
                  },
                  "bad_sub_client": {
                    "value": {
                      "error": "unknown sub-client 'north' for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "Key lacks the required scope, its origin is not allowed, or its tenant is suspended. The body never names the missing capability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "Unknown system.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "unknown system — see GET /v1/connect/systems"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "413": {
            "description": "Body larger than the configured limit (AIQ_BODY_LIMIT, default 1mb).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "error": "payload too large",
                      "limit": "1mb"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded for the current 60-second window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "internal error"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          },
          {
            "$ref": "#/components/parameters/SubClient"
          }
        ]
      }
    },
    "/v1/health": {
      "get": {
        "tags": [
          "Operations"
        ],
        "operationId": "getHealth",
        "summary": "Liveness: the process answers (no database call)",
        "security": [],
        "x-required-scope": null,
        "description": "No key needed. Cheap liveness check for probes: it never queries the database, so a slow database does not restart healthy pods. Use GET /v1/ready for the deep check (database, schema, draining).",
        "responses": {
          "200": {
            "description": "Service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "status": "up",
                      "profile": "development",
                      "service": "addressiq",
                      "dataMode": "synthetic"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ]
      }
    },
    "/v1/ready": {
      "get": {
        "tags": [
          "Operations"
        ],
        "operationId": "getReady",
        "summary": "Readiness probe: 503 while draining or when the database or schema is missing",
        "security": [],
        "x-required-scope": null,
        "description": "No key needed. Use as the load-balancer / Kubernetes readiness check; /v1/health is the liveness check.",
        "responses": {
          "200": {
            "description": "Ready for traffic.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ready"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "ready": true
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "503": {
            "description": "Not ready.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ready"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "ready": false,
                      "reason": "draining"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ]
      }
    },
    "/v1/metrics": {
      "get": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Operations"
        ],
        "operationId": "getMetrics",
        "summary": "Per-operation counts, errors and latency percentiles for your tenant",
        "x-required-scope": null,
        "description": "Per pod and in memory, so it resets on restart and differs between replicas. Any valid key works; no scope needed.",
        "responses": {
          "200": {
            "description": "Metrics by operation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Metrics"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "POST /v1/address/resolve": {
                        "count": 12,
                        "errors": 0,
                        "p50ms": 4,
                        "p95ms": 11
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "Key lacks the required scope, its origin is not allowed, or its tenant is suspended. The body never names the missing capability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded for the current 60-second window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "internal error"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ]
      }
    },
    "/v1/bulk/jobs": {
      "post": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Bulk"
        ],
        "operationId": "createBulkJob",
        "summary": "Start an async bulk job (up to 50,000 rows)",
        "description": "JSON `{ \"addresses\": [...] }` or `text/csv` with a header row and an `address` column. Each JSON item is an address string or an object `{ address, id?, countryCode? }`; CSV may add `id` and `country` (or `countryCode`) columns. `id` is your own reference (string, up to 100 characters) and comes back on every result and exception row; `countryCode` is a 2-letter hint for the routing chain (stored upper-case). Rows: up to `AIQ_BULK_JOB_MAX` (default 50,000) and the plan's `bulkRowCap`; body cap `AIQ_BULK_BODY_LIMIT` (default 30mb) for this route only. Empty, non-string or over-500-character addresses, an `id` that is not a string of at most 100 characters, and a `countryCode` that is not 2 letters are kept as `error` rows and never metered; the job is still accepted. Every other row is resolved like POST /v1/address/resolve (same routing chain, one usage-ledger row each) and counts against the monthly quota; rows do not count against the per-key rate limit. The job stores every result, so providers whose licence bars storing results are skipped; if only such providers are left the row is `unresolved` with reason `LICENCE:NOT_STORABLE`.\n\nSend `Idempotency-Key` (8-128 of `A-Z a-z 0-9 . _ : -`) to make retries safe: the same key and body replay the first response with `Idempotent-Replayed: true`; the same key with another body is 422; keys are per API key and expire after 24 h.",
        "x-required-scope": "bulk",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9._:-]{8,128}$"
            }
          },
          {
            "$ref": "#/components/parameters/SubClient"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkJobRequest"
              },
              "examples": {
                "mixed": {
                  "summary": "Two good rows, one miss, one empty row",
                  "value": {
                    "addresses": [
                      "18 Beach Rd, Bondi NSW 2026",
                      "3/56 George St, Parramatta NSW 2150",
                      "nowhere at all 99999",
                      ""
                    ]
                  },
                  "x-expect-status": 202
                },
                "withIds": {
                  "summary": "Rows with your own id and a country hint",
                  "value": {
                    "addresses": [
                      {
                        "address": "18 Beach Rd, Bondi NSW 2026",
                        "id": "crm-1001",
                        "countryCode": "AU"
                      },
                      "3/56 George St, Parramatta NSW 2150"
                    ]
                  },
                  "x-expect-status": 202
                },
                "empty": {
                  "summary": "No rows",
                  "value": {
                    "addresses": []
                  },
                  "x-expect-status": 400
                }
              }
            },
            "text/csv": {
              "schema": {
                "type": "string"
              },
              "example": "id,address,country\n1,\"18 Beach Rd, Bondi NSW 2026\",AU\n2,nowhere at all,\n"
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job accepted and queued. `Location` points at the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkJob"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "id": "6f1c2c1e-6c0b-4c55-9d43-0a4b8d3c9e11",
                      "status": "queued",
                      "total": 2,
                      "resolved": 0,
                      "unresolved": 1,
                      "pending": 1,
                      "createdAt": "2026-11-05T10:00:00.000Z",
                      "startedAt": null,
                      "finishedAt": null,
                      "error": null,
                      "links": {
                        "self": "/v1/bulk/jobs/6f1c2c1e-6c0b-4c55-9d43-0a4b8d3c9e11",
                        "results": "/v1/bulk/jobs/6f1c2c1e-6c0b-4c55-9d43-0a4b8d3c9e11/results",
                        "exceptions": "/v1/bulk/jobs/6f1c2c1e-6c0b-4c55-9d43-0a4b8d3c9e11/exceptions"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              },
              "Location": {
                "schema": {
                  "type": "string"
                },
                "description": "/v1/bulk/jobs/{id}"
              },
              "Idempotent-Replayed": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Present on a replayed response."
              }
            }
          },
          "400": {
            "description": "No rows, malformed body or CSV, more rows than allowed, or a malformed Idempotency-Key; or a bad `x-aiq-sub-client` (empty, unknown, another client's or archived).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "no rows: send at least one address"
                    }
                  },
                  "bad_sub_client": {
                    "value": {
                      "error": "unknown sub-client 'north' for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "403": {
            "description": "Key lacks the `bulk` scope, its origin is not allowed, or its tenant is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "409": {
            "description": "The first request with this Idempotency-Key is still running.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "a request with this Idempotency-Key is still in progress"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "413": {
            "description": "Body larger than AIQ_BULK_BODY_LIMIT.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "422": {
            "description": "Idempotency-Key reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "Idempotency-Key was already used with a different request"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit (`limitPerMin`) or the tenant's monthly quota (`quota_exceeded`, `resetsAt`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "rate": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  },
                  "quota": {
                    "value": {
                      "error": "quota_exceeded",
                      "resetsAt": "2026-12-01T00:00:00.000Z"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "500": {
            "description": "Unexpected error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "could not create the job"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          }
        }
      }
    },
    "/v1/bulk/jobs/{id}": {
      "get": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Bulk"
        ],
        "operationId": "getBulkJob",
        "summary": "Job status and counts",
        "x-required-scope": "bulk",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Job id returned by POST /v1/bulk/jobs."
          }
        ],
        "description": "Not metered: works over quota.",
        "responses": {
          "200": {
            "description": "Status: queued, running, done, failed (see `error`, e.g. quota_exceeded) or cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkJob"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "id": "6f1c2c1e-6c0b-4c55-9d43-0a4b8d3c9e11",
                      "status": "done",
                      "total": 2,
                      "resolved": 1,
                      "unresolved": 1,
                      "pending": 0,
                      "createdAt": "2026-11-05T10:00:00.000Z",
                      "startedAt": "2026-11-05T10:00:00.050Z",
                      "finishedAt": "2026-11-05T10:00:00.400Z",
                      "error": null,
                      "links": {
                        "self": "/v1/bulk/jobs/6f1c2c1e-6c0b-4c55-9d43-0a4b8d3c9e11",
                        "results": "/v1/bulk/jobs/6f1c2c1e-6c0b-4c55-9d43-0a4b8d3c9e11/results",
                        "exceptions": "/v1/bulk/jobs/6f1c2c1e-6c0b-4c55-9d43-0a4b8d3c9e11/exceptions"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "403": {
            "description": "Key lacks the `bulk` scope, its origin is not allowed, or its tenant is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "404": {
            "description": "No such job for this tenant. Another tenant's job id answers exactly like an unknown one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "job not found"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit (`limitPerMin`) or the tenant's monthly quota (`quota_exceeded`, `resetsAt`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "rate": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  },
                  "quota": {
                    "value": {
                      "error": "quota_exceeded",
                      "resetsAt": "2026-12-01T00:00:00.000Z"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          }
        }
      }
    },
    "/v1/bulk/jobs/{id}/results": {
      "get": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Bulk"
        ],
        "operationId": "getBulkJobResults",
        "summary": "Every input row with its result or reason, in input order",
        "x-required-scope": "bulk",
        "description": "Streams `application/x-ndjson` (one BulkJobRow per line, default) or `text/csv` (`format=csv`; cells starting with = + - @ are prefixed with ' so spreadsheets do not run them). `address` is null when the row failed or was not resolved (the `reason` says why). Partial while the job runs.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Job id returned by POST /v1/bulk/jobs."
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "ndjson",
                "csv"
              ],
              "default": "ndjson"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One row per input.",
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            },
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "$ref": "#/components/schemas/BulkJobRow"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "row,input,status,fullAddress,houseNumber,streetName,addressLine2,suburb,state,postcode,countryCode,latitude,longitude,plusCode,precision,confidence,validationStatus,reasonCodes,providerCode,reason\n\"1\",\"nowhere at all\",\"unresolved\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"unresolvable\"\n"
              }
            }
          },
          "400": {
            "description": "format is not ndjson or csv.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "format must be ndjson or csv"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "403": {
            "description": "Key lacks the `bulk` scope, its origin is not allowed, or its tenant is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "404": {
            "description": "No such job for this tenant. Another tenant's job id answers exactly like an unknown one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "job not found"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit (`limitPerMin`) or the tenant's monthly quota (`quota_exceeded`, `resetsAt`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "rate": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  },
                  "quota": {
                    "value": {
                      "error": "quota_exceeded",
                      "resetsAt": "2026-12-01T00:00:00.000Z"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          }
        }
      }
    },
    "/v1/bulk/jobs/{id}/exceptions": {
      "get": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Bulk"
        ],
        "operationId": "getBulkJobExceptions",
        "summary": "Unresolved and bad rows with their reason",
        "x-required-scope": "bulk",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Job id returned by POST /v1/bulk/jobs."
          }
        ],
        "responses": {
          "200": {
            "description": "Exception rows, input order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkJobExceptions"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "jobId": "6f1c2c1e-6c0b-4c55-9d43-0a4b8d3c9e11",
                      "status": "done",
                      "exceptions": [
                        {
                          "row": 2,
                          "id": null,
                          "input": "nowhere at all 99999",
                          "status": "unresolved",
                          "reason": "unresolvable"
                        }
                      ]
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "403": {
            "description": "Key lacks the `bulk` scope, its origin is not allowed, or its tenant is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "404": {
            "description": "No such job for this tenant. Another tenant's job id answers exactly like an unknown one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "job not found"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit (`limitPerMin`) or the tenant's monthly quota (`quota_exceeded`, `resetsAt`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "rate": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  },
                  "quota": {
                    "value": {
                      "error": "quota_exceeded",
                      "resetsAt": "2026-12-01T00:00:00.000Z"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          }
        }
      }
    },
    "/v1/bulk/jobs/{id}/cancel": {
      "post": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Bulk"
        ],
        "operationId": "cancelBulkJob",
        "summary": "Cancel a queued or running job",
        "x-required-scope": "bulk",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Job id returned by POST /v1/bulk/jobs."
          }
        ],
        "description": "Idempotent. Rows already answered stay; a finished job is returned unchanged.",
        "responses": {
          "200": {
            "description": "The job after the cancel.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkJob"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "id": "6f1c2c1e-6c0b-4c55-9d43-0a4b8d3c9e11",
                      "status": "cancelled",
                      "total": 2,
                      "resolved": 1,
                      "unresolved": 1,
                      "pending": 0,
                      "createdAt": "2026-11-05T10:00:00.000Z",
                      "startedAt": "2026-11-05T10:00:00.050Z",
                      "finishedAt": "2026-11-05T10:00:00.400Z",
                      "error": null,
                      "links": {
                        "self": "/v1/bulk/jobs/6f1c2c1e-6c0b-4c55-9d43-0a4b8d3c9e11",
                        "results": "/v1/bulk/jobs/6f1c2c1e-6c0b-4c55-9d43-0a4b8d3c9e11/results",
                        "exceptions": "/v1/bulk/jobs/6f1c2c1e-6c0b-4c55-9d43-0a4b8d3c9e11/exceptions"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "403": {
            "description": "Key lacks the `bulk` scope, its origin is not allowed, or its tenant is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "scope not granted for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "404": {
            "description": "No such job for this tenant. Another tenant's job id answers exactly like an unknown one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "job not found"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit (`limitPerMin`) or the tenant's monthly quota (`quota_exceeded`, `resetsAt`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "rate": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  },
                  "quota": {
                    "value": {
                      "error": "quota_exceeded",
                      "resetsAt": "2026-12-01T00:00:00.000Z"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          }
        }
      }
    },
    "/v1/whoami": {
      "get": {
        "security": [
          {
            "ApiKey": []
          }
        ],
        "tags": [
          "Operations"
        ],
        "operationId": "whoami",
        "summary": "This key: client, tenant, scopes, plan and quota",
        "x-required-scope": null,
        "description": "Any valid key; not metered, so it answers over quota.",
        "responses": {
          "200": {
            "description": "Who the key is.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhoAmI"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "client": {
                        "id": "b4f1f5d2-0000-4000-8000-000000000001",
                        "name": "Demo — SAP S/4 sandbox",
                        "apiKeyPrefix": "aiq_demo_5f2e",
                        "rateLimitPerMin": 120
                      },
                      "tenant": {
                        "id": "00000000-0000-4000-8000-0000000de300",
                        "code": "demo",
                        "name": "Demo tenant"
                      },
                      "scopes": [
                        "autocomplete",
                        "resolve",
                        "bulk"
                      ],
                      "plan": "sandbox",
                      "quota": {
                        "limit": 2500,
                        "used": 12,
                        "remaining": 2488,
                        "resetsAt": "2026-12-01T00:00:00.000Z",
                        "bulkRowCap": 1000
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "401": {
            "description": "Missing, unknown or suspended API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "x-api-key header required"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "403": {
            "description": "Origin not allowed for this key, or its tenant is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "origin not allowed for this key"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          },
          "429": {
            "description": "Per-key rate limit (`limitPerMin`) or the tenant's monthly quota (`quota_exceeded`, `resetsAt`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                },
                "examples": {
                  "rate": {
                    "value": {
                      "error": "rate limit exceeded",
                      "limitPerMin": 60
                    }
                  },
                  "quota": {
                    "value": {
                      "error": "quota_exceeded",
                      "resetsAt": "2026-12-01T00:00:00.000Z"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/QuotaReset"
              }
            }
          }
        }
      }
    },
    "/metrics": {
      "get": {
        "tags": [
          "Operations"
        ],
        "operationId": "getPrometheusMetrics",
        "summary": "Prometheus scrape (platform only, not for API keys)",
        "x-required-scope": null,
        "x-platform-only": true,
        "security": [
          {
            "MetricsToken": []
          }
        ],
        "description": "Per-pod Prometheus text exposition: request counts and latency histograms by route, readiness, draining and in-flight gauges. Platform operators only: bearer AIQ_METRICS_TOKEN or a PlatformAdmin user. An API key gets 403. Tenants use GET /v1/metrics.",
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Prometheus text format 0.0.4.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "No valid metrics token and not a PlatformAdmin user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "error": "forbidden"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/v1/signup": {
      "post": {
        "tags": [
          "Signup"
        ],
        "operationId": "signup",
        "summary": "Self-serve signup: email a verification token (creates nothing usable yet)",
        "security": [],
        "x-required-scope": null,
        "description": "No key needed. Off unless the operator sets AIQ_SIGNUP_ENABLED=1 (404 otherwise). Stores a pending request and emails a 24-hour, single-use token. Nothing usable exists until POST /v1/signup/verify. The 202 body is identical whether or not the email already has an account. Abuse controls: per-IP and per-email hourly limits, disposable and blocked domains, and an optional human check (captchaToken).",
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignupRequest"
              },
              "examples": {
                "signup": {
                  "summary": "New account",
                  "value": {
                    "email": "owner@example-company.com.au",
                    "companyName": "Example Company Pty Ltd"
                  },
                  "x-expect-status": 202
                },
                "disposable": {
                  "summary": "Disposable domain (400)",
                  "value": {
                    "email": "someone@mailinator.com",
                    "companyName": "Temp Co"
                  },
                  "x-expect-status": 400
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Check the mailbox.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignupAccepted"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "status": "pending",
                      "message": "If this address can be used, a verification link is on its way. It expires in 24 hours."
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "Invalid email or company name, email domain not accepted, or human check failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "email domain not accepted"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "Signup is switched off on this server (it is off unless AIQ_SIGNUP_ENABLED=1).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "signup is not enabled"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Too many attempts from this IP address or for this email in the last hour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "too many signup attempts, try again later"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "503": {
            "description": "Production server without a configured mailer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "signup mail delivery not configured"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/v1/signup/verify": {
      "get": {
        "tags": [
          "Signup"
        ],
        "operationId": "checkSignupToken",
        "summary": "Check a signup token without using it",
        "security": [],
        "x-required-scope": null,
        "x-expect-status": 400,
        "description": "Safe for mail link scanners: it does not use the token up and creates nothing. Confirm with POST /v1/signup/verify.",
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          },
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The token from the verification email."
          }
        ],
        "responses": {
          "200": {
            "description": "The token is valid and unused.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignupTokenValid"
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "Token malformed, unknown, changed, already used or older than 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "invalid or expired token"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "Signup is switched off on this server (it is off unless AIQ_SIGNUP_ENABLED=1).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "signup is not enabled"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Too many attempts from this IP address or for this email in the last hour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "too many signup attempts, try again later"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Signup"
        ],
        "operationId": "verifySignup",
        "summary": "Use a signup token: creates the sandbox tenant and its first API key",
        "security": [],
        "x-required-scope": null,
        "description": "Single use. Creates a tenant on the sandbox plan (copilot off), a first API key with scopes autocomplete, resolve and classify (rate limit and monthly quota from the sandbox plan), and a pending admin invitation. The API key is returned once, here only.",
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignupVerifyRequest"
              },
              "examples": {
                "expired": {
                  "summary": "Unknown or expired token (400)",
                  "value": {
                    "token": "00000000-0000-4000-8000-000000000000.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
                  },
                  "x-expect-status": 400
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Account created. Store the API key now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignupResult"
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "Token malformed, unknown, changed, already used or older than 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "invalid or expired token"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "Signup is switched off on this server (it is off unless AIQ_SIGNUP_ENABLED=1).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "signup is not enabled"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "The mailbox already has an account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "this email already has an account"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "Too many attempts from this IP address or for this email in the last hour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "too many signup attempts, try again later"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/v1/billing/stripe/webhook": {
      "post": {
        "tags": [
          "Billing"
        ],
        "operationId": "stripeWebhook",
        "summary": "Stripe webhook receiver (platform-facing, not for API keys)",
        "x-required-scope": null,
        "x-platform-only": true,
        "security": [
          {
            "StripeSignature": []
          }
        ],
        "description": "Called by Stripe only. The raw body is verified against the Stripe-Signature header (HMAC-SHA256 with STRIPE_WEBHOOK_SECRET, 5-minute timestamp tolerance, any of several v1 signatures, constant-time compare). Events are deduplicated by id. Handled: invoice.paid, invoice.payment_failed, customer.subscription.created/updated/deleted; others are acknowledged and ignored.",
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true,
                "description": "A Stripe Event object, verified byte for byte."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Event verified and handled (or already handled).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingWebhookAck"
                },
                "examples": {
                  "demo": {
                    "value": {
                      "received": true,
                      "duplicate": false
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "Missing or wrong signature, timestamp outside tolerance, or malformed event.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "invalid signature"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "503": {
            "description": "STRIPE_WEBHOOK_SECRET is not set on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sample": {
                    "value": {
                      "error": "billing not configured"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Client API key. The seeded demo key is aiq_demo_5f2e8c1a9b3d4e6f (scopes autocomplete, resolve, reverse, enrich, classify, zones:read, bulk, route; 120 requests/min). Issue real keys with AdminService.issueClientKey; the raw key is shown once."
      },
      "MetricsToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "AIQ_METRICS_TOKEN (platform operators only). A signed-in AddressIQ.PlatformAdmin user may also scrape. Never issued to tenants."
      },
      "StripeSignature": {
        "type": "apiKey",
        "in": "header",
        "name": "Stripe-Signature",
        "description": "Platform-facing only: Stripe signs each webhook delivery with the endpoint signing secret (STRIPE_WEBHOOK_SECRET, server environment only). Never issued to tenants."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message. On a 500 it is always \"internal error\": internal detail is never returned."
          },
          "requestId": {
            "type": "string",
            "description": "500 only: the X-Request-Id of the call, to quote to support (the real error is in the server log under this id)."
          }
        },
        "required": [
          "error"
        ]
      },
      "BulkJobRequest": {
        "type": "object",
        "properties": {
          "addresses": {
            "type": "array",
            "minItems": 1,
            "items": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "object",
                  "properties": {
                    "address": {
                      "type": "string",
                      "maxLength": 500
                    },
                    "id": {
                      "type": "string",
                      "maxLength": 100,
                      "description": "Your own reference for the row, echoed in results and exceptions."
                    },
                    "countryCode": {
                      "type": "string",
                      "pattern": "^[A-Za-z]{2}$",
                      "description": "2-letter country hint for the routing chain; stored upper-case."
                    }
                  },
                  "required": [
                    "address"
                  ]
                }
              ]
            },
            "description": "Each item is an address string or an object. Items that break a rule become error rows, never metered."
          }
        },
        "required": [
          "addresses"
        ]
      },
      "BulkJob": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "done",
              "failed",
              "cancelled"
            ]
          },
          "total": {
            "type": "integer"
          },
          "resolved": {
            "type": "integer"
          },
          "unresolved": {
            "type": "integer",
            "description": "Unresolvable plus bad rows."
          },
          "pending": {
            "type": "integer"
          },
          "createdAt": {
            "type": "string",
            "nullable": true
          },
          "startedAt": {
            "type": "string",
            "nullable": true
          },
          "finishedAt": {
            "type": "string",
            "nullable": true
          },
          "error": {
            "type": "string",
            "nullable": true,
            "description": "Why a job failed, e.g. quota_exceeded."
          },
          "links": {
            "type": "object",
            "properties": {
              "self": {
                "type": "string"
              },
              "results": {
                "type": "string"
              },
              "exceptions": {
                "type": "string"
              }
            },
            "required": [
              "self",
              "results",
              "exceptions"
            ]
          }
        },
        "required": [
          "id",
          "status",
          "total",
          "resolved",
          "unresolved",
          "pending",
          "createdAt",
          "startedAt",
          "finishedAt",
          "error",
          "links"
        ]
      },
      "BulkJobRow": {
        "type": "object",
        "properties": {
          "row": {
            "type": "integer",
            "description": "1-based input position."
          },
          "id": {
            "type": "string",
            "nullable": true,
            "description": "The `id` sent with the row, or null."
          },
          "input": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "resolved",
              "unresolved",
              "error"
            ]
          },
          "address": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CanonicalAddress"
              }
            ],
            "nullable": true
          },
          "reason": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "row",
          "id",
          "input",
          "status",
          "address",
          "reason"
        ]
      },
      "BulkJobException": {
        "type": "object",
        "properties": {
          "row": {
            "type": "integer"
          },
          "id": {
            "type": "string",
            "nullable": true,
            "description": "The `id` sent with the row, or null."
          },
          "input": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "unresolved",
              "error"
            ]
          },
          "reason": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "row",
          "id",
          "input",
          "status",
          "reason"
        ]
      },
      "BulkJobExceptions": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "done",
              "failed",
              "cancelled"
            ]
          },
          "exceptions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BulkJobException"
            }
          }
        },
        "required": [
          "jobId",
          "status",
          "exceptions"
        ]
      },
      "WhoAmI": {
        "type": "object",
        "properties": {
          "client": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "apiKeyPrefix": {
                "type": "string",
                "nullable": true
              },
              "rateLimitPerMin": {
                "type": "integer",
                "nullable": true
              }
            },
            "required": [
              "id",
              "name",
              "apiKeyPrefix",
              "rateLimitPerMin"
            ]
          },
          "tenant": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "code": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "id",
              "code",
              "name"
            ]
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "plan": {
            "type": "string",
            "enum": [
              "sandbox",
              "standard",
              "enterprise"
            ]
          },
          "quota": {
            "type": "object",
            "properties": {
              "limit": {
                "type": "integer"
              },
              "used": {
                "type": "integer"
              },
              "remaining": {
                "type": "integer"
              },
              "resetsAt": {
                "type": "string"
              },
              "bulkRowCap": {
                "type": "integer"
              }
            },
            "required": [
              "limit",
              "used",
              "remaining",
              "resetsAt",
              "bulkRowCap"
            ]
          }
        },
        "required": [
          "client",
          "tenant",
          "scopes",
          "plan",
          "quota"
        ]
      },
      "RateLimitError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "limitPerMin": {
            "type": "integer",
            "nullable": true
          },
          "resetsAt": {
            "type": "string",
            "description": "quota_exceeded only: when the tenant's monthly quota resets (ISO 8601, UTC)."
          }
        },
        "required": [
          "error"
        ]
      },
      "Point": {
        "type": "object",
        "properties": {
          "latitude": {
            "type": "number",
            "minimum": -90,
            "maximum": 90
          },
          "longitude": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          }
        },
        "required": [
          "latitude",
          "longitude"
        ]
      },
      "CanonicalAddress": {
        "type": "object",
        "properties": {
          "fullAddress": {
            "type": "string"
          },
          "houseNumber": {
            "type": "string"
          },
          "streetName": {
            "type": "string"
          },
          "addressLine2": {
            "type": "string"
          },
          "suburb": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "postcode": {
            "type": "string"
          },
          "countryCode": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2, upper case."
          },
          "latitude": {
            "type": "number",
            "nullable": true
          },
          "longitude": {
            "type": "number",
            "nullable": true
          },
          "plusCode": {
            "type": "string",
            "nullable": true,
            "description": "Open Location Code (Plus Code), 11 digits, of latitude and longitude. null when the address has no coordinates."
          },
          "precision": {
            "type": "string",
            "enum": [
              "rooftop",
              "street",
              "postcode",
              "locality",
              "pin"
            ]
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "validationStatus": {
            "type": "string",
            "enum": [
              "verified",
              "partial",
              "unverifiable",
              "provisional"
            ]
          },
          "reasonCodes": {
            "type": "string",
            "description": "Comma-separated codes, e.g. NO_HOUSE_NUMBER,LOCALITY_LEVEL_ONLY. Empty when none."
          },
          "providerCode": {
            "type": "string",
            "description": "Which provider adapter answered (gnaf, nswpoint, here, google, nominatim, ...)."
          },
          "cacheable": {
            "type": "boolean"
          }
        },
        "required": [
          "fullAddress",
          "houseNumber",
          "streetName",
          "suburb",
          "state",
          "postcode",
          "countryCode",
          "latitude",
          "longitude",
          "precision",
          "confidence",
          "validationStatus",
          "providerCode",
          "cacheable"
        ]
      },
      "Suggestion": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "provider": {
            "type": "string"
          }
        },
        "required": [
          "label",
          "countryCode",
          "provider"
        ]
      },
      "AutocompleteRequest": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "Partial address. Fewer than 2 characters returns an empty list, not an error."
          },
          "countryCode": {
            "type": "string",
            "description": "Optional ISO alpha-2 hint; otherwise detected from the text."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "description": "Default 6."
          },
          "proximity": {
            "$ref": "#/components/schemas/Point",
            "description": "Order suggestions nearest-first to this point."
          }
        },
        "required": [
          "query"
        ]
      },
      "AutocompleteResponse": {
        "type": "object",
        "properties": {
          "suggestions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Suggestion"
            }
          },
          "requestId": {
            "type": "string"
          },
          "dataMode": {
            "type": "string",
            "enum": [
              "synthetic",
              "live"
            ],
            "description": "Always present. \"synthetic\" in the demo and in every test environment; \"live\" only when real provider keys are configured (AIQ_LIVE=1)."
          }
        },
        "required": [
          "suggestions",
          "dataMode"
        ]
      },
      "ResolveRequest": {
        "type": "object",
        "properties": {
          "address": {
            "type": "string",
            "description": "Free-text address (forward geocode)."
          },
          "latitude": {
            "type": "number",
            "minimum": -90,
            "maximum": 90,
            "description": "With longitude and no address: reverse geocode (country assumed AU)."
          },
          "longitude": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          },
          "countryCode": {
            "type": "string"
          }
        },
        "description": "Send address, or latitude plus longitude. At least one form is required."
      },
      "ResolveResponse": {
        "type": "object",
        "properties": {
          "address": {
            "$ref": "#/components/schemas/CanonicalAddress"
          },
          "requestId": {
            "type": "string"
          },
          "attempts": {
            "type": "integer",
            "description": "Provider hops tried before this answer."
          },
          "cacheHit": {
            "type": "boolean"
          },
          "dataMode": {
            "type": "string",
            "enum": [
              "synthetic",
              "live"
            ],
            "description": "\"synthetic\" in the demo and in every test environment; \"live\" only when real provider keys are configured (AIQ_LIVE=1)."
          }
        },
        "required": [
          "address",
          "requestId",
          "attempts",
          "cacheHit",
          "dataMode"
        ]
      },
      "CaptureAddress": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Id of the stored provisional address."
          },
          "fullAddress": {
            "type": "string"
          },
          "houseNumber": {
            "type": "string"
          },
          "streetName": {
            "type": "string"
          },
          "addressLine2": {
            "type": "string"
          },
          "suburb": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "postcode": {
            "type": "string"
          },
          "countryCode": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2, upper case."
          },
          "latitude": {
            "type": "number",
            "nullable": true
          },
          "longitude": {
            "type": "number",
            "nullable": true
          },
          "plusCode": {
            "type": "string",
            "description": "Open Location Code (Plus Code), 11 digits, of the pin."
          },
          "precision": {
            "type": "string",
            "enum": [
              "pin"
            ],
            "description": "Always `pin`: a point, not a rooftop match."
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "validationStatus": {
            "type": "string",
            "enum": [
              "provisional"
            ],
            "description": "Becomes `verified` only when an admin re-validation finds a rooftop address within 50 m."
          },
          "reasonCodes": {
            "type": "string",
            "description": "Comma-separated codes, e.g. NO_HOUSE_NUMBER,LOCALITY_LEVEL_ONLY. Empty when none."
          },
          "providerCode": {
            "type": "string",
            "description": "Always `capture`."
          },
          "cacheable": {
            "type": "boolean"
          },
          "captureSource": {
            "type": "string",
            "enum": [
              "pin",
              "gps",
              "parcel"
            ]
          },
          "accuracyMetres": {
            "type": "number",
            "nullable": true,
            "minimum": 0
          },
          "descriptor": {
            "type": "string",
            "nullable": true,
            "maxLength": 500,
            "description": "Free text from the capturer (landmark, directions). Used as fullAddress when given."
          },
          "parcelId": {
            "type": "string",
            "nullable": true,
            "maxLength": 60
          },
          "datum": {
            "type": "string",
            "enum": [
              "WGS84"
            ]
          }
        },
        "required": [
          "id",
          "fullAddress",
          "houseNumber",
          "streetName",
          "suburb",
          "state",
          "postcode",
          "countryCode",
          "latitude",
          "longitude",
          "precision",
          "confidence",
          "validationStatus",
          "providerCode",
          "cacheable",
          "plusCode",
          "captureSource",
          "datum"
        ],
        "description": "A provisional address: pin coordinates, Plus Code and descriptor only. houseNumber and streetName are always empty; suburb, state, postcode and countryCode are best-effort context from a reverse lookup."
      },
      "CaptureRequest": {
        "type": "object",
        "properties": {
          "latitude": {
            "type": "number",
            "minimum": -90,
            "maximum": 90
          },
          "longitude": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          },
          "accuracyMetres": {
            "type": "number",
            "minimum": 0,
            "description": "Reported accuracy of the fix, in metres."
          },
          "descriptor": {
            "type": "string",
            "maxLength": 500,
            "description": "Free text: landmark, directions, what the place is."
          },
          "parcelId": {
            "type": "string",
            "maxLength": 60,
            "description": "Cadastral parcel reference, if known."
          },
          "source": {
            "type": "string",
            "enum": [
              "pin",
              "gps",
              "parcel"
            ],
            "default": "pin"
          }
        },
        "required": [
          "latitude",
          "longitude"
        ]
      },
      "CaptureResponse": {
        "type": "object",
        "properties": {
          "address": {
            "$ref": "#/components/schemas/CaptureAddress"
          },
          "requestId": {
            "type": "string"
          },
          "dataMode": {
            "type": "string",
            "enum": [
              "synthetic",
              "live"
            ],
            "description": "\"synthetic\" in the demo and in every test environment; \"live\" only when real provider keys are configured (AIQ_LIVE=1)."
          }
        },
        "required": [
          "address",
          "requestId",
          "dataMode"
        ]
      },
      "UnresolvableError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "unresolvable"
            ]
          },
          "attempts": {
            "type": "integer"
          },
          "dataMode": {
            "type": "string",
            "enum": [
              "synthetic",
              "live"
            ],
            "description": "\"synthetic\" in the demo and in every test environment; \"live\" only when real provider keys are configured (AIQ_LIVE=1)."
          },
          "suggestions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Suggestion"
            },
            "description": "Closest known addresses (\"did you mean\"). Synthetic data mode only; empty when live."
          }
        },
        "required": [
          "error",
          "attempts",
          "dataMode",
          "suggestions"
        ]
      },
      "BulkResolveRequest": {
        "type": "object",
        "properties": {
          "addresses": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "maxItems": 100
          }
        },
        "required": [
          "addresses"
        ]
      },
      "BulkItem": {
        "type": "object",
        "properties": {
          "input": {
            "type": "string"
          },
          "address": {
            "$ref": "#/components/schemas/CanonicalAddress"
          },
          "attempts": {
            "type": "integer"
          },
          "cacheHit": {
            "type": "boolean"
          },
          "error": {
            "type": "string",
            "description": "Present instead of address when this item failed."
          }
        },
        "required": [
          "input"
        ]
      },
      "BulkResolveResponse": {
        "type": "object",
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BulkItem"
            },
            "description": "Same order as the request."
          },
          "dataMode": {
            "type": "string",
            "enum": [
              "synthetic",
              "live"
            ],
            "description": "\"synthetic\" in the demo and in every test environment; \"live\" only when real provider keys are configured (AIQ_LIVE=1)."
          }
        },
        "required": [
          "results",
          "dataMode"
        ]
      },
      "EnrichRequest": {
        "type": "object",
        "properties": {
          "address": {
            "type": "string"
          },
          "addressId": {
            "type": "string",
            "format": "uuid",
            "description": "ID of an address already stored for your tenant (alternative to address)."
          },
          "countryCode": {
            "type": "string"
          }
        },
        "description": "Send address or addressId."
      },
      "Attribute": {
        "type": "object",
        "properties": {
          "attrKey": {
            "type": "string",
            "description": "zoneClassification, zone, lga, floodRisk, zoningClass, electorate, sa2, sa3, sa4 (the set is configurable per deployment)."
          },
          "attrValue": {
            "type": "string",
            "description": "String. zoneClassification carries a JSON document as a string."
          },
          "source": {
            "type": "string"
          }
        },
        "required": [
          "attrKey",
          "attrValue",
          "source"
        ]
      },
      "EnrichResponse": {
        "type": "object",
        "properties": {
          "address": {
            "$ref": "#/components/schemas/CanonicalAddress"
          },
          "attributes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Attribute"
            }
          },
          "dataMode": {
            "type": "string",
            "enum": [
              "synthetic",
              "live"
            ],
            "description": "Always present. \"synthetic\" in the demo and in every test environment; \"live\" only when real provider keys are configured (AIQ_LIVE=1)."
          }
        },
        "required": [
          "address",
          "attributes",
          "dataMode"
        ]
      },
      "ClassifyRequest": {
        "type": "object",
        "properties": {
          "latitude": {
            "type": "number",
            "minimum": -90,
            "maximum": 90
          },
          "longitude": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          },
          "mode": {
            "type": "string",
            "enum": [
              "single-winner",
              "multi-tag"
            ],
            "description": "Defaults to the key's configured mode (single-winner)."
          }
        },
        "required": [
          "latitude",
          "longitude"
        ]
      },
      "ZoneMatch": {
        "type": "object",
        "properties": {
          "zoneId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "zoneType": {
            "type": "string",
            "description": "country, state, postcode, suburb or custom."
          },
          "weight": {
            "type": "number"
          }
        },
        "required": [
          "zoneId",
          "name",
          "zoneType",
          "weight"
        ]
      },
      "ClassifyResponse": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string"
          },
          "matches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ZoneMatch"
            },
            "description": "Every zone containing the point, heaviest first."
          },
          "winner": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ZoneMatch"
              }
            ],
            "nullable": true,
            "description": "Set in single-winner mode when any zone matches; always null in multi-tag."
          },
          "dataMode": {
            "type": "string",
            "enum": [
              "synthetic",
              "live"
            ],
            "description": "Always present. \"synthetic\" in the demo and in every test environment; \"live\" only when real provider keys are configured (AIQ_LIVE=1)."
          }
        },
        "required": [
          "mode",
          "matches",
          "winner",
          "dataMode"
        ]
      },
      "RouteRequest": {
        "type": "object",
        "properties": {
          "origin": {
            "$ref": "#/components/schemas/Point"
          },
          "destination": {
            "$ref": "#/components/schemas/Point"
          }
        },
        "required": [
          "origin",
          "destination"
        ]
      },
      "RouteStats": {
        "type": "object",
        "properties": {
          "distanceKm": {
            "type": "number"
          },
          "durationMin": {
            "type": "number"
          },
          "method": {
            "type": "string",
            "enum": [
              "estimate",
              "osrm"
            ]
          },
          "note": {
            "type": "string",
            "description": "Present when method is estimate: states it is not road routing."
          },
          "dataMode": {
            "type": "string",
            "enum": [
              "synthetic",
              "live"
            ],
            "description": "Always present. \"synthetic\" in the demo and in every test environment; \"live\" only when real provider keys are configured (AIQ_LIVE=1)."
          }
        },
        "required": [
          "distanceKm",
          "durationMin",
          "method",
          "dataMode"
        ]
      },
      "RouteStep": {
        "type": "object",
        "properties": {
          "instruction": {
            "type": "string"
          },
          "distanceKm": {
            "type": "number"
          },
          "durationMin": {
            "type": "number"
          }
        },
        "required": [
          "instruction",
          "distanceKm",
          "durationMin"
        ]
      },
      "RouteDirections": {
        "type": "object",
        "properties": {
          "distanceKm": {
            "type": "number"
          },
          "durationMin": {
            "type": "number"
          },
          "steps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RouteStep"
            }
          },
          "method": {
            "type": "string",
            "enum": [
              "estimate",
              "osrm"
            ]
          },
          "note": {
            "type": "string"
          },
          "dataMode": {
            "type": "string",
            "enum": [
              "synthetic",
              "live"
            ],
            "description": "Always present. \"synthetic\" in the demo and in every test environment; \"live\" only when real provider keys are configured (AIQ_LIVE=1)."
          }
        },
        "required": [
          "distanceKm",
          "durationMin",
          "steps",
          "method",
          "dataMode"
        ]
      },
      "MatrixRequest": {
        "type": "object",
        "properties": {
          "origins": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Point"
            },
            "minItems": 1,
            "maxItems": 10
          },
          "destinations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Point"
            },
            "minItems": 1,
            "maxItems": 10
          }
        },
        "required": [
          "origins",
          "destinations"
        ]
      },
      "MatrixResponse": {
        "type": "object",
        "properties": {
          "distancesKm": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "type": "number",
                "nullable": true
              }
            },
            "description": "distancesKm[i][j] = origin i to destination j. null = unreachable (osrm only)."
          },
          "durationsMin": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "type": "number",
                "nullable": true
              }
            }
          },
          "method": {
            "type": "string",
            "enum": [
              "estimate",
              "osrm"
            ]
          },
          "note": {
            "type": "string"
          },
          "dataMode": {
            "type": "string",
            "enum": [
              "synthetic",
              "live"
            ],
            "description": "Always present. \"synthetic\" in the demo and in every test environment; \"live\" only when real provider keys are configured (AIQ_LIVE=1)."
          }
        },
        "required": [
          "distancesKm",
          "durationsMin",
          "method",
          "dataMode"
        ]
      },
      "ConnectSystem": {
        "type": "object",
        "properties": {
          "system": {
            "type": "string",
            "description": "Short key to send as \"system\" (salesforce, sap, ...)."
          },
          "name": {
            "type": "string"
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "canonical field name -> host system field name."
          }
        },
        "required": [
          "system",
          "name",
          "fields"
        ]
      },
      "ConnectSystemsResponse": {
        "type": "object",
        "properties": {
          "systems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConnectSystem"
            }
          }
        },
        "required": [
          "systems"
        ]
      },
      "ConnectVerifyRequest": {
        "type": "object",
        "properties": {
          "system": {
            "type": "string",
            "description": "Short key from GET /v1/connect/systems, or the full system name."
          },
          "records": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true,
              "description": "Host-system field names plus an optional id and/or free-text address."
            },
            "minItems": 1,
            "maxItems": 100
          }
        },
        "required": [
          "system",
          "records"
        ]
      },
      "ConnectResult": {
        "type": "object",
        "properties": {
          "id": {
            "nullable": true,
            "description": "Echo of record.id (any JSON type), or null."
          },
          "status": {
            "type": "string",
            "enum": [
              "verified",
              "partial",
              "unverifiable",
              "error"
            ]
          },
          "fields": {
            "type": "object",
            "additionalProperties": true,
            "description": "Write-back payload in the host system's own field names. Empty when unverifiable."
          },
          "address": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CanonicalAddress"
              }
            ],
            "nullable": true
          },
          "zone": {
            "type": "string",
            "nullable": true
          },
          "zones": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "error": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "status",
          "fields",
          "address",
          "zone",
          "zones"
        ]
      },
      "ConnectVerifyResponse": {
        "type": "object",
        "properties": {
          "system": {
            "type": "string"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConnectResult"
            }
          },
          "dataMode": {
            "type": "string",
            "enum": [
              "synthetic",
              "live"
            ],
            "description": "\"synthetic\" in the demo and in every test environment; \"live\" only when real provider keys are configured (AIQ_LIVE=1)."
          }
        },
        "required": [
          "system",
          "results",
          "dataMode"
        ]
      },
      "Health": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "up"
            ]
          },
          "profile": {
            "type": "string"
          },
          "service": {
            "type": "string"
          },
          "dataMode": {
            "type": "string",
            "enum": [
              "synthetic",
              "live"
            ],
            "description": "\"synthetic\" in the demo and in every test environment; \"live\" only when real provider keys are configured (AIQ_LIVE=1)."
          }
        },
        "required": [
          "status",
          "profile",
          "service",
          "dataMode"
        ]
      },
      "MetricsOp": {
        "type": "object",
        "properties": {
          "count": {
            "type": "integer"
          },
          "errors": {
            "type": "integer"
          },
          "p50ms": {
            "type": "number",
            "nullable": true
          },
          "p95ms": {
            "type": "number",
            "nullable": true
          }
        },
        "required": [
          "count",
          "errors",
          "p50ms",
          "p95ms"
        ]
      },
      "Ready": {
        "type": "object",
        "properties": {
          "ready": {
            "type": "boolean"
          },
          "reason": {
            "type": "string",
            "description": "Present when not ready: draining, or database unreachable or schema missing."
          }
        },
        "required": [
          "ready"
        ]
      },
      "Metrics": {
        "type": "object",
        "additionalProperties": {
          "$ref": "#/components/schemas/MetricsOp"
        },
        "description": "Keyed by \"METHOD /path\". Your tenant only; per pod, in memory, since process start. Empty object until you have made calls."
      },
      "PayloadTooLarge": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "limit": {
            "type": "string",
            "description": "Configured body limit (AIQ_BODY_LIMIT), e.g. 1mb."
          }
        },
        "required": [
          "error",
          "limit"
        ]
      },
      "MatchRequest": {
        "type": "object",
        "properties": {
          "addresses": {
            "type": "array",
            "minItems": 2,
            "maxItems": 2000,
            "items": {
              "oneOf": [
                {
                  "type": "string",
                  "maxLength": 500
                },
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "maxLength": 100
                    },
                    "address": {
                      "type": "string",
                      "maxLength": 500
                    }
                  },
                  "required": [
                    "address"
                  ]
                }
              ],
              "additionalProperties": true
            }
          },
          "threshold": {
            "type": "number",
            "minimum": 0.5,
            "maximum": 1,
            "description": "Can only raise the tenant threshold, never lower it."
          }
        },
        "required": [
          "addresses"
        ]
      },
      "MatchRef": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer",
            "description": "Position in the request list (0-based)."
          },
          "id": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "index",
          "id"
        ]
      },
      "MatchPair": {
        "type": "object",
        "properties": {
          "a": {
            "$ref": "#/components/schemas/MatchRef"
          },
          "b": {
            "$ref": "#/components/schemas/MatchRef"
          },
          "score": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "level": {
            "type": "string",
            "enum": [
              "duplicate",
              "possible"
            ]
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Fields that differed, for example UNIT_DIFFERS, STREET_TYPO, SUBURB_MISSING."
          }
        },
        "required": [
          "a",
          "b",
          "score",
          "level",
          "reasons"
        ]
      },
      "MatchResponse": {
        "type": "object",
        "properties": {
          "threshold": {
            "type": "number"
          },
          "total": {
            "type": "integer"
          },
          "pairs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MatchPair"
            }
          },
          "truncated": {
            "type": "boolean",
            "description": "True when more than 10,000 pairs were found and only the strongest 10,000 are returned. `groups` is always complete."
          },
          "groups": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "type": "integer"
              }
            }
          },
          "dataMode": {
            "type": "string",
            "enum": [
              "synthetic",
              "live"
            ]
          }
        },
        "required": [
          "threshold",
          "total",
          "pairs",
          "truncated",
          "groups",
          "dataMode"
        ]
      },
      "SignupRequest": {
        "type": "object",
        "required": [
          "email",
          "companyName"
        ],
        "properties": {
          "email": {
            "type": "string",
            "maxLength": 254,
            "description": "Work email. Disposable and blocked domains are refused."
          },
          "companyName": {
            "type": "string",
            "minLength": 2,
            "maxLength": 120
          },
          "captchaToken": {
            "type": "string",
            "description": "Cloudflare Turnstile or hCaptcha response token. Required only when the server has a human check configured."
          }
        }
      },
      "SignupAccepted": {
        "type": "object",
        "required": [
          "status",
          "message"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "pending"
            ]
          },
          "message": {
            "type": "string"
          }
        },
        "description": "Always the same body, whether or not the email already has an account (no user enumeration)."
      },
      "SignupVerifyRequest": {
        "type": "object",
        "required": [
          "token"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "The token from the verification email."
          }
        }
      },
      "SignupTokenValid": {
        "type": "object",
        "required": [
          "status",
          "expiresAt",
          "next"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "valid"
            ]
          },
          "expiresAt": {
            "type": "string"
          },
          "next": {
            "type": "string"
          }
        }
      },
      "SignupResult": {
        "type": "object",
        "required": [
          "tenant",
          "clientId",
          "apiKey",
          "apiKeyPrefix",
          "scopes",
          "rateLimitPerMin",
          "quota",
          "invitation",
          "notice"
        ],
        "properties": {
          "tenant": {
            "type": "object",
            "required": [
              "code",
              "name",
              "plan"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Generated by the server; never chosen by the caller."
              },
              "name": {
                "type": "string"
              },
              "plan": {
                "type": "string",
                "enum": [
                  "sandbox"
                ]
              }
            }
          },
          "clientId": {
            "type": "string"
          },
          "apiKey": {
            "type": "string",
            "description": "Shown once, in this response only; only its hash is stored."
          },
          "apiKeyPrefix": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "rateLimitPerMin": {
            "type": "integer"
          },
          "quota": {
            "type": "object",
            "required": [
              "limit",
              "bulkRowCap"
            ],
            "properties": {
              "limit": {
                "type": "integer"
              },
              "bulkRowCap": {
                "type": "integer"
              }
            }
          },
          "invitation": {
            "type": "object",
            "required": [
              "role",
              "status"
            ],
            "properties": {
              "role": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending"
                ]
              }
            }
          },
          "notice": {
            "type": "string"
          }
        }
      },
      "BillingWebhookAck": {
        "type": "object",
        "required": [
          "received",
          "duplicate"
        ],
        "properties": {
          "received": {
            "type": "boolean"
          },
          "duplicate": {
            "type": "boolean",
            "description": "true when this event id was already handled (acknowledged, not applied again)."
          }
        }
      }
    },
    "parameters": {
      "RequestId": {
        "name": "x-request-id",
        "in": "header",
        "required": false,
        "description": "Optional trace id, 8-128 characters of A-Z a-z 0-9 . _ : -. Echoed back; anything else is replaced by a generated UUID.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9._:-]{8,128}$"
        }
      },
      "SubClient": {
        "name": "x-aiq-sub-client",
        "in": "header",
        "required": false,
        "description": "Optional. The code of one of this key's own live sub-clients (exact match). Zone matching then also uses that sub-client's zones; without it only platform zones, the tenant's shared zones and this client's own client-level zones apply. Another client's zones never apply. Recorded on the usage ledger rows of the call (and, for a bulk job, on the job and every row the worker meters). Accepted on every API-key route; an empty value, an unknown code, another client's code or an archived sub-client answers 400 and nothing is processed.",
        "schema": {
          "type": "string",
          "maxLength": 40
        }
      }
    },
    "headers": {
      "QuotaLimit": {
        "description": "Monthly call quota of the key's tenant. On every authenticated /v1 response.",
        "schema": {
          "type": "integer"
        }
      },
      "QuotaRemaining": {
        "description": "Calls left this month for the tenant (all its keys together).",
        "schema": {
          "type": "integer"
        }
      },
      "QuotaReset": {
        "description": "When the quota resets: Unix seconds, first day of next month 00:00 UTC.",
        "schema": {
          "type": "integer"
        }
      },
      "RequestId": {
        "description": "The request id: your x-request-id if valid, else a generated UUID. Also in the server access log.",
        "schema": {
          "type": "string"
        }
      }
    }
  }
}
