{
  "openapi": "3.1.0",
  "info": {
    "title": "Plotted API",
    "version": "1.0.0",
    "summary": "150M+ U.S. property records — owners, contacts, dwelling attributes, spatial.",
    "description": "REST + JSON API for U.S. property and homeowner intelligence. Bearer-token auth. Designed to be consumed as a tool by AI agents (Claude, ChatGPT, Gemini, Cursor) — every endpoint returns structured JSON suitable for direct injection into tool-use responses.",
    "contact": {
      "name": "Plotted",
      "url": "https://plotted.to",
      "email": "hello@plotted.to"
    },
    "license": {
      "name": "Commercial",
      "url": "https://plotted.to/terms"
    }
  },
  "servers": [
    { "url": "https://api.plotted.to", "description": "Production (custom domain)" },
    { "url": "https://plotted-f8b8b.web.app", "description": "Current live host until api.plotted.to DNS is connected" }
  ],
  "security": [{ "bearerAuth": [] }],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API key starts with plt_live_ or plt_test_"
      }
    },
    "schemas": {
      "Parcel": {
        "type": "object",
        "description": "Canonical 47-column parcel record. All fields nullable except marked.",
        "properties": {
          "row_id":          { "type": "integer", "description": "Stable primary key. Survives rebuilds." },
          "source":          { "type": "string", "description": "Origin: county layer name or leadpilot_app_homeowner" },
          "state":           { "type": "string", "minLength": 2, "maxLength": 2 },
          "county":          { "type": "string" },
          "parcel_id":       { "type": "string", "description": "Original cadastral identifier" },
          "owner_name":      { "type": "string", "description": "Raw owner string as published" },
          "owner1":          { "type": "string" }, "owner1_type": { "type": "string", "enum": ["person","entity","trust","gov"] },
          "owner2":          { "type": "string" }, "owner2_type": { "type": "string", "enum": ["person","entity","trust","gov"] },
          "owner3":          { "type": "string" }, "owner3_type": { "type": "string", "enum": ["person","entity","trust","gov"] },
          "owner4":          { "type": "string" }, "owner4_type": { "type": "string", "enum": ["person","entity","trust","gov"] },
          "owner5":          { "type": "string" }, "owner5_type": { "type": "string", "enum": ["person","entity","trust","gov"] },
          "owner_count":     { "type": "integer", "minimum": 0, "maximum": 5 },
          "is_trust":        { "type": "boolean" },
          "is_homeowner":    { "type": "boolean" },
          "fiduciary_name":  { "type": "string", "description": "Florida NAL fields" },
          "fiduciary_code":  { "type": "string" },
          "mail_address":    { "type": "string" },
          "mail_city":       { "type": "string" },
          "mail_state":      { "type": "string" },
          "mail_zip":        { "type": "string", "pattern": "^\\d{5}$" },
          "mail_zip4":       { "type": "string", "pattern": "^\\d{4}$" },
          "situs_address":   { "type": "string" },
          "situs_city":      { "type": "string" },
          "situs_zip":       { "type": "string", "pattern": "^\\d{5}$" },
          "land_use_code":   { "type": "string" },
          "land_use_desc":   { "type": "string" },
          "year_built":      { "type": "string" },
          "bldg_sqft":       { "type": "number" },
          "res_units":       { "type": "number" },
          "bedrooms":        { "type": "number" },
          "baths_full":      { "type": "number" },
          "baths_half":      { "type": "number" },
          "assessed_value":  { "type": "number" },
          "sale_price":      { "type": "number" },
          "sale_date":       { "type": "string" },
          "acres":           { "type": "number" },
          "owner_email":     { "type": "string", "format": "email" },
          "owner_email_2":   { "type": "string", "format": "email" },
          "owner_phone":     { "type": "string" },
          "owner_phone_2":   { "type": "string" },
          "contact_match_level":  { "type": "string", "enum": ["owner","occupant"] },
          "contact_confidence":   { "type": "number", "minimum": 0, "maximum": 1 },
          "lat":             { "type": "number" },
          "lon":             { "type": "number" },
          "geo_precision":   { "type": "string", "enum": ["parcel","address","zip"] },
          "distance_mi":     { "type": "number", "description": "Only in spatial responses" }
        },
        "required": ["row_id", "state"]
      },
      "PageMeta": {
        "type": "object",
        "properties": {
          "limit":       { "type": "integer" },
          "next_cursor": { "type": "string", "description": "Pass back as ?cursor= for next page" }
        }
      },
      "CallMeta": {
        "type": "object",
        "properties": {
          "credits_used":       { "type": "integer" },
          "credits_remaining":  { "type": "integer" },
          "latency_ms":         { "type": "integer" },
          "request_id":         { "type": "string" }
        }
      },
      "ListResponse": {
        "type": "object",
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/Parcel" } },
          "page": { "$ref": "#/components/schemas/PageMeta" },
          "meta": { "$ref": "#/components/schemas/CallMeta" }
        }
      },
      "MatchInput": {
        "type": "object",
        "required": ["address"],
        "properties": {
          "address": { "type": "string" },
          "city":    { "type": "string" },
          "state":   { "type": "string" },
          "zip":     { "type": "string" }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code":       { "type": "string" },
              "message":    { "type": "string" },
              "param":      { "type": "string" },
              "request_id": { "type": "string" }
            },
            "required": ["code", "message"]
          }
        }
      }
    },
    "parameters": {
      "state":       { "name": "state", "in": "query", "schema": { "type": "string" } },
      "mail_zip":    { "name": "mail_zip", "in": "query", "schema": { "type": "string" } },
      "situs_zip":   { "name": "situs_zip", "in": "query", "schema": { "type": "string" } },
      "owner":       { "name": "owner", "in": "query", "schema": { "type": "string" } },
      "is_trust":    { "name": "is_trust", "in": "query", "schema": { "type": "boolean" } },
      "is_homeowner": { "name": "is_homeowner", "in": "query", "schema": { "type": "boolean" } },
      "has_contact": { "name": "has_contact", "in": "query", "schema": { "type": "boolean" } },
      "year_min":    { "name": "year_built_min", "in": "query", "schema": { "type": "integer" } },
      "year_max":    { "name": "year_built_max", "in": "query", "schema": { "type": "integer" } },
      "limit":       { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 1000 } },
      "cursor":      { "name": "cursor", "in": "query", "schema": { "type": "string" } }
    },
    "responses": {
      "ListOK":    { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ListResponse" } } } },
      "Unauthorized": { "description": "Missing or invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NoCredits": { "description": "Insufficient credits — top up at /pricing", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "RateLimited": { "description": "Plan rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    }
  },
  "paths": {
    "/v1/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Liveness check (no auth, no credits)",
        "security": [],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object" } } } } }
      }
    },
    "/v1/parcels": {
      "get": {
        "operationId": "listParcels",
        "summary": "Filter and list parcels",
        "description": "AND-combined filters. Pagination via cursor. Costs 1 credit.",
        "parameters": [
          { "$ref": "#/components/parameters/state" },
          { "$ref": "#/components/parameters/mail_zip" },
          { "$ref": "#/components/parameters/situs_zip" },
          { "$ref": "#/components/parameters/owner" },
          { "$ref": "#/components/parameters/is_trust" },
          { "$ref": "#/components/parameters/is_homeowner" },
          { "$ref": "#/components/parameters/has_contact" },
          { "$ref": "#/components/parameters/year_min" },
          { "$ref": "#/components/parameters/year_max" },
          { "$ref": "#/components/parameters/limit" },
          { "$ref": "#/components/parameters/cursor" }
        ],
        "responses": { "200": { "$ref": "#/components/responses/ListOK" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "402": { "$ref": "#/components/responses/NoCredits" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/v1/parcels/{row_id}": {
      "get": {
        "operationId": "getParcel",
        "summary": "Fetch a single parcel by row_id",
        "parameters": [{ "name": "row_id", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Parcel" }, "meta": { "$ref": "#/components/schemas/CallMeta" } } } } } } }
      }
    },
    "/v1/owners/search": {
      "get": {
        "operationId": "searchOwners",
        "summary": "Fuzzy trigram search on owner_name + owner1..5",
        "parameters": [
          { "name": "name", "in": "query", "required": true, "schema": { "type": "string", "minLength": 3 } },
          { "$ref": "#/components/parameters/state" },
          { "name": "min_similarity", "in": "query", "schema": { "type": "number", "default": 0.4, "minimum": 0, "maximum": 1 } }
        ],
        "responses": { "200": { "$ref": "#/components/responses/ListOK" } }
      }
    },
    "/v1/geocode": {
      "get": {
        "operationId": "geocode",
        "summary": "Address → lat/lon using Plotted's own dataset (no third-party geocoder)",
        "parameters": [
          { "name": "address", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "city", "in": "query", "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/state" },
          { "name": "zip", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/v1/reverse": {
      "get": {
        "operationId": "reverseGeocode",
        "summary": "Lat/lon → closest parcel within max_dist_m",
        "parameters": [
          { "name": "lat", "in": "query", "required": true, "schema": { "type": "number" } },
          { "name": "lon", "in": "query", "required": true, "schema": { "type": "number" } },
          { "name": "max_dist_m", "in": "query", "schema": { "type": "integer", "default": 200 } }
        ],
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/v1/parcels/nearby": {
      "get": {
        "operationId": "nearbyParcels",
        "summary": "Radius search around a lat/lon. THE killer endpoint for AI agents — combine with filters.",
        "description": "All /parcels filters stack. Returns Parcel with distance_mi added, sorted ascending.",
        "parameters": [
          { "name": "lat", "in": "query", "required": true, "schema": { "type": "number" } },
          { "name": "lon", "in": "query", "required": true, "schema": { "type": "number" } },
          { "name": "radius_mi", "in": "query", "required": true, "schema": { "type": "number", "minimum": 0.01, "maximum": 50 } },
          { "$ref": "#/components/parameters/state" },
          { "$ref": "#/components/parameters/is_trust" },
          { "$ref": "#/components/parameters/is_homeowner" },
          { "$ref": "#/components/parameters/has_contact" },
          { "$ref": "#/components/parameters/owner" },
          { "$ref": "#/components/parameters/limit" }
        ],
        "responses": { "200": { "$ref": "#/components/responses/ListOK" } }
      }
    },
    "/v1/parcels/nearby_address": {
      "get": {
        "operationId": "nearbyByAddress",
        "summary": "Radius search around an address — server geocodes the input for you (2 credits + per-result)",
        "parameters": [
          { "name": "address", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "city", "in": "query", "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/state" },
          { "name": "zip", "in": "query", "schema": { "type": "string" } },
          { "name": "radius_mi", "in": "query", "required": true, "schema": { "type": "number", "minimum": 0.01, "maximum": 50 } },
          { "$ref": "#/components/parameters/is_trust" },
          { "$ref": "#/components/parameters/has_contact" },
          { "$ref": "#/components/parameters/limit" }
        ],
        "responses": { "200": { "$ref": "#/components/responses/ListOK" } }
      }
    },
    "/v1/parcels/same_street/{row_id}": {
      "get": {
        "operationId": "sameStreet",
        "summary": "All neighbors on the same suffix-tolerant street as the given parcel",
        "parameters": [{ "name": "row_id", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": { "200": { "$ref": "#/components/responses/ListOK" } }
      }
    },
    "/v1/parcels/within": {
      "post": {
        "operationId": "parcelsInPolygon",
        "summary": "All parcels inside a GeoJSON Polygon or MultiPolygon",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["geometry"],
                "properties": {
                  "geometry": { "type": "object", "description": "GeoJSON Polygon or MultiPolygon" },
                  "filters":  { "type": "object" },
                  "limit":    { "type": "integer" }
                }
              }
            }
          }
        },
        "responses": { "200": { "$ref": "#/components/responses/ListOK" } }
      }
    },
    "/v1/parcels/bbox": {
      "post": {
        "operationId": "parcelsInBbox",
        "summary": "Map-viewport queries — rectangle north/south/east/west",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["north","south","east","west"],
                "properties": {
                  "north": { "type": "number" }, "south": { "type": "number" },
                  "east":  { "type": "number" }, "west":  { "type": "number" },
                  "filters": { "type": "object" },
                  "limit":   { "type": "integer" }
                }
              }
            }
          }
        },
        "responses": { "200": { "$ref": "#/components/responses/ListOK" } }
      }
    },
    "/v1/views/absentee": {
      "get": {
        "operationId": "absenteeOwners",
        "summary": "Pre-filtered to mail_zip ≠ situs_zip — out-of-state owners",
        "parameters": [
          { "$ref": "#/components/parameters/state" },
          { "$ref": "#/components/parameters/has_contact" },
          { "$ref": "#/components/parameters/limit" }
        ],
        "responses": { "200": { "$ref": "#/components/responses/ListOK" } }
      }
    },
    "/v1/views/trust": {
      "get": {
        "operationId": "trustHoldings",
        "summary": "Pre-filtered to is_trust = true — estate-trigger leads",
        "parameters": [
          { "$ref": "#/components/parameters/state" },
          { "$ref": "#/components/parameters/limit" }
        ],
        "responses": { "200": { "$ref": "#/components/responses/ListOK" } }
      }
    },
    "/v1/views/homeowners": {
      "get": {
        "operationId": "homeowners",
        "summary": "All known homeowners (parcel-rooted + cross-referenced additions)",
        "parameters": [
          { "$ref": "#/components/parameters/state" },
          { "$ref": "#/components/parameters/limit" }
        ],
        "responses": { "200": { "$ref": "#/components/responses/ListOK" } }
      }
    },
    "/v1/match": {
      "post": {
        "operationId": "bulkMatch",
        "summary": "Send a list of addresses, get back enriched parcel records (1 credit per input)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["items"],
                "properties": {
                  "items": { "type": "array", "maxItems": 10000, "items": { "$ref": "#/components/schemas/MatchInput" } }
                }
              }
            }
          }
        },
        "responses": { "200": { "$ref": "#/components/responses/ListOK" } }
      }
    },
    "/v1/match_and_expand": {
      "post": {
        "operationId": "matchAndExpand",
        "summary": "Match each address, then return parcels within radius_mi of each match",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["items","radius_mi"],
                "properties": {
                  "items":     { "type": "array", "maxItems": 10000, "items": { "$ref": "#/components/schemas/MatchInput" } },
                  "radius_mi": { "type": "number", "minimum": 0.01, "maximum": 50 },
                  "filters":   { "type": "object" }
                }
              }
            }
          }
        },
        "responses": { "200": { "$ref": "#/components/responses/ListOK" } }
      }
    }
  }
}
