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.
