> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shovels.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Search Properties

> **Beta.** Query parameters, response fields, and the absence-trust surface may still change in response to how the endpoint is used in practice. Treat the shape as unstable while it is in beta. Returns properties within a geo scope (state, county, city, ZIP, ZIP+4, or address) and/or owned by named `legal_owner`s, most recently permitted first. At least one of `geo_id` and `legal_owner` is required; a `legal_owner` search needs no geo scope. Pass `cursor` from a page's `next_cursor` to page through results. Filter by `permit_tags` (presence and `-`-prefixed exclusion), `permit_status`, `permit_from`, and `property_type`. Properties with no attributed type are excluded; property attributes are available for 70.1% of properties. An absence filter (a `-`-prefixed tag) returns properties with no resolved permit of the excluded type in covered, arrived data: a permit that never linked to an address, or one that has not yet arrived within a jurisdiction's ingestion lag, is not counted against the claim. Every absence-class row carries a `trust` object stating three honesty terms — the jurisdiction's permit `coverage_tier`, its `unresolved_rate` (the share of its permits that never linked to an address), and the row's `data_horizon` — plus `horizon_basis` (whether that horizon is measured, pooled, or a state prior) and `footprint_basis` (whether suppression could resolve the row's geo); the response `trust_summary` row-weights them across the page. A presence-only search omits both. Freshness: /properties is served from a whole-table rebuild swapped in by a blue/green cutover — the rebuild is the unit of refresh, and the absence-trust metadata is rebuilt from the same data it describes. An absence claim's staleness bound is the rebuild cadence plus each jurisdiction's permit-ingestion lag: permits reach the data a median 84 days (p90 188 days) after their start_date, so a recent 'no permit since D' stays under-observed until that lag elapses. Each row's `data_horizon` is the per-row handle on that bound — the most recent start_date past which 'no X since D' is under-observed for that row. Not supported, each for one reason: `permit_to` or any upper date bound — a max-date rollup cannot bound a closed date window without dropping addresses whose latest permit postdates it (use /permits/search); 'before D' absence — a max-date cannot tell a history that also holds later permits from one that does not; closed-window absence within [D1, D2] — only 'ever' and 'since D' are expressible on max-date maps; 'has X since D and no Y since D' in one query — a mixed query binds an exclusion to 'ever', so per-predicate dates cannot be composed; and jurisdiction geo_ids as a scope.



## OpenAPI

````yaml https://api.shovels.ai/spec/v2/openapi.production.yaml get /properties/search
openapi: 3.1.0
info:
  title: The Shovels API v2
  description: >

    The Shovels REST API makes it easy for technology developers in the
    property, climate, and

    construction industries to access detailed information about building
    permits, contractors and

    construction activity. Our API is designed to be intuitive and fast. We look
    forward to seeing what

    you build with it:

    [let us
    know](https://docs.google.com/forms/d/e/1FAIpQLSfs5Z6NZyPdnRpL96bMduH95OhfFZgLz9Hkc0-Y7pukUxSLxQ/viewform)

    and we'll check it out!


    ## Key Features

    The API offers access to two primary objects: **Permits** and
    **Contractors**.

    - **Permits**: Official documents issued by city or county authorities
    required before commencing

    construction or alterations to a building.

    - **Contractors**: Skilled professionals in the building trades who
    undertake permitted

    construction projects on various properties.


    And some additional resources:

    - **Lists**: Endpoints for predefined values like tags and property types
    for query parameters.

    - **Addresses**: Endpoint for searching and resolving valid US addresses.

    - **Meta**: Endpoints for metadata about the API and the data behind it.


    ## Getting started


    To begin using the API, please contact our sales team at

    [sales@shovels.ai](mailto:sales@shovels.ai) or grab a

    [free API key](https://app.shovels.ai/create-account/).


    > **Info**: The free API key has a limited number of calls.

    If you hit the limit and need more, please reach out to
    [sales@shovels.ai](mailto:sales@shovels.ai)

    or call us at [1-800-511-7457](tel:+18005117457).


    ### Authentication

    Our API uses a straightforward header-based authentication method:

    ```

    X-API-Key: YOUR_API_KEY_HERE

    ```


    Example Request:


    ```sh

    curl -X GET "https://api.shovels.ai/v2/meta/release" \
            -H "X-API-Key: YOUR_API_KEY_HERE"
    ```


    ## API Details


    ### Quick Overview


    A few quick details about our API:


    | Type                |
    Description                                                                |

    |---------------------|----------------------------------------------------------------------------|

    | SSL only            | We require that all requests are done over
    SSL.                            |

    | UTF-8 encoding      | We use UTF-8
    everywhere.                                                   |

    | Method              | GET for all read
    calls.                                                    |

    | Date format         | All dates in the API are strings in the following
    format: YYYY-MM-DD.      |


    ### Response Types


    The API supports the following response codes:


    | Code | Description |

    |------|-------------|

    | **200 OK** | Everything worked as expected. |

    | **400 Bad Request** | There was something wrong with your request.
    Double-check your input. |

    | **401 Unauthorized** | You need to log in to access this. Make sure your
    API key is correct. |

    | **402 Payment Required** | Credit or trial limit exceeded. Response
    includes an upgrade link. |

    | **403 Forbidden** | You don't have permission to access this. |

    | **404 Not Found** | We couldn't find what you're looking for. Check the
    URL or resource ID. |

    | **422 Unprocessable Entity** | There's an issue with the data you
    sent.         Check [Error Handling](#error-handling) if you get this error.
    |

    | **429 Too Many Requests** | You're sending requests too quickly. Slow down
    and try again later. |

    | **500 Internal Server Error** | Yikes, something went wrong on our end.
    Please let us know         at [support@shovels.ai](support@shovels.ai) |


    ### Data Format


    The API returns data in JSON format, either as pages or single objects.


    Paginated responses have the following structure:

    ```json

    {
      "items": [...],
      "size": 50,
      "next_cursor": "eyJkYXR..." | null
    }

    ```

    Where objects are returned as an array in the 'items' field.


    ### Cursor-Based Pagination


    The API uses cursor-based pagination for all paginated endpoints. This
    method uses an opaque

    cursor token to maintain your position in the result set, offering better
    performance,

    consistency, and stability, especially for large datasets.


    ```json

    {
      "items": [...],
      "size": 50,
      "next_cursor": "eyJkYXR.lIjoiMjA.yMy0"
    }

    ```


    To use cursor-based pagination:

    - For the first page: Simply make a request without any pagination
    parameters (or optionally
      specify `size` to control page size)
    - For subsequent pages: Include the `next_cursor` value from the previous
    response using the
      `cursor` parameter

    Example:

    ```

    GET /v2/permits/search?size=10

    GET /v2/permits/search?size=10&cursor=eyJkYXR.lIjoiMjA.yMy0

    ```


    When there are no more results, the `next_cursor` value will be `null`.


    ### Versioning


    The current version of the API is v2, which is reflected in the endpoints
    URL structure:

    `/v2/`. We plan to evolve our API by releasing new versions to ensure
    backward compatibility while

    maintaining a steady pace of continuous improvements.


    ### Error Handling


    Proper error messages and HTTP codes are provided to help you troubleshoot
    issues effectively. Refer

    to the [Response Types](#response-types) section for an overview of HTTP
    error codes and how to

    handle them. Below we describe how to interpret HTTP 422 code.


    #### 422 Unprocessable Entity

    A 422 Unprocessable Entity response occurs when the server understands the
    request but cannot

    process it due to invalid data. This helps you identify issues with your
    input.


    The response includes:


    - **loc**: The location of the error. The first value indicates the location
    and the second
      specifies the problematic field. Common values for the first value include:
        - **body**: The error is in the request body.
        - **query**: The error is in the query parameters.
        - **path**: The error is in the URL path.
        - **header**: The error is in the request headers.
    - **msg**: A message describing the error.

    - **type**: The type of error.


    This information helps you correct your request by pinpointing the exact
    issue. Here are some

    examples:


    **Request Body Error**

    ```json

    {
      "detail": [
        {
          "loc": ["body", "first_name"],
          "msg": "Field is required",
          "type": "value_error.missing"
        }
      ]
    }

    ```


    **Query Parameter Error**

    ```json

    {
      "detail": [
        {
          "loc": ["query", "page"],
          "msg": "Page must be a positive integer",
          "type": "type_error.integer"
        }
      ]
    }

    ```

    **Path Parameter Error**

    ```json

    {
      "detail": [
        {
          "loc": ["path", "id"],
          "msg": "Invalid ID format",
          "type": "value_error.id"
        }
      ]
    }

    ```

    **Header Error**

    ```json

    {
      "detail": [
        {
          "loc": ["header", "X-API-Key"],
          "msg": "API key is missing",
          "type": "value_error.missing"
        }
      ]
    }

    ```

    These examples show how different types of errors are reported, helping you
    to diagnose and fix

    issues in your API requests.


    ## Credit Limits


    API usage is tracked using credits. Each record returned counts as one
    credit.


    ### Response Headers


    Successful JSON responses that return records include credit headers:

    - `X-Credits-Request`: Credits consumed by this request

    - `X-Credits-Limit`: Your monthly credit limit (omitted if unlimited)

    - `X-Credits-Remaining`: Credits remaining in your limit (omitted if
    unlimited)


    ### Checking Usage


    Use `GET /v2/usage` to check your current credit usage:


    ```json

    {
      "credits_used": 847293,
      "credit_limit": 1000000
    }

    ```


    ### Exceeding Limits


    When you exceed your credit limit, the API returns HTTP 402 with a
    structured `detail` object:


    ```json

    {
      "detail": {
        "error": "Monthly credit limit exceeded.",
        "limit": 1000000,
        "upgrade_url": "https://pay.shovels.ai/p/login/14k6qo1KG7MjdlSaEE"
      }
    }

    ```


    When a trial API key exceeds its call limit, HTTP 402 is returned with
    `detail` as a plain string containing the upgrade URL inline:


    ```json

    {
      "detail": "...trial limit message... Self-serve upgrade: https://pay.shovels.ai/p/login/14k6qo1KG7MjdlSaEE — ..."
    }

    ```


    Credits are calculated on a rolling 30-day window. Usage older than 30 days
    automatically falls off.
  version: 2.0.0
servers:
  - url: https://api.shovels.ai/v2
    description: Shovels API
security: []
tags:
  - name: Meta
    description: Endpoints that provide information about the API and the data.
  - name: Lists
    description: >-
      Predefined lists of values and categories, such as tags and property
      types, which can be utilized as query parameters in other API
      interactions.
  - name: Permits
    description: >-
      Official documents issued by cities or counties before construction or
      alteration of a building can begin.
  - name: Decisions
    description: >-
      Zoning and land-use decisions extracted from city council and planning
      department meeting records.
  - name: Contractors
    description: >-
      Licensed professionals who do permitted work on residential and commercial
      buildings.
  - name: Properties
    description: >-
      Addresses with their permit history, ownership and attributes, including
      absence queries — properties *without* a given kind of permit. **Beta.**
      Query parameters, response fields, and the absence-trust surface may still
      change in response to how the endpoint is used in practice. Treat the
      shape as unstable while it is in beta.
  - name: Addresses
    description: US address ID resolution, lookup and metrics endpoints.
  - name: Cities
    description: City ID resolution, lookup and metrics endpoints.
  - name: Counties
    description: County ID resolution, lookup and metrics endpoints.
  - name: Jurisdictions
    description: Jurisdiction ID resolution, lookup and metrics endpoints.
  - name: States
    description: State metrics endpoints.
  - name: Usage
    description: Credit usage tracking.
paths:
  /properties/search:
    get:
      tags:
        - Properties
        - Properties
      summary: Search Properties
      description: >-
        **Beta.** Query parameters, response fields, and the absence-trust
        surface may still change in response to how the endpoint is used in
        practice. Treat the shape as unstable while it is in beta. Returns
        properties within a geo scope (state, county, city, ZIP, ZIP+4, or
        address) and/or owned by named `legal_owner`s, most recently permitted
        first. At least one of `geo_id` and `legal_owner` is required; a
        `legal_owner` search needs no geo scope. Pass `cursor` from a page's
        `next_cursor` to page through results. Filter by `permit_tags` (presence
        and `-`-prefixed exclusion), `permit_status`, `permit_from`, and
        `property_type`. Properties with no attributed type are excluded;
        property attributes are available for 70.1% of properties. An absence
        filter (a `-`-prefixed tag) returns properties with no resolved permit
        of the excluded type in covered, arrived data: a permit that never
        linked to an address, or one that has not yet arrived within a
        jurisdiction's ingestion lag, is not counted against the claim. Every
        absence-class row carries a `trust` object stating three honesty terms —
        the jurisdiction's permit `coverage_tier`, its `unresolved_rate` (the
        share of its permits that never linked to an address), and the row's
        `data_horizon` — plus `horizon_basis` (whether that horizon is measured,
        pooled, or a state prior) and `footprint_basis` (whether suppression
        could resolve the row's geo); the response `trust_summary` row-weights
        them across the page. A presence-only search omits both. Freshness:
        /properties is served from a whole-table rebuild swapped in by a
        blue/green cutover — the rebuild is the unit of refresh, and the
        absence-trust metadata is rebuilt from the same data it describes. An
        absence claim's staleness bound is the rebuild cadence plus each
        jurisdiction's permit-ingestion lag: permits reach the data a median 84
        days (p90 188 days) after their start_date, so a recent 'no permit since
        D' stays under-observed until that lag elapses. Each row's
        `data_horizon` is the per-row handle on that bound — the most recent
        start_date past which 'no X since D' is under-observed for that row. Not
        supported, each for one reason: `permit_to` or any upper date bound — a
        max-date rollup cannot bound a closed date window without dropping
        addresses whose latest permit postdates it (use /permits/search);
        'before D' absence — a max-date cannot tell a history that also holds
        later permits from one that does not; closed-window absence within [D1,
        D2] — only 'ever' and 'since D' are expressible on max-date maps; 'has X
        since D and no Y since D' in one query — a mixed query binds an
        exclusion to 'ever', so per-predicate dates cannot be composed; and
        jurisdiction geo_ids as a scope.
      operationId: search_properties_properties_search_get
      parameters:
        - name: geo_id
          in: query
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: Geolocation ID
            description: >-
              Scope: a 2-letter state, 5-digit ZIP, ZIP+4, or a Shovels
              state/county/city/address geolocation id. Jurisdiction ids are not
              accepted. Optional when `legal_owner` is given, required
              otherwise.
          description: >-
            Scope: a 2-letter state, 5-digit ZIP, ZIP+4, or a Shovels
            state/county/city/address geolocation id. Jurisdiction ids are not
            accepted. Optional when `legal_owner` is given, required otherwise.
        - name: legal_owner
          in: query
          required: false
          schema:
            anyOf:
              - type: array
                items:
                  type: string
                maxItems: 10
              - type: 'null'
            title: Legal Owner
            description: >-
              Filter by the property's legal owner, matched on the owner's
              canonical form — case and spacing variants of one name return the
              same properties. Pass up to 10 `legal_owner` params to span
              several owners. Without a `geo_id` this searches an owner's
              properties nationwide; with one it narrows that scope to the
              owner. A name the data uses for an unidentified owner (for example
              'RECORD OWNER') is rejected.
          description: >-
            Filter by the property's legal owner, matched on the owner's
            canonical form — case and spacing variants of one name return the
            same properties. Pass up to 10 `legal_owner` params to span several
            owners. Without a `geo_id` this searches an owner's properties
            nationwide; with one it narrows that scope to the owner. A name the
            data uses for an unidentified owner (for example 'RECORD OWNER') is
            rejected.
        - name: permit_tags
          in: query
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            title: Permit Tags
            description: >-
              Filter by canonical permit tags. Repeat the key for several tags
              (e.g. `?permit_tags=roofing&permit_tags=-solar`). A positive tag
              keeps properties with that tag on some permit; a `-` prefix
              excludes the tag (properties without it). Multiple positive tags
              require each tag (address grain — a documented divergence from
              /permits/search, where they must share one permit). With
              `permit_status`, each positive tag must be matched by a permit
              that also has one of the statuses. With `permit_from`: a positive
              tag matches since the date; an exclusion means 'no such tag since
              the date' in a pure-absence query, or 'never' when combined with
              any positive filter.
          description: >-
            Filter by canonical permit tags. Repeat the key for several tags
            (e.g. `?permit_tags=roofing&permit_tags=-solar`). A positive tag
            keeps properties with that tag on some permit; a `-` prefix excludes
            the tag (properties without it). Multiple positive tags require each
            tag (address grain — a documented divergence from /permits/search,
            where they must share one permit). With `permit_status`, each
            positive tag must be matched by a permit that also has one of the
            statuses. With `permit_from`: a positive tag matches since the date;
            an exclusion means 'no such tag since the date' in a pure-absence
            query, or 'never' when combined with any positive filter.
        - name: permit_status
          in: query
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            title: Permit Status
            description: >-
              Filter by permit status; repeat the key for several (final,
              in_review, inactive, active). With positive `permit_tags` this is
              a same-permit conjunction; on its own it keeps properties with a
              permit of one of the statuses. 'unknown' is not a filterable
              status.
          description: >-
            Filter by permit status; repeat the key for several (final,
            in_review, inactive, active). With positive `permit_tags` this is a
            same-permit conjunction; on its own it keeps properties with a
            permit of one of the statuses. 'unknown' is not a filterable status.
        - name: permit_from
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date
              - type: 'null'
            title: Permit From
            description: >-
              Bind the tag/status/absence filters to this ISO date (YYYY-MM-DD):
              a positive filter matches since the date, a pure-absence exclusion
              means 'none since the date'. With no tag/status filter it keeps
              properties whose most recent permit is on or after the date.
          description: >-
            Bind the tag/status/absence filters to this ISO date (YYYY-MM-DD): a
            positive filter matches since the date, a pure-absence exclusion
            means 'none since the date'. With no tag/status filter it keeps
            properties whose most recent permit is on or after the date.
        - name: permit_to
          in: query
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: Permit To
            description: >-
              Not supported on /properties/search — use /permits/search for a
              date-window search.
          description: >-
            Not supported on /properties/search — use /permits/search for a
            date-window search.
        - name: property_type
          in: query
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            description: >-
              Filter by property type: residential, commercial, industrial,
              agricultural, vacant land, exempt, miscellaneous, office,
              recreational. Repeat the key to match any of several types (e.g.
              `?property_type=residential&property_type=commercial`). Properties
              with no attributed type are excluded; property attributes are
              available for 70.1% of properties.
            title: Property Type
          description: >-
            Filter by property type: residential, commercial, industrial,
            agricultural, vacant land, exempt, miscellaneous, office,
            recreational. Repeat the key to match any of several types (e.g.
            `?property_type=residential&property_type=commercial`). Properties
            with no attributed type are excluded; property attributes are
            available for 70.1% of properties.
        - name: permit_tags_unfinaled
          in: query
          required: false
          schema:
            anyOf:
              - items:
                  type: string
                type: array
              - type: 'null'
            title: Permit Tags Unfinaled
            description: >-
              Keep properties with an UNFINALED permit of each named canonical
              tag. Repeat the key for several (e.g.
              `?permit_tags_unfinaled=solar&permit_tags_unfinaled=roofing`).
              Permit status — not a permit's final_date — is the finaled flag: a
              final permit carries a NULL final_date 14.7% of the time, and ~41%
              of permits with a NULL final_date are in fact finaled by status,
              so a `final_date IS NULL` test over-counts unfinaled permits and
              is wrong ~41% of the time. A tag is unfinaled for an address only
              when its latest non-final permit of that tag is more recent than
              its latest final permit of that tag (equal dates count as
              finaled); this clear rule removes the false positives a plain 'any
              non-final permit of this tag' test would keep — for solar, 31.5%
              of them, though the share varies by tag (nearer 13-18% for tags
              such as roofing and hvac). A permit whose status is unknown (NULL)
              counts as neither final nor unfinaled. With `permit_from`, the
              tag's latest unfinaled permit must be on or after the date.
          description: >-
            Keep properties with an UNFINALED permit of each named canonical
            tag. Repeat the key for several (e.g.
            `?permit_tags_unfinaled=solar&permit_tags_unfinaled=roofing`).
            Permit status — not a permit's final_date — is the finaled flag: a
            final permit carries a NULL final_date 14.7% of the time, and ~41%
            of permits with a NULL final_date are in fact finaled by status, so
            a `final_date IS NULL` test over-counts unfinaled permits and is
            wrong ~41% of the time. A tag is unfinaled for an address only when
            its latest non-final permit of that tag is more recent than its
            latest final permit of that tag (equal dates count as finaled); this
            clear rule removes the false positives a plain 'any non-final permit
            of this tag' test would keep — for solar, 31.5% of them, though the
            share varies by tag (nearer 13-18% for tags such as roofing and
            hvac). A permit whose status is unknown (NULL) counts as neither
            final nor unfinaled. With `permit_from`, the tag's latest unfinaled
            permit must be on or after the date.
        - name: property_min_market_value
          in: query
          required: false
          schema:
            anyOf:
              - type: integer
              - type: 'null'
            title: Minimum Property Market Value
            description: >-
              Minimum assessed market value in integer cents (dollars x 100). A
              property with no assessed market value never matches a range
              filter (a value is available for 61.0% of properties).
          description: >-
            Minimum assessed market value in integer cents (dollars x 100). A
            property with no assessed market value never matches a range filter
            (a value is available for 61.0% of properties).
        - name: property_max_market_value
          in: query
          required: false
          schema:
            anyOf:
              - type: integer
              - type: 'null'
            title: Maximum Property Market Value
            description: >-
              Maximum assessed market value in integer cents (dollars x 100). A
              property with no assessed market value never matches a range
              filter (a value is available for 61.0% of properties).
          description: >-
            Maximum assessed market value in integer cents (dollars x 100). A
            property with no assessed market value never matches a range filter
            (a value is available for 61.0% of properties).
        - name: property_min_lot_size
          in: query
          required: false
          schema:
            anyOf:
              - type: integer
              - type: 'null'
            title: Minimum Property Lot Size
            description: >-
              Minimum lot size in square feet. A property with no lot size never
              matches a range filter (a value is available for 68.7% of
              properties).
          description: >-
            Minimum lot size in square feet. A property with no lot size never
            matches a range filter (a value is available for 68.7% of
            properties).
        - name: property_max_lot_size
          in: query
          required: false
          schema:
            anyOf:
              - type: integer
              - type: 'null'
            title: Maximum Property Lot Size
            description: >-
              Maximum lot size in square feet. A property with no lot size never
              matches a range filter (a value is available for 68.7% of
              properties).
          description: >-
            Maximum lot size in square feet. A property with no lot size never
            matches a range filter (a value is available for 68.7% of
            properties).
        - name: property_min_building_area
          in: query
          required: false
          schema:
            anyOf:
              - type: integer
              - type: 'null'
            title: Minimum Property Building Area
            description: >-
              Minimum building area in square feet. A property with no building
              area never matches a range filter (a value is available for 60.8%
              of properties).
          description: >-
            Minimum building area in square feet. A property with no building
            area never matches a range filter (a value is available for 60.8% of
            properties).
        - name: property_max_building_area
          in: query
          required: false
          schema:
            anyOf:
              - type: integer
              - type: 'null'
            title: Maximum Property Building Area
            description: >-
              Maximum building area in square feet. A property with no building
              area never matches a range filter (a value is available for 60.8%
              of properties).
          description: >-
            Maximum building area in square feet. A property with no building
            area never matches a range filter (a value is available for 60.8% of
            properties).
        - name: property_min_unit_count
          in: query
          required: false
          schema:
            anyOf:
              - type: integer
              - type: 'null'
            title: Minimum Property Unit Count
            description: >-
              Minimum unit count. A property with no unit count never matches a
              range filter (a value is available for 70.1% of properties).
          description: >-
            Minimum unit count. A property with no unit count never matches a
            range filter (a value is available for 70.1% of properties).
        - name: property_max_unit_count
          in: query
          required: false
          schema:
            anyOf:
              - type: integer
              - type: 'null'
            title: Maximum Property Unit Count
            description: >-
              Maximum unit count. A property with no unit count never matches a
              range filter (a value is available for 70.1% of properties).
          description: >-
            Maximum unit count. A property with no unit count never matches a
            range filter (a value is available for 70.1% of properties).
        - name: property_min_year_built
          in: query
          required: false
          schema:
            anyOf:
              - type: integer
              - type: 'null'
            title: Minimum Property Year Built
            description: >-
              Minimum year built. A property with no year built never matches a
              range filter (a value is available for 61.1% of properties).
          description: >-
            Minimum year built. A property with no year built never matches a
            range filter (a value is available for 61.1% of properties).
        - name: property_max_year_built
          in: query
          required: false
          schema:
            anyOf:
              - type: integer
              - type: 'null'
            title: Maximum Property Year Built
            description: >-
              Maximum year built. A property with no year built never matches a
              range filter (a value is available for 61.1% of properties).
          description: >-
            Maximum year built. A property with no year built never matches a
            range filter (a value is available for 61.1% of properties).
        - name: cursor
          in: query
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            description: Opaque keyset cursor from a previous page's next_cursor.
            title: Cursor
          description: Opaque keyset cursor from a previous page's next_cursor.
        - name: size
          in: query
          required: false
          schema:
            type: integer
            maximum: 100
            minimum: 1
            description: Page size (1-100).
            default: 50
            title: Size
          description: Page size (1-100).
        - name: include_total_count
          in: query
          required: false
          schema:
            type: boolean
            description: >-
              When true and on the first page (no cursor), include a total_count
              capped at 10,000.
            default: false
            title: Include Total Count
          description: >-
            When true and on the first page (no cursor), include a total_count
            capped at 10,000.
      responses:
        '200':
          description: A page of properties in the scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPropertiesResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security:
        - APIKeyHeader: []
components:
  schemas:
    PaginatedPropertiesResponse:
      properties:
        items:
          items:
            $ref: '#/components/schemas/PropertiesRead'
          type: array
          title: Items
          description: The list of items returned in the response following given criteria.
        size:
          type: integer
          title: Size
          description: The number of items returned in the response.
        next_cursor:
          anyOf:
            - type: string
            - type: 'null'
          title: Next Cursor
          description: The cursor for retrieving the next page of results.
        total_count:
          anyOf:
            - $ref: '#/components/schemas/TotalCount'
            - type: 'null'
          description: >-
            Total result count (capped at 10,000). Present on first-page
            responses when include_count=true. null if the count query timed
            out.
        trust_summary:
          anyOf:
            - $ref: '#/components/schemas/TrustSummary'
            - type: 'null'
          description: Row-weighted trust summary; present only on absence-class responses.
      type: object
      required:
        - items
        - size
        - next_cursor
      title: PaginatedPropertiesResponse
      description: >-
        Paginated /properties response; carries the honesty summary on absence
        pages.
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
    PropertiesRead:
      properties:
        id:
          type: string
          title: Id
          description: The base64 address handle; the property's public id.
        street_no:
          anyOf:
            - type: string
            - type: 'null'
          title: Street No
          description: Street number of the address.
        street:
          anyOf:
            - type: string
            - type: 'null'
          title: Street
          description: Street name of the address.
        city:
          anyOf:
            - type: string
            - type: 'null'
          title: City
          description: City of the address.
        city_id:
          anyOf:
            - type: string
            - type: 'null'
          title: City Id
          description: Base64 city handle.
        zip_code:
          anyOf:
            - type: string
            - type: 'null'
          title: Zip Code
          description: ZIP code of the address.
        zip_code_ext:
          anyOf:
            - type: string
            - type: 'null'
          title: Zip Code Ext
          description: ZIP+4 extension of the address.
        county:
          anyOf:
            - type: string
            - type: 'null'
          title: County
          description: County of the address.
        county_id:
          anyOf:
            - type: string
            - type: 'null'
          title: County Id
          description: Base64 county handle.
        state:
          anyOf:
            - type: string
            - type: 'null'
          title: State
          description: State of the address.
        lat:
          anyOf:
            - type: number
            - type: 'null'
          title: Lat
          description: Latitude of the address.
        long:
          anyOf:
            - type: number
            - type: 'null'
          title: Long
          description: Longitude of the address.
        permit_count:
          type: integer
          title: Permit Count
          description: Count of permits linked to the address.
          default: 0
        untagged_permit_count:
          type: integer
          title: Untagged Permit Count
          description: Count of the address's permits carrying no canonical tag.
          default: 0
        total_job_value:
          type: integer
          title: Total Job Value
          description: Sum of permit job values in integer cents (dollars x 100).
          default: 0
        contractor_count:
          type: integer
          title: Contractor Count
          description: Distinct contractors across the address's permits.
          default: 0
        tags:
          items:
            type: string
          type: array
          title: Tags
          description: Distinct canonical tags present on the address's permits.
          default: []
        tag_status_pairs:
          items:
            type: string
          type: array
          title: Tag Status Pairs
          description: Distinct tag:status pairs (status folded to 'unknown' when NULL).
          default: []
        statuses:
          items:
            type: string
          type: array
          title: Statuses
          description: Distinct permit statuses incl the 'unknown' NULL fold.
          default: []
        tag_tally:
          additionalProperties:
            type: integer
          type: object
          title: Tag Tally
          description: Map tag -> permit count.
          default: {}
        last_date_by_tag:
          additionalProperties:
            type: string
          type: object
          title: Last Date By Tag
          description: Map tag -> latest permit start_date.
          default: {}
        last_unfinaled_date_by_tag:
          additionalProperties:
            type: string
          type: object
          title: Last Unfinaled Date By Tag
          description: Map tag -> latest non-final start_date under the clear rule.
          default: {}
        last_date_by_pair:
          additionalProperties:
            type: string
          type: object
          title: Last Date By Pair
          description: Map tag:status -> latest permit start_date.
          default: {}
        last_date_by_status:
          additionalProperties:
            type: string
          type: object
          title: Last Date By Status
          description: Map status -> latest permit start_date.
          default: {}
        last_permit_date:
          anyOf:
            - type: string
              format: date
            - type: 'null'
          title: Last Permit Date
          description: >-
            Latest permit start_date across all of the address's permits; null
            when never permitted.
        apn:
          anyOf:
            - type: string
            - type: 'null'
          title: Apn
          description: Assessor parcel number.
        property_type:
          anyOf:
            - type: string
            - type: 'null'
          title: Property Type
          description: Property type.
        property_type_detail:
          anyOf:
            - type: string
            - type: 'null'
          title: Property Type Detail
          description: Property type detail.
        year_built:
          anyOf:
            - type: integer
            - type: 'null'
          title: Year Built
          description: Year the property was built.
        lot_size:
          anyOf:
            - type: integer
            - type: 'null'
          title: Lot Size
          description: Lot size of the property.
        story_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Story Count
          description: Number of stories in the property.
        unit_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Unit Count
          description: Number of units in the property.
        building_area:
          anyOf:
            - type: integer
            - type: 'null'
          title: Building Area
          description: Building area of the property.
        assess_market_value:
          anyOf:
            - type: integer
            - type: 'null'
          title: Assess Market Value
          description: Assessed market value in integer cents (dollars x 100).
        owner_type:
          anyOf:
            - type: string
            - type: 'null'
          title: Owner Type
          description: Type of property owner.
        legal_owner:
          anyOf:
            - type: string
            - type: 'null'
          title: Legal Owner
          description: Legal owner of the property.
        trust:
          anyOf:
            - $ref: '#/components/schemas/PropertyTrust'
            - type: 'null'
          description: >-
            Per-row absence-honesty surface; present only on absence-class
            responses.
      type: object
      required:
        - id
      title: PropertiesRead
      description: >-
        Response schema for one property, served by both /properties endpoints.


        The public id is the base64 address handle. Presence arrays and the maps
        default to

        present-and-empty so a never-permitted row serializes zero counts and
        empty

        collections; attributes, dates, and trust are nullable. trust is None on
        non-absence

        responses and a PropertyTrust on absence responses
        (optional-by-polarity).
    TotalCount:
      properties:
        value:
          type: integer
          minimum: 0
          title: Value
          description: The count value; capped at the probe's cap (10,000 on the wire).
        relation:
          type: string
          enum:
            - eq
            - gte
          title: Relation
          description: >-
            "eq" means value is the exact count. "gte" means the actual count is
            at least value (the cap).
      type: object
      required:
        - value
        - relation
      title: TotalCount
      description: >-
        Capped result count with Elasticsearch-style {value, relation} shape.


        When the exact count is known and within the cap, relation is "eq" and
        value is

        that exact count. When the count exceeds the cap, relation is "gte" and
        value is

        the cap the count was probed against, meaning "the actual count is at
        least

        value". The cap is COUNT_CAP for every wire-facing endpoint; internal
        guard paths

        probe against their own cap, so value carries whatever cap produced it.
    TrustSummary:
      properties:
        rows_flagged:
          type: integer
          minimum: 0
          title: Rows Flagged
          description: Number of rows on the page carrying a trust flag.
        row_weighted_unresolved_rate:
          type: number
          maximum: 1
          minimum: 0
          title: Row Weighted Unresolved Rate
          description: Row-weighted mean unresolved_rate across the page.
        expected_miss_rate:
          type: number
          maximum: 1
          minimum: 0
          title: Expected Miss Rate
          description: >-
            Estimated share of true matches absent from arrived data due to
            ingestion lag.
        suppressed_scopes:
          type: integer
          minimum: 0
          title: Suppressed Scopes
          description: Number of footprint scopes excluded from the result by suppression.
      type: object
      required:
        - rows_flagged
        - row_weighted_unresolved_rate
        - expected_miss_rate
        - suppressed_scopes
      title: TrustSummary
      description: >-
        Response-level, row-weighted absence-honesty summary for an
        absence-class page.
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
              - type: string
              - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
        input:
          title: Input
        ctx:
          type: object
          title: Context
      type: object
      required:
        - loc
        - msg
        - type
      title: ValidationError
    PropertyTrust:
      properties:
        unresolved_rate:
          type: number
          maximum: 1
          minimum: 0
          title: Unresolved Rate
          description: >-
            Share of the row jurisdiction's permits of the tag that never linked
            to an address.
        coverage_tier:
          type: string
          enum:
            - high
            - medium
            - low
          title: Coverage Tier
          description: Coverage bucket for the row's jurisdiction (90/50 cutoffs).
        data_horizon:
          type: string
          format: date
          title: Data Horizon
          description: >-
            Most recent start_date past which 'no X since D' is under-observed;
            never null.
        horizon_basis:
          type: string
          enum:
            - measured
            - pooled
            - prior
          title: Horizon Basis
          description: >-
            How data_horizon was estimated: a measured cohort, a pooled
            fallback, or the state prior.
        trust_jurisdiction_basis:
          type: string
          enum:
            - own
            - dominant
            - unknown
          title: Trust Jurisdiction Basis
          description: >-
            Whether the trust join used the row's own jurisdiction, its ZIP's
            dominant one, or none.
        trust_jurisdiction_error_bar:
          type: number
          maximum: 1
          minimum: 0
          title: Trust Jurisdiction Error Bar
          description: >-
            Measured error rate of the trust jurisdiction: the 6.13%
            ZIP-dominant estimate error on a 'dominant' basis, 0 on an 'own' or
            'unknown' basis.
          default: 0
        footprint_basis:
          type: string
          enum:
            - matched
            - unknown
          title: Footprint Basis
          description: Whether footprint suppression could resolve the row's geo scope.
        flags:
          items:
            type: string
          type: array
          title: Flags
          description: >-
            Row-grain honesty flags, e.g. since_d_beyond_horizon or
            trust_row_missing.
          default: []
      type: object
      required:
        - unresolved_rate
        - coverage_tier
        - data_horizon
        - horizon_basis
        - trust_jurisdiction_basis
        - footprint_basis
      title: PropertyTrust
      description: >-
        Per-row absence-honesty surface for one property in an absence-class
        response.


        Present and non-null on every row of an absence or mixed-exclusion
        response, absent

        on presence-only responses (optional-by-polarity, wired at the route).
        Every field

        is required: a row whose (jurisdiction, tag) trust lookup misses carries
        the

        conservative-fallback instance rather than a null object.
  securitySchemes:
    APIKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key

````