{
  "openapi": "3.1.0",
  "info": {
    "title": "AreaOps API",
    "version": "2026-08-22",
    "summary": "Census-scored ZIP code territory intelligence for home-services brands.",
    "description": "AreaOps scores every US ZIP code 0-100 for home-services demand fit from US Census ACS data, and manages the service areas that operators, marketing agencies and franchise groups run on top of those scores.\n\nTwo layers. The public layer needs no credential and is citation-safe: methodology, per-vertical summaries, and sampled ZIP and metro rankings. The authenticated layer needs an API key (or a signed-in session) and returns full ranked lists, account-scoped territory plans, brand footprints and exports.\n\nAI agents should prefer the Model Context Protocol server at POST /api/mcp, which exposes the same capabilities as 17 typed tools with confirm-gated writes. This REST contract is the equivalent for plain HTTP clients. Discovery for both is public: GET /api/mcp/info needs no key.",
    "contact": {
      "name": "AreaOps support",
      "email": "support@areaops.app",
      "url": "https://areaops.app/contact"
    },
    "termsOfService": "https://areaops.app/terms",
    "license": {
      "name": "Proprietary",
      "url": "https://areaops.app/terms"
    }
  },
  "externalDocs": {
    "description": "AreaOps developer portal",
    "url": "https://areaops.app/developers"
  },
  "servers": [
    {
      "url": "https://areaops.app/api",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "health",
      "description": "Service liveness."
    },
    {
      "name": "discovery",
      "description": "Machine-readable descriptions of this API and its MCP server."
    },
    {
      "name": "public-data",
      "description": "Citation-safe ZIP, vertical and metro data. No credential required."
    },
    {
      "name": "share-links",
      "description": "Read a public territory share link minted from the app or by an agent."
    },
    {
      "name": "agent",
      "description": "Authenticated, metered territory intelligence for AI agents."
    },
    {
      "name": "mcp",
      "description": "Model Context Protocol endpoint."
    }
  ],
  "paths": {
    "/healthz": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "health"
        ],
        "summary": "Service health.",
        "description": "Liveness and database reachability. Returns 200 when the API can reach its database and 503 when it cannot; the process is serving traffic in both cases.",
        "security": [],
        "responses": {
          "200": {
            "description": "Healthy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthStatus"
                }
              }
            }
          },
          "503": {
            "description": "Degraded — the database is unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthStatus"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiDocument",
        "tags": [
          "discovery"
        ],
        "summary": "This OpenAPI contract.",
        "description": "Returns this document. Unauthenticated, so an agent can discover the API surface before it has a credential. Content-negotiable: send Accept: application/yaml for the YAML form, or use /api/openapi.yaml.",
        "security": [],
        "responses": {
          "200": {
            "description": "The OpenAPI 3.1 document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              },
              "application/yaml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "406": {
            "description": "No representation matches the Accept header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/mcp/info": {
      "get": {
        "operationId": "getMcpDiscovery",
        "tags": [
          "discovery",
          "mcp"
        ],
        "summary": "MCP server discovery document.",
        "description": "Public description of the AreaOps Model Context Protocol server: transport, endpoint, authentication, and the full tool catalogue with each tool's billable unit weight. Needs no credential — this is the front door for agent directories and answer engines.",
        "security": [],
        "responses": {
          "200": {
            "description": "MCP discovery document.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpInfo"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After gives the seconds to wait; RateLimit and RateLimit-Policy carry the current state and the policy in force.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit": {
                "description": "Current state, e.g. \"default\";r=0;t=42",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Policy in force, e.g. \"default\";q=60;w=60",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "callMcp",
        "tags": [
          "mcp"
        ],
        "summary": "Model Context Protocol endpoint (JSON-RPC 2.0 over Streamable HTTP).",
        "description": "Stateless Streamable HTTP MCP endpoint. Send JSON-RPC 2.0 — `tools/list` to enumerate, `tools/call` to invoke. Authenticate with an aoa_live_ API key as a bearer token. Every write tool requires `confirm: true`, an active write entitlement, and passes the key's brand scope; added ZIPs land in a human review queue rather than going live. Calls are metered in units against the plan's monthly allowance.",
        "requestBody": {
          "required": true,
          "description": "A JSON-RPC 2.0 request object.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JsonRpcRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC 2.0 response. Tool errors are carried in the envelope, not the HTTP status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "No active subscription, or the monthly unit allowance is exhausted (code: usage_quota_exceeded).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After gives the seconds to wait; RateLimit and RateLimit-Policy carry the current state and the policy in force.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit": {
                "description": "Current state, e.g. \"default\";r=0;t=42",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Policy in force, e.g. \"default\";q=60;w=60",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/public/zip/{zip}": {
      "get": {
        "operationId": "getPublicZipProfile",
        "tags": [
          "public-data"
        ],
        "summary": "Citation-safe profile for one ZIP code.",
        "description": "Census demographics and per-vertical targeting scores for a single ZIP, limited to launched verticals inside launched metros. Scores are returned without the proprietary reasoning strings. A ZIP outside a launched metro returns 404 — this endpoint is not a route to enumerating the national dataset.",
        "security": [],
        "parameters": [
          {
            "name": "zip",
            "in": "path",
            "required": true,
            "description": "Five-digit US ZIP code.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{5}$",
              "examples": [
                "30309"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ZIP profile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicZipProfile"
                }
              }
            }
          },
          "400": {
            "description": "Malformed ZIP code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No public data for this ZIP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After gives the seconds to wait; RateLimit and RateLimit-Policy carry the current state and the policy in force.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit": {
                "description": "Current state, e.g. \"default\";r=0;t=42",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Policy in force, e.g. \"default\";q=60;w=60",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/public/zip/{zip}/vertical/{vertical}": {
      "get": {
        "operationId": "getPublicZipVerticalScore",
        "tags": [
          "public-data"
        ],
        "summary": "One ZIP's score for one vertical.",
        "description": "The targeting score and Census signals behind it for a single ZIP and vertical, with the metro it belongs to and the ACS vintage the numbers came from.",
        "security": [],
        "parameters": [
          {
            "name": "zip",
            "in": "path",
            "required": true,
            "description": "Five-digit US ZIP code.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{5}$",
              "examples": [
                "30309"
              ]
            }
          },
          {
            "name": "vertical",
            "in": "path",
            "required": true,
            "description": "Home-services vertical slug. Only launched verticals are served publicly.",
            "schema": {
              "type": "string",
              "enum": [
                "roofing",
                "hvac",
                "plumbing",
                "electrical",
                "windows-siding",
                "gutters",
                "landscaping",
                "pest-control",
                "professional-services",
                "real-estate",
                "moving"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ZIP score for the vertical.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicZipVerticalScore"
                }
              }
            }
          },
          "400": {
            "description": "Malformed ZIP code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Vertical not launched, or no public data for this ZIP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After gives the seconds to wait; RateLimit and RateLimit-Policy carry the current state and the policy in force.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit": {
                "description": "Current state, e.g. \"default\";r=0;t=42",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Policy in force, e.g. \"default\";q=60;w=60",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/public/vertical/{vertical}": {
      "get": {
        "operationId": "getPublicVerticalSummary",
        "tags": [
          "public-data"
        ],
        "summary": "Summary for one vertical across launched metros.",
        "description": "Which signals drive the score for this vertical, how it is weighted, and a sampled set of top-scoring ZIPs per launched metro. Sampled deliberately: see x-areaops-data-boundary.",
        "security": [],
        "parameters": [
          {
            "name": "vertical",
            "in": "path",
            "required": true,
            "description": "Home-services vertical slug. Only launched verticals are served publicly.",
            "schema": {
              "type": "string",
              "enum": [
                "roofing",
                "hvac",
                "plumbing",
                "electrical",
                "windows-siding",
                "gutters",
                "landscaping",
                "pest-control",
                "professional-services",
                "real-estate",
                "moving"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Vertical summary.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicVerticalSummary"
                }
              }
            }
          },
          "404": {
            "description": "Vertical not launched.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After gives the seconds to wait; RateLimit and RateLimit-Policy carry the current state and the policy in force.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit": {
                "description": "Current state, e.g. \"default\";r=0;t=42",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Policy in force, e.g. \"default\";q=60;w=60",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/public/metro/{metroSlug}/vertical/{vertical}": {
      "get": {
        "operationId": "getPublicMetroVertical",
        "tags": [
          "public-data"
        ],
        "summary": "Metro-level summary for one vertical.",
        "description": "Aggregate demand picture for a launched metro and vertical: ZIP count, score distribution, and a sampled ranking.",
        "security": [],
        "parameters": [
          {
            "name": "metroSlug",
            "in": "path",
            "required": true,
            "description": "Launched metro slug, e.g. atlanta-ga.",
            "schema": {
              "type": "string",
              "enum": [
                "dallas-tx",
                "houston-tx",
                "phoenix-az",
                "los-angeles-ca",
                "chicago-il",
                "atlanta-ga",
                "denver-co",
                "charlotte-nc",
                "orlando-fl",
                "nashville-tn",
                "tampa-fl",
                "austin-tx",
                "san-antonio-tx",
                "jacksonville-fl",
                "raleigh-nc",
                "minneapolis-mn",
                "seattle-wa",
                "portland-or",
                "sacramento-ca",
                "las-vegas-nv"
              ]
            }
          },
          {
            "name": "vertical",
            "in": "path",
            "required": true,
            "description": "Home-services vertical slug. Only launched verticals are served publicly.",
            "schema": {
              "type": "string",
              "enum": [
                "roofing",
                "hvac",
                "plumbing",
                "electrical",
                "windows-siding",
                "gutters",
                "landscaping",
                "pest-control",
                "professional-services",
                "real-estate",
                "moving"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Metro × vertical summary.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicMetroVertical"
                }
              }
            }
          },
          "404": {
            "description": "Metro or vertical not launched.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After gives the seconds to wait; RateLimit and RateLimit-Policy carry the current state and the policy in force.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit": {
                "description": "Current state, e.g. \"default\";r=0;t=42",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Policy in force, e.g. \"default\";q=60;w=60",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/public/metro/{metroSlug}/vertical/{vertical}/best-zips": {
      "get": {
        "operationId": "getPublicBestZips",
        "tags": [
          "public-data"
        ],
        "summary": "Top-scoring ZIPs in a metro for a vertical.",
        "description": "The highest-scoring ZIP codes for a vertical inside a launched metro, sampled to the public table limit. The full ranked list is an authenticated call — areaops_score_territory over MCP, or POST /agent/territory-plan.",
        "security": [],
        "parameters": [
          {
            "name": "metroSlug",
            "in": "path",
            "required": true,
            "description": "Launched metro slug, e.g. atlanta-ga.",
            "schema": {
              "type": "string",
              "enum": [
                "dallas-tx",
                "houston-tx",
                "phoenix-az",
                "los-angeles-ca",
                "chicago-il",
                "atlanta-ga",
                "denver-co",
                "charlotte-nc",
                "orlando-fl",
                "nashville-tn",
                "tampa-fl",
                "austin-tx",
                "san-antonio-tx",
                "jacksonville-fl",
                "raleigh-nc",
                "minneapolis-mn",
                "seattle-wa",
                "portland-or",
                "sacramento-ca",
                "las-vegas-nv"
              ]
            }
          },
          {
            "name": "vertical",
            "in": "path",
            "required": true,
            "description": "Home-services vertical slug. Only launched verticals are served publicly.",
            "schema": {
              "type": "string",
              "enum": [
                "roofing",
                "hvac",
                "plumbing",
                "electrical",
                "windows-siding",
                "gutters",
                "landscaping",
                "pest-control",
                "professional-services",
                "real-estate",
                "moving"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked ZIP sample.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicBestZips"
                }
              }
            }
          },
          "404": {
            "description": "Metro or vertical not launched.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After gives the seconds to wait; RateLimit and RateLimit-Policy carry the current state and the policy in force.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit": {
                "description": "Current state, e.g. \"default\";r=0;t=42",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Policy in force, e.g. \"default\";q=60;w=60",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/public/data-study": {
      "get": {
        "operationId": "getPublicDataStudy",
        "tags": [
          "public-data"
        ],
        "summary": "The AreaOps ZIP targeting score data study.",
        "description": "Aggregate findings across every launched vertical and metro — the citation-friendly dataset behind the published study. The same data is available as CSV at /public/data-study.csv.",
        "security": [],
        "responses": {
          "200": {
            "description": "Data study payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicDataStudy"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After gives the seconds to wait; RateLimit and RateLimit-Policy carry the current state and the policy in force.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit": {
                "description": "Current state, e.g. \"default\";r=0;t=42",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Policy in force, e.g. \"default\";q=60;w=60",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/shared/{token}/meta": {
      "get": {
        "operationId": "getSharedMapMeta",
        "tags": [
          "share-links"
        ],
        "summary": "Metadata for a public territory share link.",
        "description": "Brand name, creator, expiry and link kind for a share link. Accepts either the link's UUID or its short code. Expired and revoked links return 404 rather than an empty map.",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "Share-link UUID or short code.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Share link metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SharedMapMeta"
                }
              }
            }
          },
          "404": {
            "description": "Link is unknown, revoked, or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After gives the seconds to wait; RateLimit and RateLimit-Policy carry the current state and the policy in force.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit": {
                "description": "Current state, e.g. \"default\";r=0;t=42",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Policy in force, e.g. \"default\";q=60;w=60",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/shared/{token}/boundaries": {
      "get": {
        "operationId": "getSharedMapBoundaries",
        "tags": [
          "share-links"
        ],
        "summary": "ZIP boundary GeoJSON for a public share link.",
        "description": "The territory geometry behind a share link, as a GeoJSON FeatureCollection. Feature properties are allow-listed — a shared map carries the ZIPs and their display state, never the org's internal fields.",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "Share-link UUID or short code.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "GeoJSON FeatureCollection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GeoJsonFeatureCollection"
                }
              }
            }
          },
          "404": {
            "description": "Link is unknown, revoked, or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After gives the seconds to wait; RateLimit and RateLimit-Policy carry the current state and the policy in force.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit": {
                "description": "Current state, e.g. \"default\";r=0;t=42",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Policy in force, e.g. \"default\";q=60;w=60",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/map-context": {
      "get": {
        "operationId": "getMapContext",
        "tags": [
          "public-data"
        ],
        "summary": "Basemap geography for a bounding box.",
        "description": "Self-hosted Census TIGER basemap geography — states, counties, water, roads, places — for the requested bounding box and zoom, plus the palette to draw it with. Public because the share-link page is public and needs a basemap. Cached for 24 hours with an ETag.",
        "security": [],
        "parameters": [
          {
            "name": "bbox",
            "in": "query",
            "required": true,
            "description": "Bounding box as minLng,minLat,maxLng,maxLat.",
            "schema": {
              "type": "string",
              "examples": [
                "-84.6,33.6,-84.2,34.0"
              ]
            }
          },
          {
            "name": "zoom",
            "in": "query",
            "required": true,
            "description": "Map zoom level; selects which layers are returned.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 18
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Basemap layers and palette.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MapContext"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After gives the seconds to wait; RateLimit and RateLimit-Policy carry the current state and the policy in force.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit": {
                "description": "Current state, e.g. \"default\";r=0;t=42",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Policy in force, e.g. \"default\";q=60;w=60",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/agent/capabilities": {
      "get": {
        "operationId": "getAgentCapabilities",
        "tags": [
          "agent"
        ],
        "summary": "Describe AreaOps agent tools and this account's access.",
        "description": "What this account can do right now: the tool catalogue, whether Agent/API access is enabled, the plan it derives from, and the current usage meter. Call this first — it is the cheapest way to find out whether a territory-plan call will be served.",
        "responses": {
          "200": {
            "description": "Capabilities and account access state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentCapabilities"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid authentication.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After gives the seconds to wait; RateLimit and RateLimit-Policy carry the current state and the policy in force.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit": {
                "description": "Current state, e.g. \"default\";r=0;t=42",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Policy in force, e.g. \"default\";q=60;w=60",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/agent/territory-plan": {
      "post": {
        "operationId": "createTerritoryPlan",
        "tags": [
          "agent"
        ],
        "summary": "Generate a ranked ZIP territory plan for a vertical and market.",
        "description": "Paid authenticated endpoint. Takes either a launched metro slug or an explicit ZIP list, and returns ranked ZIP recommendations with operator next actions and source notes. Metered identically to the areaops_score_territory MCP tool, so REST is not a cheaper path to the same data. Does not expose raw scoring weights or bulk exports.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TerritoryPlanRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Territory plan generated from deterministic ZIP rankings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TerritoryPlan"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid authentication.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Agent/API access requires an active subscription or trial, or the monthly included usage is exhausted (code: usage_quota_exceeded, with the usage meter).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No data available for the requested market.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After gives the seconds to wait; RateLimit and RateLimit-Policy carry the current state and the policy in force.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit": {
                "description": "Current state, e.g. \"default\";r=0;t=42",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Policy in force, e.g. \"default\";q=60;w=60",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Usage recording is unavailable; the call was not served.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/agent/openapi.json": {
      "get": {
        "operationId": "getAgentOpenApi",
        "tags": [
          "discovery"
        ],
        "summary": "This OpenAPI contract (authenticated alias).",
        "description": "Identical to GET /openapi.json, kept for clients that integrated against this path before the contract was published publicly. Prefer /openapi.json.",
        "responses": {
          "200": {
            "description": "The OpenAPI 3.1 document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid authentication.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/agent/mcp-manifest.json": {
      "get": {
        "operationId": "getAgentMcpManifest",
        "tags": [
          "discovery"
        ],
        "summary": "Deprecated MCP-style manifest.",
        "description": "A bespoke pre-MCP manifest format, superseded by the real Model Context Protocol server at POST /api/mcp and its public discovery document at GET /api/mcp/info. Retained because llms.txt and robots.txt reference it. Do not build new integrations against it.",
        "deprecated": true,
        "responses": {
          "200": {
            "description": "MCP-style manifest.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid authentication.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An AreaOps API key, minted in Settings → Developers & API and prefixed aoa_live_. Send it as `Authorization: Bearer <key>`. The /agent/* REST endpoints additionally accept a signed-in AreaOps browser session cookie."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every REST error in this API has this shape.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message."
          },
          "code": {
            "type": "string",
            "description": "Machine-readable code where one exists — e.g. usage_quota_exceeded, subscription_required, seat_limit_exceeded, not_acceptable, not_found."
          }
        },
        "required": [
          "error"
        ]
      },
      "HealthStatus": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "degraded"
            ]
          }
        },
        "required": [
          "status"
        ]
      },
      "McpInfo": {
        "type": "object",
        "description": "Public description of the MCP server and its tools.",
        "properties": {
          "name": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "transport": {
            "type": "string",
            "examples": [
              "streamable-http"
            ]
          },
          "endpoint": {
            "type": "string",
            "format": "uri"
          },
          "authentication": {
            "type": "string"
          },
          "tools": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/McpToolSummary"
            }
          },
          "docs": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      },
      "McpToolSummary": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "examples": [
              "areaops_score_territory"
            ]
          },
          "summary": {
            "type": "string"
          },
          "units": {
            "type": "integer",
            "description": "Billable units consumed per call.",
            "minimum": 1
          },
          "write": {
            "type": "boolean",
            "description": "Present and true for confirm-gated write tools."
          }
        },
        "required": [
          "name",
          "summary",
          "units"
        ]
      },
      "JsonRpcRequest": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              }
            ]
          },
          "method": {
            "type": "string",
            "examples": [
              "tools/list",
              "tools/call"
            ]
          },
          "params": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "required": [
          "jsonrpc",
          "method"
        ]
      },
      "JsonRpcResponse": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "result": {
            "type": "object",
            "additionalProperties": true
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "integer"
              },
              "message": {
                "type": "string"
              },
              "data": {
                "additionalProperties": true
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "jsonrpc"
        ]
      },
      "ZipDemographics": {
        "type": "object",
        "description": "US Census ACS 5-Year signals for one ZIP. Any field may be null where the ACS suppresses it.",
        "properties": {
          "zipCode": {
            "type": "string",
            "pattern": "^[0-9]{5}$"
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "state": {
            "type": [
              "string",
              "null"
            ]
          },
          "populationTotal": {
            "type": [
              "integer",
              "null"
            ]
          },
          "householdsTotal": {
            "type": [
              "integer",
              "null"
            ]
          },
          "medianHouseholdIncome": {
            "type": [
              "integer",
              "null"
            ]
          },
          "ownerOccupiedHouseholds": {
            "type": [
              "integer",
              "null"
            ]
          },
          "ownerOccupiedPct": {
            "type": [
              "number",
              "null"
            ]
          },
          "medianHomeValue": {
            "type": [
              "integer",
              "null"
            ]
          },
          "singleFamilyDetachedPct": {
            "type": [
              "number",
              "null"
            ]
          },
          "medianAge": {
            "type": [
              "number",
              "null"
            ]
          },
          "medianYearBuilt": {
            "type": [
              "integer",
              "null"
            ]
          },
          "homeAgePre1980Pct": {
            "type": [
              "number",
              "null"
            ]
          },
          "homeAge19801999Pct": {
            "type": [
              "number",
              "null"
            ]
          },
          "homeAge2000PlusPct": {
            "type": [
              "number",
              "null"
            ]
          },
          "sourceVintage": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "US Census ACS 5-Year"
            ]
          }
        },
        "required": [
          "zipCode"
        ]
      },
      "VerticalScore": {
        "type": "object",
        "properties": {
          "verticalKey": {
            "type": "string"
          },
          "verticalSlug": {
            "type": "string"
          },
          "verticalLabel": {
            "type": "string"
          },
          "score": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          }
        },
        "required": [
          "verticalKey",
          "verticalSlug",
          "score"
        ]
      },
      "PublicZipProfile": {
        "type": "object",
        "properties": {
          "zipCode": {
            "type": "string",
            "pattern": "^[0-9]{5}$"
          },
          "demographics": {
            "$ref": "#/components/schemas/ZipDemographics"
          },
          "scores": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VerticalScore"
            }
          },
          "metroSlug": {
            "type": [
              "string",
              "null"
            ]
          },
          "metroLabel": {
            "type": [
              "string",
              "null"
            ]
          },
          "vintage": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "zipCode",
          "demographics",
          "scores"
        ]
      },
      "PublicZipVerticalScore": {
        "type": "object",
        "properties": {
          "zipCode": {
            "type": "string",
            "pattern": "^[0-9]{5}$"
          },
          "verticalSlug": {
            "type": "string"
          },
          "score": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          },
          "demographics": {
            "$ref": "#/components/schemas/ZipDemographics"
          },
          "metroSlug": {
            "type": [
              "string",
              "null"
            ]
          },
          "metroLabel": {
            "type": [
              "string",
              "null"
            ]
          },
          "metroFullName": {
            "type": [
              "string",
              "null"
            ]
          },
          "vintage": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "zipCode",
          "verticalSlug"
        ]
      },
      "PublicVerticalSummary": {
        "type": "object",
        "description": "Signals, weighting narrative and sampled top ZIPs for one vertical.",
        "properties": {
          "verticalSlug": {
            "type": "string"
          },
          "verticalLabel": {
            "type": "string"
          },
          "metros": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "topZips": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RankedZip"
            }
          },
          "vintage": {
            "type": "string"
          }
        },
        "required": [
          "verticalSlug"
        ]
      },
      "RankedZip": {
        "type": "object",
        "properties": {
          "zipCode": {
            "type": "string",
            "pattern": "^[0-9]{5}$"
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "state": {
            "type": [
              "string",
              "null"
            ]
          },
          "score": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "rank": {
            "type": "integer",
            "minimum": 1
          }
        },
        "required": [
          "zipCode",
          "score"
        ]
      },
      "PublicMetroVertical": {
        "type": "object",
        "properties": {
          "metroSlug": {
            "type": "string"
          },
          "metroLabel": {
            "type": "string"
          },
          "verticalSlug": {
            "type": "string"
          },
          "zipCount": {
            "type": "integer",
            "minimum": 0
          },
          "averageScore": {
            "type": [
              "number",
              "null"
            ]
          },
          "topZips": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RankedZip"
            }
          },
          "vintage": {
            "type": "string"
          }
        },
        "required": [
          "metroSlug",
          "verticalSlug"
        ]
      },
      "PublicBestZips": {
        "type": "object",
        "properties": {
          "metroSlug": {
            "type": "string"
          },
          "verticalSlug": {
            "type": "string"
          },
          "zips": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RankedZip"
            }
          },
          "sampled": {
            "type": "boolean",
            "description": "True when the list is truncated to the public table limit."
          },
          "vintage": {
            "type": "string"
          }
        },
        "required": [
          "metroSlug",
          "verticalSlug",
          "zips"
        ]
      },
      "PublicDataStudy": {
        "type": "object",
        "description": "Aggregate study findings across launched verticals and metros.",
        "properties": {
          "verticals": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "metros": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "vintage": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SharedMapMeta": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string"
          },
          "brandName": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdByName": {
            "type": [
              "string",
              "null"
            ]
          },
          "hideBranding": {
            "type": "boolean"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "kind": {
            "type": "string",
            "enum": [
              "brand",
              "zip_snapshot"
            ]
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "vertical": {
            "type": [
              "string",
              "null"
            ]
          },
          "frozenAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "token",
          "kind"
        ]
      },
      "GeoJsonFeatureCollection": {
        "type": "object",
        "description": "RFC 7946 FeatureCollection. Feature properties are allow-listed.",
        "properties": {
          "type": {
            "type": "string",
            "const": "FeatureCollection"
          },
          "features": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "const": "Feature"
                },
                "geometry": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true
                },
                "properties": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true
                }
              },
              "required": [
                "type",
                "geometry"
              ]
            }
          }
        },
        "required": [
          "type",
          "features"
        ]
      },
      "MapContext": {
        "type": "object",
        "properties": {
          "layers": {
            "type": "object",
            "additionalProperties": true
          },
          "meta": {
            "type": "object",
            "description": "Render metadata, including the palette for each basemap style.",
            "additionalProperties": true
          }
        }
      },
      "AgentCapabilities": {
        "type": "object",
        "properties": {
          "capabilities": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                }
              },
              "required": [
                "name",
                "description"
              ]
            }
          },
          "access": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "plan": {
                "type": "string"
              },
              "subscriptionStatus": {
                "type": "string"
              },
              "requiredPlan": {
                "type": "string"
              },
              "reason": {
                "type": "string"
              }
            },
            "required": [
              "enabled"
            ]
          }
        },
        "required": [
          "capabilities",
          "access"
        ]
      },
      "TerritoryPlanRequest": {
        "type": "object",
        "description": "Supply either metroSlug or zipCodes. Supplying neither is a 400.",
        "properties": {
          "vertical": {
            "type": "string",
            "description": "Vertical slug to score for.",
            "examples": [
              "roofing"
            ]
          },
          "metroSlug": {
            "type": "string",
            "description": "Launched metro to score across.",
            "examples": [
              "atlanta-ga"
            ]
          },
          "zipCodes": {
            "type": "array",
            "description": "Explicit ZIP list to score instead of a metro.",
            "maxItems": 250,
            "items": {
              "type": "string",
              "pattern": "^[0-9]{5}$"
            }
          },
          "objective": {
            "type": "string",
            "description": "What the plan is for. Changes which signals are emphasised and what next actions are returned.",
            "enum": [
              "expansion",
              "lsa",
              "direct_mail",
              "cleanup"
            ],
            "default": "expansion"
          },
          "maxResults": {
            "type": "integer",
            "minimum": 3,
            "maximum": 50,
            "default": 12
          }
        },
        "required": [
          "vertical"
        ],
        "anyOf": [
          {
            "required": [
              "metroSlug"
            ]
          },
          {
            "required": [
              "zipCodes"
            ]
          }
        ],
        "additionalProperties": false
      },
      "TerritoryPlan": {
        "type": "object",
        "properties": {
          "vertical": {
            "type": "string"
          },
          "objective": {
            "type": "string"
          },
          "metroSlug": {
            "type": [
              "string",
              "null"
            ]
          },
          "recommendations": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "zipCode": {
                  "type": "string",
                  "pattern": "^[0-9]{5}$"
                },
                "city": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "state": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "score": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 100
                },
                "rank": {
                  "type": "integer",
                  "minimum": 1
                },
                "rationale": {
                  "type": "string",
                  "description": "Plain-language reasons behind the score."
                }
              },
              "required": [
                "zipCode",
                "score"
              ]
            }
          },
          "nextActions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sourceNotes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "usage": {
            "type": "object",
            "properties": {
              "unitsCharged": {
                "type": "integer"
              },
              "includedUnitsPerMonth": {
                "type": "integer"
              },
              "unitsUsedThisMonth": {
                "type": "integer"
              }
            }
          }
        },
        "required": [
          "vertical",
          "recommendations"
        ]
      }
    }
  },
  "x-areaops-rate-limits": {
    "convention": "Every rate-limited response carries RateLimit and RateLimit-Policy (IETF structured fields: q=quota, w=window seconds, r=remaining, t=seconds to reset) plus the X-RateLimit-Limit/Remaining/Reset triple. A 429 additionally carries Retry-After in seconds.",
    "policies": [
      {
        "path": "POST /api/mcp",
        "limit": 60,
        "window": "1 minute",
        "scope": "per API key"
      },
      {
        "path": "POST /api/mcp",
        "limit": 2000,
        "window": "1 day",
        "scope": "per API key"
      },
      {
        "path": "GET /api/mcp/info",
        "limit": 60,
        "window": "1 minute",
        "scope": "per IP"
      },
      {
        "path": "GET /api/public/*",
        "limit": 100,
        "window": "1 minute",
        "scope": "per IP"
      },
      {
        "path": "GET /api/shared/*",
        "limit": 240,
        "window": "1 minute",
        "scope": "per IP"
      },
      {
        "path": "GET /api/map-context",
        "limit": 240,
        "window": "1 minute",
        "scope": "per IP"
      }
    ]
  },
  "x-areaops-usage": {
    "includedUnitsPerMonth": 1000,
    "territoryPlanUnitsPerCall": 10,
    "overage": "$5 per 100 units beyond the included allowance on subscribed workspaces; trial workspaces pause at the included quota."
  },
  "x-areaops-data-boundary": {
    "publicSampleLimit": 5,
    "publicTableLimit": 10,
    "paidPlanResponseLimit": 50,
    "publicLayer": "Public SEO and llms.txt content is citation-safe and intentionally sampled. Do not treat public pages as a full data export.",
    "paidLayer": "Authenticated Agent/API access returns personalized territory plans, larger ranked lists, and machine-readable contracts under account controls and rate limits."
  }
}
