openapi: 3.0.3
info:
  title: MapWise Parcels API
  version: "2.0"
  description: >-
    Programmatic access to Florida parcel data across all 67 counties — search by
    owner, PIN, address, acreage, sales, value, and more. Every input is public
    record, sourced directly from the county property appraisers and the Florida
    Department of Revenue, with zero licensed or third-party-aggregated data.
    This API is for targeted queries and integrations, not bulk data download.
servers:
  - url: https://maps.mapwise.com/api_v2
    description: Production
security:
  - ApiKeyHeader: []
  - BearerAuth: []
tags:
  - name: Parcels
    description: Parcel search and retrieval.
paths:
  /parcels/test:
    get:
      tags: [Parcels]
      summary: Test parcel search (no API key required)
      description: >-
        Identical query surface to `/parcels`, but requires no API key and does
        not count toward billing. Results are limited to a fixed data scope for
        evaluation. Use this to explore the request/response format before
        integrating.
      security: []
      parameters:
        - $ref: '#/components/parameters/searchCounty'
        - $ref: '#/components/parameters/searchPin'
        - $ref: '#/components/parameters/searchParcelId'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
      responses:
        '200':
          description: Matching parcels within the test data scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParcelTestResponse'
        '400':
          description: Invalid parameter.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /parcels:
    get:
      tags: [Parcels]
      summary: Search parcels
      description: >-
        Full parcel search across all 67 counties. Requires an API key. Billed
        per record returned. Combine spatial, owner, value, and characteristic
        filters; paginate with `limit`/`offset`.
      parameters:
        # Output format & paging
        - $ref: '#/components/parameters/format'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/start'
        - $ref: '#/components/parameters/offset'
        # Sorting
        - name: sortBy
          in: query
          description: Field to sort by.
          schema: { type: string, enum: [owner, county, pin, acres, sale_date] }
        - name: order
          in: query
          description: Sort direction.
          schema: { type: string, enum: [ASC, DESC] }
        # County
        - $ref: '#/components/parameters/searchCounty'
        - name: searchCountyMulti
          in: query
          description: >-
            Comma-separated list of counties (each validated against the county
            list; `ALL` accepted; case- and separator-insensitive). NOTE: when
            combined with `searchCounty`, the two are AND-ed — sending both
            narrows to the intersection and disjoint sets return an empty 200.
            (Behavior tracked in api_v2#1602.)
          schema: { type: string }
          example: DUVAL,CLAY
        # Owner / mailing / identity filters
        - { name: searchOwner, in: query, description: Owner name (partial match)., schema: { type: string } }
        - { name: searchMailing, in: query, description: Owner mailing address (partial match)., schema: { type: string } }
        - $ref: '#/components/parameters/searchPin'
        - $ref: '#/components/parameters/searchParcelId'
        # Site address filters
        - { name: searchAddress, in: query, description: Site street address (partial match)., schema: { type: string } }
        - { name: searchNumber, in: query, description: Site street number., schema: { type: string } }
        - { name: searchStreet, in: query, description: Site street name., schema: { type: string } }
        - { name: searchUnit, in: query, description: Site unit., schema: { type: string } }
        - { name: searchCity, in: query, description: Site city., schema: { type: string } }
        - { name: searchZipcode, in: query, description: Site ZIP (5-digit or ZIP+4)., schema: { type: string, pattern: '^\d{5}(?:-\d{4})?$' } }
        # Owner address filters
        - { name: searchCityOwner, in: query, description: Owner-address city., schema: { type: string } }
        - { name: searchStateOwner, in: query, description: Owner-address state., schema: { type: string } }
        - { name: searchZipcodeOwner, in: query, description: Owner-address ZIP (5-digit or ZIP+4)., schema: { type: string, pattern: '^\d{5}(?:-\d{4})?$' } }
        # Acreage & sales-amount
        - { name: searchAcresMin, in: query, schema: { type: number, minimum: 0 } }
        - { name: searchAcresMax, in: query, schema: { type: number, minimum: 0 } }
        - { name: saleAmountMin, in: query, schema: { type: number, minimum: 0 } }
        - { name: saleAmountMax, in: query, schema: { type: number, minimum: 0 } }
        - { name: saleDateMin, in: query, description: 'Earliest sale date (YYYY-MM-DD).', schema: { type: string, format: date } }
        - { name: saleDateMax, in: query, description: 'Latest sale date (YYYY-MM-DD).', schema: { type: string, format: date } }
        # Square footage
        - { name: searchSqftHtdMin, in: query, schema: { type: number, minimum: 0 } }
        - { name: searchSqftHtdMax, in: query, schema: { type: number, minimum: 0 } }
        - { name: searchSqftTotMin, in: query, schema: { type: number, minimum: 0 } }
        - { name: searchSqftTotMax, in: query, schema: { type: number, minimum: 0 } }
        - { name: searchSqftAdjMin, in: query, schema: { type: number, minimum: 0 } }
        - { name: searchSqftAdjMax, in: query, schema: { type: number, minimum: 0 } }
        # Year built
        - { name: searchYearBuiltMin, in: query, schema: { type: integer, minimum: 1500, maximum: 2200 } }
        - { name: searchYearBuiltMax, in: query, schema: { type: integer, minimum: 1500, maximum: 2200 } }
        - { name: searchYearBuiltEffMin, in: query, schema: { type: integer, minimum: 1500, maximum: 2200 } }
        - { name: searchYearBuiltEffMax, in: query, schema: { type: integer, minimum: 1500, maximum: 2200 } }
        # Market value
        - { name: searchMarketValueMin, in: query, schema: { type: number, minimum: 0 } }
        - { name: searchMarketValueMax, in: query, schema: { type: number, minimum: 0 } }
        # Land use / legal
        - { name: searchLandUse, in: query, description: 'DOR land-use code(s), comma-separated.', schema: { type: string } }
        - { name: searchLandUse4, in: query, description: '4-digit land-use code(s), comma-separated.', schema: { type: string } }
        - { name: searchSubdivision, in: query, schema: { type: string } }
        - { name: searchLegal, in: query, description: Legal description (partial match)., schema: { type: string } }
        - { name: searchBook, in: query, schema: { type: string } }
        - { name: searchPage, in: query, schema: { type: string } }
        - { name: searchDocnum, in: query, schema: { type: string } }
        # Spatial
        - { name: bbox, in: query, description: 'Bounding box xmin,ymin,xmax,ymax (EPSG:4326).', schema: { type: string }, example: '-82.35,29.64,-82.30,29.67' }
        - { name: lat_lon, in: query, description: 'Point "lat,lon" for point-in-parcel search.', schema: { type: string }, example: '29.65,-82.32' }
        - { name: searchGeometry, in: query, description: WKT geometry for spatial search (GeoJSON via POST body)., schema: { type: string } }
        - { name: bufferPoint, in: query, description: Treat lat_lon as the center of a buffer., schema: { type: boolean } }
        - { name: bufferMeters, in: query, description: 'Buffer radius in METRES (1-50000; 1 mile ~ 1609).', schema: { type: integer, minimum: 1, maximum: 50000 } }
        # Aggregation & response flags
        - { name: searchAggType, in: query, description: 'Aggregation mode.', schema: { type: string, enum: [NOAGG, OWNER, OWNERSINGLE, SALE] } }
        - { name: includeGeometry, in: query, description: Include parcel geometry in the response., schema: { type: boolean } }
        - { name: includeLegal, in: query, description: Include the legal description., schema: { type: boolean } }
        - { name: includeHazards, in: query, description: Include hazard (flood) data., schema: { type: boolean } }
        - { name: includeFloodZoneFragments, in: query, description: Include per-zone flood fragments., schema: { type: boolean } }
      responses:
        '200':
          description: Matching parcels.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParcelSearchResponse'
        '400':
          description: Invalid parameter.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '429':
          description: Rate limit or spend cap exceeded.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
components:
  securitySchemes:
    ApiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
    BearerAuth:
      type: http
      scheme: bearer
  parameters:
    format:
      name: format
      in: query
      description: Response format (case-insensitive).
      schema: { type: string, enum: [JSON, HTML] }
    limit:
      name: limit
      in: query
      description: Page size (1-100).
      schema: { type: integer, minimum: 1, maximum: 100 }
    start:
      name: start
      in: query
      description: Row offset (alias of offset).
      schema: { type: integer, minimum: 0 }
    offset:
      name: offset
      in: query
      description: Row offset for pagination.
      schema: { type: integer, minimum: 0 }
    searchCounty:
      name: searchCounty
      in: query
      description: County name (case- and separator-insensitive; `ALL` for statewide).
      schema: { type: string }
      example: ALACHUA
    searchPin:
      name: searchPin
      in: query
      description: Parcel identification number (supports `*` wildcard).
      schema: { type: string }
    searchParcelId:
      name: searchParcelId
      in: query
      description: Parcel ID (supports `*` wildcard).
      schema: { type: string }
  schemas:
    ErrorEnvelope:
      type: object
      required: [success, errormsg]
      properties:
        success: { type: boolean, example: false }
        errormsg: { type: string }
        status: { type: integer }
    ParcelSearchResponse:
      type: object
      required: [success, data, meta]
      example:
        {
          "success": true,
          "data": [
            {
              "meta": {
                "ogc_fid": 1058677512,
                "county": "ALACHUA",
                "extracted_at": "20260814",
                "count": 1,
                "pa_pin_link": "https://qpublic.schneidercorp.com/Application.aspx?AppID=1081&LayerID=26490&PageTypeID=4&PageID=10770&Q=422075161&KeyValue=12057-000-000"
              },
              "identifiers": {
                "pin": "12057-000-000",
                "pin_clean": "12057000000",
                "pin2": null,
                "pin2_clean": null,
                "altkey": null,
                "prop_details": null
              },
              "owner": {
                "primary_name": "1000 SE 7TH LLC",
                "secondary_name": null,
                "tertiary_name": null,
                "address_line1": "1882 CARIBBEAN RD W",
                "address_line2": null,
                "address_line3": null,
                "city": "WEST PALM BEACH",
                "state": "FL",
                "zipcode": "33406"
              },
              "site": {
                "address": null,
                "city": null,
                "zipcode": null,
                "subdivision": "WAITES, W.H. BLKS 4&5 R 2 (ROPERS ADDN)",
                "subdivision_common": null,
                "subdivision_id": "209300",
                "condo": "NO"
              },
              "land": {
                "acres_deed": null,
                "acres_gis": 0.10391214201781951,
                "zoning": "SF",
                "land_use": {
                  "luse": "0000",
                  "luse_desc": "VACANT RESIDENTIAL",
                  "lusedor": "00",
                  "lusedor_desc": "Vacant Residential"
                }
              },
              "building": {
                "num_buildings": null,
                "stories": null,
                "sqft": {
                  "heated": null,
                  "total": null,
                  "adjusted": null
                },
                "total_sqft": null,
                "heated_sqft": null,
                "year_built_actual": null,
                "year_built_effective": null,
                "beds": null,
                "baths": {
                  "full": null,
                  "half": null
                }
              },
              "valuation": {
                "market": {
                  "building": 0,
                  "improvements": null,
                  "land": 58000,
                  "ag": 0,
                  "total": 58000
                },
                "assessed_total": 5500,
                "exempt_total": 0,
                "taxable_total": 5500
              },
              "sales": {
                "recent": {
                  "date": "2025-12-10",
                  "amount": 50000,
                  "type": "WD        ",
                  "qual": "Q",
                  "vac": "Vac",
                  "book": "5247                ",
                  "page": "1849                ",
                  "docnum": null,
                  "grantor": null
                },
                "previous": [
                  {
                    "date": "2016-08-18",
                    "amount": 3800,
                    "type": "TD        ",
                    "qual": "U",
                    "vac": "Vac",
                    "book": "4455                ",
                    "page": "0104                ",
                    "docnum": null,
                    "grantor": null
                  }
                ]
              }
            }
          ],
          "meta": {
            "record_count": 1,
            "total_count": 108938
          }
        }
      properties:
        success: { type: boolean, example: true }
        data:
          type: array
          items: { $ref: '#/components/schemas/ParcelRecord' }
        meta: { $ref: '#/components/schemas/SearchMeta' }
    ParcelTestResponse:
      type: object
      required: [success, data, meta]
      properties:
        success: { type: boolean, example: true }
        data:
          type: array
          items: { $ref: '#/components/schemas/ParcelRecord' }
        meta: { $ref: '#/components/schemas/TestMeta' }
    SearchMeta:
      type: object
      required: [record_count, total_count]
      properties:
        record_count: { type: integer, description: Records returned in this page. }
        total_count: { type: integer, description: Total matching records. }
    TestMeta:
      allOf:
        - $ref: '#/components/schemas/SearchMeta'
        - type: object
          properties:
            test_mode: { type: boolean }
            data_scope:
              type: object
              properties:
                county: { type: string }
    ParcelRecord:
      type: object
      properties:
        meta:
          type: object
          properties:
            ogc_fid: { type: integer }
            county: { type: string }
            extracted_at: { type: string, description: When this parcel was last extracted from the source. }
            count: { type: integer }
            pa_pin_link: { type: string, description: Link to the county property appraiser record. }
        identifiers:
          type: object
          properties:
            pin: { type: string }
            pin_clean: { type: string }
            pin2: { type: string, nullable: true }
            pin2_clean: { type: string, nullable: true }
            altkey: { type: string, nullable: true }
            prop_details: { type: string, nullable: true }
        owner:
          type: object
          properties:
            primary_name: { type: string }
            secondary_name: { type: string, nullable: true }
            tertiary_name: { type: string, nullable: true }
            address_line1: { type: string }
            address_line2: { type: string, nullable: true }
            address_line3: { type: string, nullable: true }
            city: { type: string }
            state: { type: string }
            zipcode: { type: string }
        site:
          type: object
          properties:
            address: { type: string, nullable: true }
            city: { type: string, nullable: true }
            zipcode: { type: string, nullable: true }
            subdivision: { type: string, nullable: true }
            subdivision_common: { type: string, nullable: true }
            subdivision_id: { type: string, nullable: true }
            condo: { type: string, nullable: true }
        land:
          type: object
          properties:
            acres_deed: { type: number, nullable: true }
            acres_gis: { type: number, nullable: true }
            zoning: { type: string, nullable: true }
            land_use:
              type: object
              properties:
                luse: { type: string, nullable: true }
                luse_desc: { type: string, nullable: true }
                lusedor: { type: string, nullable: true }
                lusedor_desc: { type: string, nullable: true }
        building:
          type: object
          properties:
            num_buildings: { type: integer, nullable: true }
            stories: { type: number, nullable: true }
            sqft:
              type: object
              properties:
                heated: { type: number, nullable: true }
                total: { type: number, nullable: true }
                adjusted: { type: number, nullable: true }
            total_sqft: { type: number, nullable: true }
            heated_sqft: { type: number, nullable: true }
            year_built_actual: { type: integer, nullable: true }
            year_built_effective: { type: integer, nullable: true }
            beds: { type: integer, nullable: true }
            baths:
              type: object
              properties:
                full: { type: integer, nullable: true }
                half: { type: integer, nullable: true }
        valuation:
          type: object
          properties:
            market:
              type: object
              properties:
                building: { type: integer, nullable: true }
                improvements: { type: integer, nullable: true }
                land: { type: integer, nullable: true }
                ag: { type: integer, nullable: true }
                total: { type: integer, nullable: true }
            assessed_total: { type: integer, nullable: true }
            exempt_total: { type: integer, nullable: true }
            taxable_total: { type: integer, nullable: true }
        sales:
          type: object
          properties:
            recent: { $ref: '#/components/schemas/Sale' }
            previous:
              type: array
              items: { $ref: '#/components/schemas/Sale' }
    Sale:
      type: object
      nullable: true
      properties:
        date: { type: string, nullable: true }
        amount: { type: integer, nullable: true }
        type: { type: string, nullable: true }
        qual: { type: string, nullable: true }
        vac: { type: string, nullable: true }
        book: { type: string, nullable: true }
        page: { type: string, nullable: true }
        docnum: { type: string, nullable: true }
        grantor: { type: string, nullable: true }
