openapi: '3.1.0'
info:
  title: Manhattin Public API
  version: '1.5.0'
  summary: Read rental listings and submit leads for properties managed in Manhattin.
  description: |-
    Manhattin is property management software for Canada. This API is the integration surface
    for a property or portfolio website: read the units you can advertise, read one unit in
    detail, and post a contact-form submission back as a lead.

    ## When to use this API

    - You are building or maintaining a rental website (WordPress, Next.js, static) and need a
      live vacancy list for buildings managed in Manhattin.
    - You are wiring a contact or "book a viewing" form on such a site to the property manager.
    - You are an agency or listing portal syncing availability for partner buildings.

    It is NOT a general property-management API: there is no endpoint for leases, rent
    payments, maintenance requests, accounting or tenant records, and no endpoint writes unit
    state. Staff own that data inside Manhattin.

    ## Three key families, three surfaces

    | Key prefix | Scope | Endpoints |
    | --- | --- | --- |
    | `pk_live_` | one property | `/api/public/availability`, `/api/public/units`, `/api/public/unit/{unit_id}`, `/api/public/inquiries` |
    | `ok_live_` | one organization, many properties | `/api/public/org/*` |
    | `agency_live_` | properties under a partnership agreement | `/api/v1/agency/availability` |

    Sending a key to the wrong family returns `403 INVALID_KEY`. This is the single most
    common integration mistake.

    ## Getting a key

    Keys are issued by Manhattin. Email developer@manhattin.com, or claim a key you were sent an
    invitation for at https://manhattin.com/api-key/claim. The plain-text key is shown once and stored
    only as a SHA-256 hash. Keep it server-side: `POST` on the inquiry endpoints creates lead
    records, so a leaked key lets anyone file inquiries against your property.

    ## Conventions

    - No pagination, no cursors: list endpoints return the whole result set.
    - No webhooks. Poll on a schedule and cache on your side.
    - Errors are always JSON: `{ "error": "<code>", "message": "<human text>" }`.
    - Every response carries `RateLimit`, `RateLimit-Policy`, `RateLimit-Limit`,
      `RateLimit-Remaining` and `RateLimit-Reset`, plus `Retry-After` on a 429. The
      publicly cacheable documents (`/openapi.json`, `/api/docs/*`) carry the policy
      without live counters, because a shared cache would serve one client's numbers
      to everyone.
    - To watch the live counters without a key, call `GET https://manhattin.com/api/public/pricing`:
      it is unauthenticated and uncached, so it answers 200 with the full set. Browser
      callers can read them cross-origin — every RateLimit header is named in
      `Access-Control-Expose-Headers`.

    ## MCP server

    The same endpoints are exposed as sixteen Model Context Protocol tools at
    `https://manhattin.com/api/mcp`, over the Streamable HTTP transport (JSON-RPC 2.0, protocol
    versions `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`).
    Discovery documents:

    - `https://manhattin.com/.well-known/mcp` — manifest (SEP-1960)
    - `https://manhattin.com/.well-known/mcp/server-card.json` — server card (SEP-1649)

    It authenticates with the same keys, calls these same endpoints, and is subject to
    the same rate limits — it is a client of this API, not a privileged path around it.
    The tools `list_pricing_plans` and `get_openapi_specification` need no key.

    Five further tools serve the developer building an integration rather than the site
    running it — they read the API-key console: which property a key is bound to, what
    the site actually sent, which posted fields the endpoint ignored. Those take a
    console token in `X-Manhattin-Console-Token`, not an API key, and contact details
    in the submissions view are redacted unless explicitly requested.

    ## Function calling

    Every operation here is also published as a ready-made tool at `https://manhattin.com/tools.json`,
    in both the OpenAI `function` shape and the Anthropic `input_schema` shape. Use it
    rather than converting this document yourself: the flattening of path, query, header
    and body parameters into one JSON Schema is the one we intend, and the five
    operations that take no parameters carry an explicit empty object schema there
    rather than no schema at all.

    ## Versioning and deprecation

    The API is versioned in the URL path: `/api/public/*` and `/api/v1/*` are the current,
    stable surfaces, and a breaking change gets a new path rather than altering one already
    in use. Adding a field to a response is not a breaking change — ignore fields you do not
    recognise.

    A retiring endpoint announces it in its own responses, so no changelog needs watching:

    - `Deprecation` (RFC 9745) — an `@`-prefixed epoch second, when it became deprecated.
    - `Sunset` (RFC 8594) — an HTTP-date, when it stops working.
    - `Link: <https://manhattin.com/developers>; rel="deprecation"` — what to do instead.
    - `"deprecated": true` on the operation in this document.

    After the sunset date it answers `410 ENDPOINT_WITHDRAWN` and keeps sending both
    headers, so a client arriving late still learns when the ground moved. A withdrawn
    endpoint never degrades to a `404`, and its path is never reused.

    At least 90 days separate the `Deprecation` date from the `Sunset` date. The only
    exception is a retirement forced by a security or legal obligation, where the window
    may be shorter; the headers carry the real dates either way, so read them rather than
    counting on the minimum.

    ## Withdrawn

    `POST /api/public/units/update-status` was withdrawn on 2026-08-04 and answers
    `410 ENDPOINT_WITHDRAWN`. It is deliberately absent from `paths`: an operation
    listed there becomes a callable tool in a function-calling bridge, and a tool
    that can only fail is worse than no tool. Unit availability is owned by staff in
    Manhattin.
  contact:
    name: Manhattin developer support
    email: 'developer@manhattin.com'
    url: 'https://manhattin.com/developers'
  license:
    name: Proprietary
    url: 'https://manhattin.com/terms'
  termsOfService: 'https://manhattin.com/terms'
servers:
  - url: 'https://manhattin.com'
    description: Production
externalDocs:
  description: 'Prose documentation, including the agent brief'
  url: 'https://manhattin.com/developers'
tags:
  - name: Property API
    description: 'Property-scoped endpoints. The property is resolved from the `pk_live_` key; there is no property parameter.'
    externalDocs:
      description: API reference
      url: 'https://manhattin.com/api/docs/api/API_REFERENCE.md?raw=1'
  - name: Organization API
    description: 'Portfolio-wide endpoints for an `ok_live_` key. Response shapes differ from the property API.'
    externalDocs:
      description: Organization guide
      url: 'https://manhattin.com/api/docs/api/ORG_API_GUIDE.md?raw=1'
  - name: Agency API
    description: 'Availability sync for agency partners holding an `agency_live_` key.'
  - name: Discovery
    description: Unauthenticated endpoints an agent can call to understand the product.
security:
  - PropertyApiKey: []
paths:
  '/openapi.json':
    get:
      operationId: getOpenApiSpecification
      tags:
        - Discovery
      summary: Fetch this OpenAPI document
      description: 'Returns the machine-readable description of every endpoint below. Requires no authentication and no key, so an agent can discover the API surface before anyone has issued it credentials. The YAML form is at /api/openapi.yaml.'
      security: []
      responses:
        '200':
          description: The OpenAPI 3.1 document.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/OpenApiDocument'
  '/api/docs/{path}':
    get:
      operationId: getDocumentationFile
      tags:
        - Discovery
      summary: Fetch an integration guide as markdown or HTML
      description: 'Serves the prose documentation under `docs/api/` and `docs/wordpress-plugin/`. Markdown files render as styled HTML for a browser and as raw markdown for an agent — set `raw=1`, `format=md`, or send `Accept: text/markdown`. No key required. Known files: `api/FOR_AGENTS.md`, `api/QUICK_START.md`, `api/API_REFERENCE.md`, `api/ORG_API_GUIDE.md`, `api/CLIENT_OPTIONS.md`, `api/TESTING.md`.'
      security: []
      parameters:
        - name: path
          in: path
          required: true
          description: 'Folder and file, slash-separated, e.g. `api/FOR_AGENTS.md`. Only the `api` and `wordpress-plugin` folders are served; anything else answers 404.'
          schema:
            type: string
            pattern: '^(api|wordpress-plugin)/.+$'
          example: api/FOR_AGENTS.md
        - name: raw
          in: query
          required: false
          description: 'Set to `1` for plain markdown with no HTML wrapper.'
          schema:
            type: string
            enum:
              - '1'
          example: '1'
        - name: format
          in: query
          required: false
          description: 'Set to `md` for plain markdown. Equivalent to `raw=1`.'
          schema:
            type: string
            enum:
              - md
        - name: Accept
          in: header
          required: false
          description: 'Send `text/markdown` to receive markdown instead of the HTML rendering, without a query parameter.'
          schema:
            type: string
          example: text/markdown
      responses:
        '200':
          description: 'The document, as markdown or as rendered HTML.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
            Cache-Control:
              description: 'Always `public, max-age=3600`.'
              schema:
                type: string
          content:
            text/markdown:
              schema:
                type: string
            text/html:
              schema:
                type: string
        '400':
          description: The path tried to escape the documentation folder.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Invalid path
                message: Invalid path
        '404':
          description: 'No such document, or a folder that is not published.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: File not found
                message: File not found
        '429':
          description: 'Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
            Retry-After:
              '$ref': '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: RATE_LIMIT_EXCEEDED
                message: Rate limit exceeded
  '/api/public/pricing':
    get:
      operationId: listPublicPricingPlans
      tags:
        - Discovery
      summary: List published subscription plans
      description: 'Returns the Manhattin subscription plans shown on the public pricing section, including the free tier. Requires no authentication — this is the endpoint to call to confirm unauthenticated access works before requesting a key.'
      security: []
      responses:
        '200':
          description: 'Published plans, in display order.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/PricingResponse'
        '500':
          description: Unhandled failure. Retry with backoff.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Internal server error
                message: An unexpected error occurred
  '/api/public/availability':
    get:
      operationId: listPropertyAvailability
      tags:
        - Property API
      summary: List units that can be advertised on this property
      description: |-
        Marketable units for the property behind the key: vacant units with no lease starting
        later than today, plus occupied units whose active lease ends within three calendar
        months. Units at status `leased`, `reserved` or `maintenance` are never returned.

        Commercial units are NOT filtered out — check `classification` if your site is
        residential-only. This endpoint sends no `Cache-Control`; cache it yourself.

        Not interchangeable with `listPropertyUnits`: the rent field is `rent` here and
        `monthly_rent` there, and the two disagree about which vacant units count as available.
      parameters:
        - name: unit_type
          in: query
          required: false
          description: 'Exact match on the unit type string, e.g. `4 1/2`. Free text, not a fixed vocabulary.'
          schema:
            type: string
          example: '4 1/2'
        - name: bedrooms
          in: query
          required: false
          description: Exact match on bedroom count.
          schema:
            type: integer
            minimum: 0
          example: 2
        - name: min_rent
          in: query
          required: false
          description: Keep units whose market rent is greater than or equal to this value.
          schema:
            type: number
            minimum: 0
          example: 1000
        - name: max_rent
          in: query
          required: false
          description: Keep units whose market rent is less than or equal to this value.
          schema:
            type: number
            minimum: 0
          example: 2000
      responses:
        '200':
          description: Matching units.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/PropertyAvailabilityResponse'
        '401':
          description: No API key was supplied.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Missing API key
                message: Please provide an API key in the X-API-Key header or Authorization header
        '403':
          description: 'The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: INVALID_KEY
                message: Invalid API key
        '429':
          description: 'Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
            Retry-After:
              '$ref': '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: RATE_LIMIT_EXCEEDED
                message: Rate limit exceeded
        '500':
          description: Unhandled failure. Retry with backoff.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Internal server error
                message: An unexpected error occurred
  '/api/public/units':
    get:
      operationId: listPropertyUnits
      tags:
        - Property API
      summary: 'List every unit on this property, available and not'
      description: |-
        The whole building roster plus active promotions. Every active unit appears in exactly
        one of `units` (advertisable) and `unavailable_units` (with a reason).

        Accepts no query parameters — filter client-side. There is no top-level `success`
        field, so `if (data.success)` silently skips every unit here; check `response.ok` or
        the presence of `units`. Responses carry `Cache-Control: public, max-age=300`, so a
        status change made by staff can take up to five minutes to appear.
      responses:
        '200':
          description: The full roster.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
            Cache-Control:
              description: 'Always `public, max-age=300`.'
              schema:
                type: string
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/PropertyUnitsResponse'
        '401':
          description: No API key was supplied.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Missing API key
                message: Please provide an API key in the X-API-Key header or Authorization header
        '403':
          description: 'The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: INVALID_KEY
                message: Invalid API key
        '429':
          description: 'Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
            Retry-After:
              '$ref': '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: RATE_LIMIT_EXCEEDED
                message: Rate limit exceeded
        '500':
          description: Unhandled failure. Retry with backoff.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Internal server error
                message: An unexpected error occurred
  '/api/public/unit/{unit_id}':
    get:
      operationId: getPropertyUnit
      tags:
        - Property API
      summary: Fetch one residential unit in detail
      description: 'Detail view for a unit on the property behind the key. Commercial units are never returned here — they answer 404, as do unknown, inactive, and other properties’ units. The response does not distinguish between those cases.'
      parameters:
        - name: unit_id
          in: path
          required: true
          description: 'The `id` of a unit as returned by `listPropertyAvailability` or `listPropertyUnits`.'
          schema:
            type: string
            format: uuid
          example: '3f0b1c8e-6a2d-4b91-8f77-1d2c3e4a5b60'
      responses:
        '200':
          description: The unit.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/PropertyUnitDetailResponse'
        '401':
          description: No API key was supplied.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Missing API key
                message: Please provide an API key in the X-API-Key header or Authorization header
        '403':
          description: 'The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: INVALID_KEY
                message: Invalid API key
        '404':
          description: No residential unit with that id on this property.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Unit not found
                message: The requested unit does not exist or is not available
        '429':
          description: 'Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
            Retry-After:
              '$ref': '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: RATE_LIMIT_EXCEEDED
                message: Rate limit exceeded
        '500':
          description: Unhandled failure. Retry with backoff.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Internal server error
                message: An unexpected error occurred
  '/api/public/inquiries':
    post:
      operationId: createPropertyInquiry
      tags:
        - Property API
      summary: Submit a contact-form inquiry for this property
      description: |-
        Creates a marketing lead on the property behind the key. `email` is the only required
        field. Fields not listed in the request schema are discarded silently — including
        `budget_range`; put that information in `message`.

        Cloudflare Turnstile verification is in soft launch: send the token as
        `cf-turnstile-response` in the body or the `X-Turnstile-Token` header. A missing token
        is accepted today and will be rejected with `TURNSTILE_REQUIRED` once enforcement is on.

        Responds 201, not 200.
      parameters:
        - name: X-Turnstile-Token
          in: header
          required: false
          description: 'Cloudflare Turnstile token. Read before the body field of the same purpose. Soft-launch: a missing token is accepted today and will be rejected with `TURNSTILE_REQUIRED` once enforcement is on.'
          schema:
            type: string
        - name: CF-Turnstile-Response
          in: header
          required: false
          description: The same token under the name the Cloudflare widget uses.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              '$ref': '#/components/schemas/PropertyInquiryRequest'
      responses:
        '201':
          description: Lead created.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/InquiryCreatedResponse'
        '400':
          description: 'Missing or malformed `email`, or a `unit_id` belonging to another property (`UNIT_MISMATCH`).'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Invalid email
                message: Please provide a valid email address
        '401':
          description: No API key was supplied.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Missing API key
                message: Please provide an API key in the X-API-Key header or Authorization header
        '403':
          description: 'The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: INVALID_KEY
                message: Invalid API key
        '404':
          description: 'The supplied `unit_id` does not exist.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: UNIT_NOT_FOUND
                message: The specified unit could not be found.
        '429':
          description: 'Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
            Retry-After:
              '$ref': '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: RATE_LIMIT_EXCEEDED
                message: Rate limit exceeded
        '500':
          description: Unhandled failure. Retry with backoff.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Internal server error
                message: An unexpected error occurred
  '/api/public/org/properties':
    get:
      operationId: listOrganizationProperties
      tags:
        - Organization API
      summary: List every active property in the organization
      description: 'Returns each active property with its address, amenities, photos and unit counts, ordered by display name. Accepts no query parameters. `available_units` counts units sitting at raw status `vacant`; it is not the same figure as the `count` from `listOrganizationAvailability`, which applies the marketing rules.'
      security:
        - OrgApiKey: []
        - OrgBearerKey: []
      responses:
        '200':
          description: 'The organization’s properties.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/OrganizationPropertiesResponse'
        '401':
          description: No API key was supplied.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Missing API key
                message: Please provide an API key in the X-API-Key header or Authorization header
        '403':
          description: 'The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: INVALID_KEY
                message: Invalid API key
        '429':
          description: 'Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
            Retry-After:
              '$ref': '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: RATE_LIMIT_EXCEEDED
                message: Rate limit exceeded
        '500':
          description: Unhandled failure. Retry with backoff.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Internal server error
                message: An unexpected error occurred
  '/api/public/org/availability':
    get:
      operationId: listOrganizationAvailability
      tags:
        - Organization API
      summary: List advertisable units across the whole portfolio
      description: 'Marketable units from every active property: vacant with no future lease signed, or occupied with an active lease ending within three calendar months and no successor. Includes both residential and commercial units. Sorted by property name, then unit number. An unknown `property_id` returns an empty successful result, not an error.'
      security:
        - OrgApiKey: []
        - OrgBearerKey: []
      parameters:
        - name: property_id
          in: query
          required: false
          description: 'Restrict to one property, as returned by `listOrganizationProperties`.'
          schema:
            type: string
            format: uuid
        - name: unit_type
          in: query
          required: false
          description: Exact match on the unit type string.
          schema:
            type: string
        - name: bedrooms
          in: query
          required: false
          description: Exact match on bedroom count.
          schema:
            type: integer
            minimum: 0
        - name: min_rent
          in: query
          required: false
          description: Market rent greater than or equal to this value. Units with no market rent are excluded whenever either bound is supplied.
          schema:
            type: number
            minimum: 0
        - name: max_rent
          in: query
          required: false
          description: Market rent less than or equal to this value.
          schema:
            type: number
            minimum: 0
      responses:
        '200':
          description: 'Matching units, each carrying its property.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/OrganizationAvailabilityResponse'
        '401':
          description: No API key was supplied.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Missing API key
                message: Please provide an API key in the X-API-Key header or Authorization header
        '403':
          description: 'The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: INVALID_KEY
                message: Invalid API key
        '429':
          description: 'Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
            Retry-After:
              '$ref': '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: RATE_LIMIT_EXCEEDED
                message: Rate limit exceeded
        '500':
          description: Unhandled failure. Retry with backoff.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Internal server error
                message: An unexpected error occurred
  '/api/public/org/units':
    get:
      operationId: listOrganizationUnits
      tags:
        - Organization API
      summary: List every active unit in the portfolio with a computed availability status
      description: 'Every active, non-archived unit with an `availability_status`, the current lease end date, any successor lease start date, and a transfer intent date. Use this when you want to run your own display rules rather than the marketing rules baked into `listOrganizationAvailability`. Only `property_id` is supported as a filter.'
      security:
        - OrgApiKey: []
        - OrgBearerKey: []
      parameters:
        - name: property_id
          in: query
          required: false
          description: Restrict to one property. An unknown id returns an empty successful result.
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Every active unit.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/OrganizationUnitsResponse'
        '401':
          description: No API key was supplied.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Missing API key
                message: Please provide an API key in the X-API-Key header or Authorization header
        '403':
          description: 'The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: INVALID_KEY
                message: Invalid API key
        '429':
          description: 'Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
            Retry-After:
              '$ref': '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: RATE_LIMIT_EXCEEDED
                message: Rate limit exceeded
        '500':
          description: Unhandled failure. Retry with backoff.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Internal server error
                message: An unexpected error occurred
  '/api/public/org/inquiries':
    post:
      operationId: createOrganizationInquiry
      tags:
        - Organization API
      summary: Submit a contact-form inquiry for the organization
      description: 'Creates a lead against the organization behind the key. `property_id` is optional — omit it for a portfolio-level lead. Supplying only `unit_id` infers the property. Responds 201. Note the asymmetric validation codes: a deactivated property in your own organization answers `PROPERTY_NOT_FOUND` (404), while a property belonging to another organization answers `PROPERTY_MISMATCH` (400).'
      security:
        - OrgApiKey: []
        - OrgBearerKey: []
      parameters:
        - name: X-Turnstile-Token
          in: header
          required: false
          description: 'Cloudflare Turnstile token. Read before the body field of the same purpose. Soft-launch: a missing token is accepted today and will be rejected with `TURNSTILE_REQUIRED` once enforcement is on.'
          schema:
            type: string
        - name: CF-Turnstile-Response
          in: header
          required: false
          description: The same token under the name the Cloudflare widget uses.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              '$ref': '#/components/schemas/OrganizationInquiryRequest'
      responses:
        '201':
          description: Lead created.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/InquiryCreatedResponse'
        '400':
          description: 'Missing or malformed `email`, or a `PROPERTY_MISMATCH` / `UNIT_MISMATCH` / `UNIT_PROPERTY_MISMATCH`.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: UNIT_PROPERTY_MISMATCH
                message: The specified unit does not belong to the specified property.
        '401':
          description: No API key was supplied.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Missing API key
                message: Please provide an API key in the X-API-Key header or Authorization header
        '403':
          description: 'The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: INVALID_KEY
                message: Invalid API key
        '404':
          description: '`PROPERTY_NOT_FOUND` or `UNIT_NOT_FOUND`.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: PROPERTY_NOT_FOUND
                message: The specified property could not be found.
        '429':
          description: 'Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
            Retry-After:
              '$ref': '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: RATE_LIMIT_EXCEEDED
                message: Rate limit exceeded
        '500':
          description: Unhandled failure. Retry with backoff.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Internal server error
                message: An unexpected error occurred
  '/api/v1/agency/availability':
    get:
      operationId: listAgencyAvailability
      tags:
        - Agency API
      summary: Sync unit availability for partner properties
      description: 'Every unit on the properties covered by your partnership agreements, split into `available` and `unavailable`, with a `meta` block giving counts and a pull timestamp. Rate limited to 5 requests per day per key, resetting at midnight UTC — sync on a schedule, never per page view. Keys expire when the partnership agreement ends.'
      security:
        - AgencyApiKey: []
      responses:
        '200':
          description: Availability snapshot.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/AgencyAvailabilityResponse'
        '401':
          description: No API key was supplied.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Missing API key
                message: Please provide an API key in the X-API-Key header or Authorization header
        '403':
          description: 'The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: INVALID_KEY
                message: Invalid API key
        '429':
          description: 'Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.'
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
            Retry-After:
              '$ref': '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: RATE_LIMIT_EXCEEDED
                message: Rate limit exceeded
        '500':
          description: Unhandled failure. Retry with backoff.
          headers:
            RateLimit:
              '$ref': '#/components/headers/RateLimit'
            RateLimit-Policy:
              '$ref': '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              '$ref': '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              '$ref': '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              '$ref': '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                '$ref': '#/components/schemas/Error'
              example:
                error: Internal server error
                message: An unexpected error occurred
components:
  headers:
    RateLimit:
      description: 'Current state of the applicable policy, as an RFC 8941 structured field: `"<policy>";r=<remaining>;t=<seconds until reset>`.'
      schema:
        type: string
      example: '"publicApi";r=29;t=41'
    RateLimitPolicy:
      description: 'The policy itself: `"<policy>";q=<requests>;w=<window in seconds>`. Constant for a given endpoint tier.'
      schema:
        type: string
      example: '"publicApi";q=30;w=60'
    RateLimitLimit:
      description: Requests permitted in the current window.
      schema:
        type: integer
        minimum: 0
      example: 30
    RateLimitRemaining:
      description: Requests still permitted in the current window.
      schema:
        type: integer
        minimum: 0
      example: 29
    RateLimitReset:
      description: 'Seconds until the window resets. Note the legacy `X-RateLimit-Reset`, also sent, is an epoch timestamp in milliseconds instead.'
      schema:
        type: integer
        minimum: 0
      example: 41
    RetryAfter:
      description: 'Seconds to wait before retrying. Sent on 429 only, and never zero.'
      schema:
        type: integer
        minimum: 1
      example: 41
  securitySchemes:
    PropertyApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: 'A property key: `pk_live_` followed by 64 lowercase hex characters (72 characters total). Bound to exactly one property. Checked before the Authorization header. Keep it server-side.'
    PropertyBearerKey:
      type: http
      scheme: bearer
      bearerFormat: 'pk_live_<64 hex>'
      description: 'The same property key sent as `Authorization: Bearer pk_live_...`.'
    OrgApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: 'An organization key: `ok_live_...`, scoped to every active property in one organization. Rejected with `INVALID_KEY` on the property endpoints.'
    OrgBearerKey:
      type: http
      scheme: bearer
      bearerFormat: ok_live_...
      description: 'The same organization key sent as `Authorization: Bearer ok_live_...`.'
    AgencyApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: 'An agency partner key, `agency_live_XXXX_YYYY...`, scoped to the properties in your partnership agreements.'
  schemas:
    OpenApiDocument:
      type: object
      title: OpenApiDocument
      description: 'An OpenAPI 3.1 description document. Only the fields every reader needs are constrained here; the rest is the OpenAPI specification itself.'
      required:
        - openapi
        - info
        - paths
      properties:
        openapi:
          type: string
          description: 'Specification version, e.g. `3.1.0`.'
        info:
          type: object
          description: 'Title, version and contact details for this API.'
          required:
            - title
            - version
          properties:
            title:
              type: string
            version:
              type: string
            description:
              type: string
        servers:
          type: array
          description: Origins this document describes.
          items:
            type: object
            properties:
              url:
                type: string
                format: uri
              description:
                type: string
        paths:
          type: object
          description: Path item objects keyed by URL template.
          additionalProperties: true
        components:
          type: object
          description: 'Reusable schemas, headers and security schemes.'
          additionalProperties: true
    Error:
      type: object
      title: Error
      description: 'Every error response, on every endpoint. `error` is a stable machine code for the documented codes (`INVALID_KEY`, `RATE_LIMIT_EXCEEDED`, `ORIGIN_NOT_ALLOWED`, `TURNSTILE_REQUIRED`, `UNIT_NOT_FOUND`, `UNIT_MISMATCH`, `PROPERTY_NOT_FOUND`, `PROPERTY_MISMATCH`, `UNIT_PROPERTY_MISMATCH`, `CONFIGURATION_ERROR`, `ENDPOINT_WITHDRAWN`, `NOT_FOUND`) and a short human label otherwise. Match on the HTTP status plus `error`. Retry 5xx with backoff; do not retry a 4xx other than 429.'
      required:
        - error
        - message
      properties:
        error:
          type: string
          description: Machine code or short label.
        message:
          type: string
          description: Human-readable explanation.
        retryAfter:
          type: integer
          description: Seconds to wait. Present on the per-IP burst 429 only.
        hint:
          type: string
          description: 'What to do next, when the API can suggest one.'
        documentation_url:
          type: string
          format: uri
          description: Where the resolution is documented.
    UnitPhoto:
      type: object
      title: UnitPhoto
      description: 'One photo. Extra keys may be present; store the object as received.'
      properties:
        url:
          type: string
          format: uri
          description: Public URL of the image.
        storage_path:
          type: string
          description: Internal storage key. Opaque.
        caption:
          type:
            - string
            - 'null'
        is_primary:
          type: boolean
          description: 'True for the unit’s lead image.'
        uploaded_at:
          type: string
          format: date-time
    ParkingSpot:
      type: object
      title: ParkingSpot
      description: A parking spot rentable with the unit.
      properties:
        spot_number:
          type: string
        spot_type:
          type: string
          enum:
            - indoor
            - outdoor
            - covered
            - handicap
          description: Physical kind of spot.
        additional_cost:
          type:
            - number
            - 'null'
    StorageUnit:
      type: object
      title: StorageUnit
      description: A storage locker rentable with the unit.
      properties:
        unit_number:
          type: string
        size:
          type:
            - string
            - 'null'
        location:
          type:
            - string
            - 'null'
        additional_cost:
          type:
            - number
            - 'null'
    Promotion:
      type: object
      title: Promotion
      description: 'An active, publishable promotion. `valid_from` / `valid_until` may be null, meaning no bound on that side; the API does not filter on them, so check the dates before displaying an offer.'
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description_en:
          type:
            - string
            - 'null'
        description_fr:
          type:
            - string
            - 'null'
        description_es:
          type:
            - string
            - 'null'
        discount_type:
          type: string
          enum:
            - flat_off_market
            - percentage_off_market
            - free_month
            - rent_credit
            - custom
            - free_addon
          description: 'Decides which of `discount_value_cents` / `discount_percentage` is set.'
        discount_value_cents:
          type:
            - integer
            - 'null'
          description: Amount in cents. For a unit-targeted promotion this is the per-unit override.
        discount_percentage:
          type:
            - number
            - 'null'
          description: 'Percentage, e.g. 10.00.'
        duration_months:
          type:
            - integer
            - 'null'
        valid_from:
          type:
            - string
            - 'null'
        valid_until:
          type:
            - string
            - 'null'
        discount_timing:
          type: string
          enum:
            - from_start
            - from_end
          description: Whether the discount applies at the beginning or end of the lease.
    AvailableUnit:
      type: object
      title: AvailableUnit
      description: 'A unit returned by `listPropertyAvailability`. Rent is in `rent`.'
      required:
        - id
        - unit_number
        - availability_reason
      properties:
        id:
          type: string
          format: uuid
        unit_number:
          type: string
        floor:
          type:
            - integer
            - 'null'
        unit_type:
          type:
            - string
            - 'null'
          description: 'Free text, e.g. `4 1/2`, `Retail`.'
        classification:
          type: string
          enum:
            - residential
            - commercial
          description: 'Defaults to `residential` when unset. Filter on it if your site is residential-only.'
        bedrooms:
          type:
            - integer
            - 'null'
        bathrooms:
          type:
            - number
            - 'null'
          description: 'One decimal place, e.g. 1.5.'
        square_feet:
          type:
            - integer
            - 'null'
        rent:
          type:
            - number
            - 'null'
          description: 'The unit’s market rent.'
        features:
          type:
            - array
            - 'null'
          items:
            type: string
        parking_spots:
          type: array
          items:
            '$ref': '#/components/schemas/ParkingSpot'
        storage_units:
          type: array
          items:
            '$ref': '#/components/schemas/StorageUnit'
        primary_photo_url:
          type:
            - string
            - 'null'
        photos:
          type: array
          items:
            '$ref': '#/components/schemas/UnitPhoto'
        floor_plan_url:
          type:
            - string
            - 'null'
          description: 'Opaque URL carrying a `?v=` cache-busting token. Always use the latest value returned.'
        floor_area_plan_url:
          type:
            - string
            - 'null'
        unit_fact_sheet_url:
          type:
            - string
            - 'null'
        is_affordable_housing:
          type: boolean
        availability_reason:
          type: string
          enum:
            - vacant
            - lease_ending_soon
          description: Why the unit is advertisable.
        lease_end_date:
          type: string
          format: date
          description: 'Present ONLY when `availability_reason` is `lease_ending_soon`. Test for the key, not for null.'
    RosterUnit:
      type: object
      title: RosterUnit
      description: 'A unit returned by `listPropertyUnits`. Rent is in `monthly_rent`, not `rent`.'
      required:
        - id
        - unit_number
        - status
      properties:
        id:
          type: string
          format: uuid
        unit_number:
          type: string
        status:
          type: string
          enum:
            - vacant
            - occupied
            - leased
            - maintenance
            - reserved
          description: Raw unit status. These five are the complete set.
        classification:
          type: string
          enum:
            - residential
            - commercial
        monthly_rent:
          type:
            - number
            - 'null'
          description: 'Vacant: market rent, then current rent. Occupied: the lease rent, then market, then current.'
        square_feet:
          type:
            - integer
            - 'null'
        bedrooms:
          type:
            - integer
            - 'null'
        bathrooms:
          type:
            - number
            - 'null'
        floor:
          type:
            - integer
            - 'null'
        unit_type:
          type:
            - string
            - 'null'
        features:
          type: array
          items:
            type: string
        is_affordable_housing:
          type: boolean
        availability_reason:
          type: string
          enum:
            - vacant
            - lease_ending_soon
        lease_end_date:
          type: string
          format: date
          description: 'Only on `lease_ending_soon`.'
        primary_photo_url:
          type:
            - string
            - 'null'
        photos:
          type: array
          items:
            '$ref': '#/components/schemas/UnitPhoto'
        floor_plan_url:
          type:
            - string
            - 'null'
        floor_area_plan_url:
          type:
            - string
            - 'null'
        unit_fact_sheet_url:
          type:
            - string
            - 'null'
        promotions:
          type: array
          description: 'Property-wide promotions PLUS ones targeted at this unit. Property-wide entries repeat inside every unit — de-duplicate before rendering both lists.'
          items:
            '$ref': '#/components/schemas/Promotion'
    UnavailableUnit:
      type: object
      title: UnavailableUnit
      description: 'A unit that cannot be advertised, and why.'
      required:
        - id
        - unit_number
        - unavailability_reason
      properties:
        id:
          type: string
          format: uuid
        unit_number:
          type: string
        status:
          type: string
          enum:
            - vacant
            - occupied
            - leased
            - maintenance
            - reserved
        classification:
          type: string
          enum:
            - residential
            - commercial
        unavailability_reason:
          type: string
          enum:
            - reserved
            - maintenance
            - lease_active
            - future_lease
          description: '`lease_active` means spoken for now; `future_lease` means vacant with a lease starting later than today.'
    PropertyAvailabilityResponse:
      type: object
      title: PropertyAvailabilityResponse
      description: 'The `listPropertyAvailability` envelope: a success flag, the building amenities, a count and the units.'
      required:
        - success
        - count
        - units
      properties:
        success:
          type: boolean
          description: Always true on a 200 from this endpoint.
        property_amenities:
          type: array
          description: Building-level amenity labels. Free text.
          items:
            type: string
        count:
          type: integer
          description: 'Length of `units`.'
        units:
          type: array
          items:
            '$ref': '#/components/schemas/AvailableUnit'
    PropertyUnitsResponse:
      type: object
      title: PropertyUnitsResponse
      description: 'Note the absence of a `success` field — check `response.ok` instead.'
      required:
        - units
        - count
        - unavailable_units
        - unavailable_count
      properties:
        property_amenities:
          type: array
          items:
            type: string
        promotions:
          type: array
          description: Property-wide promotions only.
          items:
            '$ref': '#/components/schemas/Promotion'
        units:
          type: array
          items:
            '$ref': '#/components/schemas/RosterUnit'
        count:
          type: integer
        unavailable_units:
          type: array
          items:
            '$ref': '#/components/schemas/UnavailableUnit'
        unavailable_count:
          type: integer
    PropertyUnitDetailResponse:
      type: object
      title: PropertyUnitDetailResponse
      description: 'The `getPropertyUnit` envelope: a success flag and one residential unit.'
      required:
        - success
        - unit
      properties:
        success:
          type: boolean
        unit:
          type: object
          description: 'Always residential — commercial units answer 404 on this endpoint.'
          properties:
            id:
              type: string
              format: uuid
            unit_number:
              type: string
            floor:
              type:
                - integer
                - 'null'
            unit_type:
              type:
                - string
                - 'null'
            bedrooms:
              type:
                - integer
                - 'null'
            bathrooms:
              type:
                - number
                - 'null'
            square_feet:
              type:
                - integer
                - 'null'
            rent:
              type:
                - number
                - 'null'
              description: 'Market rent when `status` is `vacant`, otherwise the current rent.'
            status:
              type: string
              enum:
                - vacant
                - occupied
                - leased
                - maintenance
                - reserved
            is_affordable_housing:
              type: boolean
            features:
              type:
                - array
                - 'null'
              items:
                type: string
            restrictions:
              type:
                - object
                - 'null'
              description: 'Free-form. Keys seen in practice: min_age, max_age, min_income, no_pets, no_smoking, accessibility_required, max_occupants, student_housing, senior_housing.'
              additionalProperties: true
            parking_spots:
              type: array
              items:
                '$ref': '#/components/schemas/ParkingSpot'
            storage_units:
              type: array
              items:
                '$ref': '#/components/schemas/StorageUnit'
            available:
              type: boolean
              description: 'True only when `status` is `vacant`. Lease-blind: a vacant unit with a signed future lease still reports true. Use a list endpoint for the lease-aware answer.'
            primary_photo_url:
              type:
                - string
                - 'null'
            photos:
              type: array
              items:
                '$ref': '#/components/schemas/UnitPhoto'
            floor_plan_url:
              type:
                - string
                - 'null'
            floor_area_plan_url:
              type:
                - string
                - 'null'
            unit_fact_sheet_url:
              type:
                - string
                - 'null'
    PropertyInquiryRequest:
      type: object
      title: PropertyInquiryRequest
      description: The complete set of fields the endpoint reads. Anything else is ignored silently.
      required:
        - email
      properties:
        email:
          type: string
          format: email
          description: 'The only required field. Checked against `^[^\s@]+@[^\s@]+\.[^\s@]+$`.'
        first_name:
          type: string
        last_name:
          type: string
        phone:
          type: string
          description: Stored as sent. No format validation.
        message:
          type: string
          description: 'Lands in the lead notes, with `Submitted from: <origin>` appended when an Origin header is present.'
        unit_id:
          type: string
          format: uuid
          description: Must exist and belong to the property behind the key.
        preferred_move_in:
          type: string
          format: date
        requested_visit_date:
          type: string
          format: date
        newsletter_signup:
          type: boolean
          default: false
        apartment_type:
          type: string
          description: 'Normalized server-side to Quebec room counts: `2 Bedroom` and `2 chambres` both become `4 1/2`. Unrecognised values are stored verbatim.'
        language:
          type: string
          enum:
            - EN
            - FR
            - ES
          description: 'Website language. Stored as sent; not rejected if it differs.'
        cf-turnstile-response:
          type: string
          description: 'Cloudflare Turnstile token, as posted by the standard widget.'
    OrganizationInquiryRequest:
      type: object
      title: OrganizationInquiryRequest
      description: 'As `PropertyInquiryRequest`, plus an optional `property_id`.'
      required:
        - email
      properties:
        email:
          type: string
          format: email
          description: The only required field.
        first_name:
          type: string
        last_name:
          type: string
        phone:
          type: string
        message:
          type: string
          description: Lands in the lead notes.
        property_id:
          type: string
          format: uuid
          description: Omit for a portfolio-level lead not tied to a building.
        unit_id:
          type: string
          format: uuid
          description: 'When supplied without `property_id`, the property is inferred from the unit.'
        preferred_move_in:
          type: string
          format: date
        requested_visit_date:
          type: string
          format: date
        newsletter_signup:
          type: boolean
          default: false
        apartment_type:
          type: string
          description: 'Normalized to Manhattin’s vocabulary where possible.'
        language:
          type: string
          enum:
            - EN
            - FR
            - ES
        cf-turnstile-response:
          type: string
          description: Cloudflare Turnstile token.
    InquiryCreatedResponse:
      type: object
      title: InquiryCreatedResponse
      description: 'Returned with HTTP 201 by both inquiry endpoints. Error bodies carry no `success` field at all, so check the status code.'
      required:
        - success
        - lead_id
      properties:
        success:
          type: boolean
        lead_id:
          type: string
          format: uuid
          description: Identifier of the created lead.
        message:
          type: string
          description: Copy suitable for showing the visitor.
    OrganizationProperty:
      type: object
      title: OrganizationProperty
      description: 'Address parts are empty strings, not null, when unset — treat an empty string as "not available".'
      required:
        - id
        - name
      properties:
        id:
          type: string
          format: uuid
          description: 'Pass as `property_id` elsewhere.'
        name:
          type: string
        address:
          type: string
        city:
          type: string
        province:
          type: string
        postal_code:
          type: string
        phone:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
        website:
          type:
            - string
            - 'null'
        property_type:
          type:
            - string
            - 'null'
          enum:
            - residential
            - commercial
            - mixed
            - null
        year_built:
          type:
            - integer
            - 'null'
        total_floors:
          type:
            - integer
            - 'null'
        amenities:
          type: array
          items:
            type: string
        photos:
          type: array
          items:
            '$ref': '#/components/schemas/UnitPhoto'
        logo_url:
          type:
            - string
            - 'null'
        total_units:
          type: integer
          description: 'Count of active units, computed per request.'
        available_units:
          type: integer
          description: 'Count of active units at raw status `vacant`. A headline figure — not the marketable count.'
    OrganizationPropertiesResponse:
      type: object
      title: OrganizationPropertiesResponse
      description: 'The `listOrganizationProperties` envelope.'
      required:
        - success
        - count
        - properties
      properties:
        success:
          type: boolean
        count:
          type: integer
        properties:
          type: array
          items:
            '$ref': '#/components/schemas/OrganizationProperty'
    OrganizationAvailableUnit:
      allOf:
        - '$ref': '#/components/schemas/AvailableUnit'
        - type: object
          description: The portfolio variant adds the owning property.
          properties:
            property:
              type: object
              description: Identity of the building. No postal code or coordinates here.
              properties:
                id:
                  type: string
                  format: uuid
                name:
                  type: string
                address:
                  type: string
                city:
                  type: string
                province:
                  type: string
      title: OrganizationAvailableUnit
      description: A marketable unit anywhere in the portfolio.
    OrganizationAvailabilityResponse:
      type: object
      title: OrganizationAvailabilityResponse
      description: 'The `listOrganizationAvailability` envelope.'
      required:
        - success
        - count
        - units
      properties:
        success:
          type: boolean
        count:
          type: integer
        units:
          type: array
          items:
            '$ref': '#/components/schemas/OrganizationAvailableUnit'
    OrganizationRosterUnit:
      type: object
      title: OrganizationRosterUnit
      description: 'Every active unit, with a computed availability status and lease dates.'
      required:
        - id
        - unit_number
        - status
        - availability_status
      properties:
        id:
          type: string
          format: uuid
        unit_number:
          type: string
        status:
          type: string
          enum:
            - vacant
            - occupied
            - maintenance
            - reserved
            - leased
          description: 'Raw status. Reading this alongside `availability_status` is the robust approach.'
        classification:
          type: string
          enum:
            - residential
            - commercial
        monthly_rent:
          type:
            - number
            - 'null'
          description: 'Market rent, else current rent, else null.'
        square_feet:
          type:
            - integer
            - 'null'
        bedrooms:
          type:
            - integer
            - 'null'
        bathrooms:
          type:
            - number
            - 'null'
        floor:
          type:
            - integer
            - 'null'
        unit_type:
          type:
            - string
            - 'null'
        features:
          type: array
          items:
            type: string
        is_affordable_housing:
          type: boolean
        availability_status:
          type: string
          enum:
            - vacant_available
            - vacant_leased
            - occupied_available
            - occupied_leased
            - unavailable
          description: '`unavailable` covers raw status `maintenance` or `reserved` and was added 2026-08-04; add a case for it if you switch on this field.'
        lease_end_date:
          type:
            - string
            - 'null'
          format: date
          description: 'End of the current active lease. Always present as a key, unlike on the availability endpoint.'
        future_lease_start_date:
          type:
            - string
            - 'null'
          format: date
          description: 'When the next tenant takes possession, if a successor lease exists.'
        transfer_intent_date:
          type:
            - string
            - 'null'
          format: date
          description: Set when the current lease is flagged for transfer and no successor lease exists yet. Such units are usually absent from the availability endpoint.
        primary_photo_url:
          type:
            - string
            - 'null'
        photos:
          type: array
          items:
            '$ref': '#/components/schemas/UnitPhoto'
        floor_plan_url:
          type:
            - string
            - 'null'
        floor_area_plan_url:
          type:
            - string
            - 'null'
        unit_fact_sheet_url:
          type:
            - string
            - 'null'
        property:
          type: object
          description: 'Richer than the availability variant: includes coordinates and amenities.'
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            address:
              type: string
            city:
              type: string
            province:
              type: string
            postal_code:
              type: string
            latitude:
              type:
                - number
                - 'null'
            longitude:
              type:
                - number
                - 'null'
            amenities:
              type: array
              items:
                type: string
    OrganizationUnitsResponse:
      type: object
      title: OrganizationUnitsResponse
      description: 'The `listOrganizationUnits` envelope. No `success` field — check the status code.'
      required:
        - units
      properties:
        units:
          type: array
          items:
            '$ref': '#/components/schemas/OrganizationRosterUnit'
        count:
          type: integer
    AgencyAvailabilityResponse:
      type: object
      title: AgencyAvailabilityResponse
      description: 'The `listAgencyAvailability` snapshot: available units, unavailable ids with reasons, and counts.'
      required:
        - available
        - unavailable
        - meta
      properties:
        available:
          type: array
          description: Units the agency may list.
          items:
            '$ref': '#/components/schemas/AgencyUnit'
        unavailable:
          type: array
          description: 'Units that exist but cannot be listed, with a reason.'
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              reason:
                type: string
                description: 'Why the unit cannot be listed, e.g. `leased`.'
        meta:
          type: object
          description: Counts and the time the snapshot was taken.
          properties:
            available_count:
              type: integer
            unavailable_count:
              type: integer
            properties_count:
              type: integer
            pulled_at:
              type: string
              format: date-time
    AgencyUnit:
      type: object
      title: AgencyUnit
      description: 'The agency variant flattens the property onto the unit and names rent `market_rent`.'
      required:
        - id
        - unit_number
        - property_id
      properties:
        id:
          type: string
          format: uuid
        unit_number:
          type: string
        floor:
          type:
            - integer
            - 'null'
        unit_type:
          type:
            - string
            - 'null'
        classification:
          type: string
          enum:
            - residential
            - commercial
        bedrooms:
          type:
            - integer
            - 'null'
        bathrooms:
          type:
            - number
            - 'null'
        square_feet:
          type:
            - integer
            - 'null'
        market_rent:
          type:
            - number
            - 'null'
        features:
          type:
            - object
            - 'null'
          description: 'Feature flags keyed by name, e.g. `{ "dishwasher": true }`.'
          additionalProperties: true
        parking_spots:
          type:
            - array
            - object
            - 'null'
        storage_units:
          type:
            - array
            - object
            - 'null'
        primary_photo_url:
          type:
            - string
            - 'null'
        photos:
          type: array
          items:
            type: string
            format: uri
        floor_plan_url:
          type:
            - string
            - 'null'
        floor_area_plan_url:
          type:
            - string
            - 'null'
        unit_fact_sheet_url:
          type:
            - string
            - 'null'
        is_affordable_housing:
          type: boolean
        availability_status:
          type: string
        availability_reason:
          type: string
        available_date:
          type:
            - string
            - 'null'
          format: date
        property_id:
          type: string
          format: uuid
        property_name:
          type: string
        property_address:
          type: string
        property_latitude:
          type:
            - number
            - 'null'
        property_longitude:
          type:
            - number
            - 'null'
        promotions:
          type: array
          items:
            '$ref': '#/components/schemas/Promotion'
    PricingPlan:
      type: object
      title: PricingPlan
      description: One published subscription plan as shown on the pricing section.
      properties:
        id:
          type: string
        key:
          type: string
          description: 'Stable plan identifier, e.g. `starter`, `enterprise`.'
        name:
          type: string
          description: Display name.
        price:
          type: string
          description: 'Formatted price, or `Custom` for a quoted plan.'
        period:
          type: string
          description: '`/month`, or empty for a quoted plan.'
        desc:
          type: string
        features:
          type: array
          items:
            type: string
        cta:
          type: string
          description: Call to action shown on the pricing card.
        badge:
          type:
            - object
            - 'null'
          properties:
            text:
              type: string
            type:
              type: string
        tier:
          type:
            - string
            - 'null'
        perUnitPrice:
          type:
            - integer
            - 'null'
          description: Per-unit surcharge in cents.
        includesUnits:
          type:
            - integer
            - 'null'
          description: Units included before the surcharge.
    PricingResponse:
      type: object
      title: PricingResponse
      description: 'The `listPublicPricingPlans` envelope: published plans in display order.'
      required:
        - plans
      properties:
        plans:
          type: array
          items:
            '$ref': '#/components/schemas/PricingPlan'
        cached_at:
          type: string
          format: date-time
