{
  "openapi": "3.1.0",
  "info": {
    "title": "whohouse property records API",
    "version": "1.1.0",
    "description": "Property-address lookup for individual and business workspaces. Returns recorded owner or recorded buyer names, property facts, sale history, mortgage records, valuations and source photos where available. No contact enrichment, mailing addresses, raw source payloads or inferred personal-financial profiles. Recorded buyers are labeled separately and are not proof of current ownership. Formatting repair is distinct from a verified source-address match. API access requires an active paid plan. New accounts receive five one-time credits for single-address searches through their browser session; no intended-use application is required. Keys are scoped, revocable, shown once at creation and expire after their configured lifetime. Access status, key validity and workspace membership are rechecked during work; unavailable records are suppressed when results are read or exported. Source coverage and permitted uses vary.",
    "termsOfService": "https://whohouse.io/legal"
  },
  "servers": [
    {
      "url": "https://whohouse.io"
    }
  ],
  "externalDocs": {
    "description": "Developer guide",
    "url": "https://whohouse.io/developers"
  },
  "security": [
    {
      "bearerApiKey": []
    },
    {
      "sessionCookie": []
    }
  ],
  "tags": [
    {
      "name": "Normalization"
    },
    {
      "name": "Lookups"
    }
  ],
  "paths": {
    "/api/v1/normalize": {
      "post": {
        "tags": [
          "Normalization"
        ],
        "operationId": "normalizeAddresses",
        "summary": "Repair address syntax without running property lookups",
        "description": "Normalizes up to 1,000 submitted addresses without consuming lookup credits or making source requests. Keeps the original input and unit distinctions. Missing ZIP codes are not guessed. Ambiguous street/city boundaries are returned for review. Syntax normalization alone always returns verified:false. API keys require lookup:write.",
        "x-required-scopes": [
          "lookup:write"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/Origin"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "addresses"
                ],
                "properties": {
                  "addresses": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 1000,
                    "items": {
                      "description": "A US property street address, normally a string of at most 500 characters. Invalid or non-string items receive their own invalid or needs_review status."
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One normalization per input, in input order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "addresses"
                  ],
                  "properties": {
                    "addresses": {
                      "type": "array",
                      "maxItems": 1000,
                      "items": {
                        "$ref": "#/components/schemas/Normalization"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "$ref": "#/components/responses/Error402"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "413": {
            "$ref": "#/components/responses/Error413"
          },
          "415": {
            "$ref": "#/components/responses/Error415"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/api/v1/lookup": {
      "post": {
        "tags": [
          "Lookups"
        ],
        "operationId": "lookupAddress",
        "summary": "Submit one property address",
        "description": "Returns a durable asynchronous job and preserves input order. Free accounts may submit one address through their browser session. Their five lifetime credits are shared across all workspaces owned by the account, and creating a workspace does not reset them. API keys require lookup:write. The complete submission reserves quota atomically before source work begins; a request exceeding remaining credits creates no job. Only matched records with a recorded owner or recorded buyer name consume credits. Invalid, ambiguous, missing-owner, not-found, failed, and duplicate records are not charged. Distinct submitted spellings resolving to the same canonical address are charged once across the free owner-account pool, or once per paid workspace billing period. A previously unseen spelling can require an available reservation before the canonical match is known. Workers make at most three concurrent source lookups across the service. Source requests remain subject to configured service limits; cached records can remain available. Failed rows are not retried automatically. Results are retained for 30 days and at most 500 completed jobs per workspace. This endpoint requests the full record directly and is chargeable on a successful owner match; it is not a free preview. Fresh source responses are cached server-side for up to 24 hours. A cache hit avoids a provider request but does not itself waive a first reveal credit.",
        "x-required-scopes": [
          "lookup:write"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/Origin"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "address"
                ],
                "properties": {
                  "address": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "US property street address. A city or ZIP and state are needed for normalization.",
                    "examples": [
                      "100 Example St, Omaha, NE 68152"
                    ]
                  }
                }
              },
              "example": {
                "address": "100 Example St, Omaha, NE 68152"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The durable job. Poll the Location URL. A replay of a retained idempotency key returns the same job; replayed is true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            },
            "headers": {
              "Location": {
                "description": "Relative URL for polling this job.",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Suggested seconds before polling.",
                "schema": {
                  "type": "string",
                  "const": "1"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "$ref": "#/components/responses/Error402"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "409": {
            "$ref": "#/components/responses/Error409"
          },
          "413": {
            "$ref": "#/components/responses/Error413"
          },
          "415": {
            "$ref": "#/components/responses/Error415"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/api/v1/batches": {
      "post": {
        "tags": [
          "Lookups"
        ],
        "operationId": "createBatch",
        "summary": "Submit up to 1,000 property addresses",
        "description": "Returns a durable asynchronous job and preserves input order. Requires an active paid plan and an accessible workspace. API keys require lookup:write. The complete submission reserves quota atomically before source work begins; a request exceeding remaining credits creates no job. Only matched records with a recorded owner or recorded buyer name consume credits. Invalid, ambiguous, missing-owner, not-found, failed, and duplicate records are not charged. Distinct submitted spellings resolving to the same canonical address are charged once per workspace billing period. Workers make at most three concurrent source lookups across the service. Source requests remain subject to configured service limits; cached records can remain available. Failed rows are not retried automatically. Results are retained for 30 days and at most 500 completed jobs per workspace. This endpoint requests the full record directly and is chargeable on a successful owner match; it is not a free preview. Fresh source responses are cached server-side for up to 24 hours. A cache hit avoids a provider request but does not itself waive a first reveal credit.",
        "x-required-scopes": [
          "lookup:write"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/Origin"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "addresses"
                ],
                "properties": {
                  "addresses": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 1000,
                    "items": {
                      "description": "A US property street address, normally a string of at most 500 characters. Invalid or non-string items receive their own invalid or needs_review status."
                    }
                  }
                }
              },
              "example": {
                "addresses": [
                  "100 example st omaha ,ne",
                  "200 Example St Apt 2, Omaha, NE 68152"
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The durable job. Poll the Location URL. A replay of a retained idempotency key returns the same job; replayed is true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            },
            "headers": {
              "Location": {
                "description": "Relative URL for polling this job.",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Suggested seconds before polling.",
                "schema": {
                  "type": "string",
                  "const": "1"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "$ref": "#/components/responses/Error402"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "409": {
            "$ref": "#/components/responses/Error409"
          },
          "413": {
            "$ref": "#/components/responses/Error413"
          },
          "415": {
            "$ref": "#/components/responses/Error415"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      },
      "get": {
        "tags": [
          "Lookups"
        ],
        "operationId": "listBatches",
        "summary": "List recent jobs in the current workspace",
        "description": "Returns job metadata only, with the newest jobs first. Includes single-address jobs and batches. Expired jobs are excluded. Requires lookup:read for API keys.",
        "x-required-scopes": [
          "lookup:read"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 30
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recent jobs. This endpoint does not include addresses or owner names.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "jobs"
                  ],
                  "properties": {
                    "jobs": {
                      "type": "array",
                      "maxItems": 100,
                      "items": {
                        "$ref": "#/components/schemas/Job"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "$ref": "#/components/responses/Error402"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/api/v1/batches/{jobId}": {
      "get": {
        "tags": [
          "Lookups"
        ],
        "operationId": "getBatch",
        "summary": "Poll a job and read a page of results",
        "description": "Read input-ordered rows at any point in processing. Poll until job.status is completed. offset is zero-based; request another page when offset + items.length is less than pagination.total. Approval and privacy suppression are enforced on every read. A suppressed result is replaced with status:suppressed, result:null and a safe error. Requires lookup:read for API keys.",
        "x-required-scopes": [
          "lookup:read"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/JobId"
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Job, current page of rows and workspace usage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobResults"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "$ref": "#/components/responses/Error402"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "410": {
            "$ref": "#/components/responses/Error410"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/api/v1/batches/{jobId}/export.csv": {
      "get": {
        "tags": [
          "Lookups"
        ],
        "operationId": "exportBatchCsv",
        "summary": "Export all rows from a job as CSV",
        "description": "Exports all rows (up to 1,000) in input order, including any currently pending rows. Wait for completed status for a final export. Values are CSV-quoted and spreadsheet formula prefixes are neutralized. Requires an active paid plan, including browser-session exports. Privacy suppression and workspace access are rechecked. Columns: input, normalized_address, status, owner_name, owner_relationship, owner_recorded_at, parcel_id, county_fips, property_type, bedrooms, bathrooms, building_area_sqft, year_built, lot_area_acres, source, source_updated_at, retrieved_at, charged, error, source_by_field. The source columns describe the owner source, not every property field. source_by_field is a JSON object carrying field-level source names and dates; it may be empty for older retained jobs. Requires lookup:read for API keys. The sales, mortgages, valuation, photos and source_by_field columns contain JSON, preserving repeated records and dates. Personal contact enrichment and raw provider payloads are excluded.",
        "x-required-scopes": [
          "lookup:read"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/JobId"
          }
        ],
        "responses": {
          "200": {
            "description": "CSV attachment. No raw source fields or personal enrichment are included.",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "$ref": "#/components/responses/Error402"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "410": {
            "$ref": "#/components/responses/Error410"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "whohouse API key",
        "description": "Use Authorization: Bearer wh_live_.... Keys require lookup:write for normalize/submit and lookup:read for list/poll/export. A key is permanently tied to one workspace."
      },
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "__Host-property_session",
        "description": "The first-party browser's HTTPS session. Send X-Workspace-Id and same-origin Origin on mutations. External integrations should use a bearer API key."
      }
    },
    "parameters": {
      "WorkspaceId": {
        "name": "X-Workspace-Id",
        "in": "header",
        "required": false,
        "description": "Optional with a bearer key; if supplied it must match the key's workspace. Required when authenticating with the browser session.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "Origin": {
        "name": "Origin",
        "in": "header",
        "required": false,
        "description": "Required to equal the website origin for browser-session mutations. Bearer API requests do not require this header.",
        "schema": {
          "type": "string",
          "format": "uri"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Reuse the same key and exact address payload to recover a submitted job without creating a second job. Reusing a retained key with a different payload returns 409. Keys are scoped to the workspace and expire with job retention.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128,
          "pattern": "^[\\x21-\\x7e]{1,128}$"
        }
      },
      "JobId": {
        "name": "jobId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A safe, user-readable error message. Raw source transport errors are never returned."
          },
          "code": {
            "type": "string",
            "description": "A stable code when the error is handled by the workspace API. Some shared HTTP guards return only error."
          }
        }
      },
      "Normalization": {
        "type": "object",
        "required": [
          "input",
          "normalized",
          "key",
          "components",
          "status",
          "confidence",
          "verified",
          "ambiguous",
          "changes"
        ],
        "properties": {
          "input": {
            "type": [
              "string",
              "null"
            ]
          },
          "normalized": {
            "type": [
              "string",
              "null"
            ]
          },
          "key": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque address identity used for matching. Do not interpret its representation as a permanent property ID."
          },
          "components": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/AddressComponents"
              },
              {
                "type": "null"
              }
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "normalized",
              "needs_review",
              "invalid"
            ]
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Confidence in syntax parsing only. This is not a current-title or postal-deliverability guarantee."
          },
          "verified": {
            "type": "boolean",
            "description": "True only after the reported source address has passed an exact normalized street/state/locality/ZIP and unit compatibility check."
          },
          "ambiguous": {
            "type": "boolean",
            "description": "Whether the input parser needs clarification."
          },
          "changes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "formatting"
              ]
            }
          },
          "reason": {
            "type": "string"
          },
          "boundaryInferred": {
            "type": "boolean"
          },
          "matchedAddress": {
            "type": "string"
          },
          "matchConfidence": {
            "type": "number",
            "const": 1,
            "description": "An exact source-address match. This does not verify ownership or current title."
          }
        }
      },
      "AddressComponents": {
        "type": "object",
        "required": [
          "street",
          "city",
          "state",
          "zip",
          "unit"
        ],
        "properties": {
          "street": {
            "type": "string"
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "state": {
            "type": "string",
            "pattern": "^[A-Z]{2}$"
          },
          "zip": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{5}(?:-\\d{4})?$",
            "description": "Never guessed. A ZIP supplied by a matched source is shown in matchedAddress."
          },
          "unit": {
            "type": [
              "string",
              "null"
            ],
            "description": "Secondary address designator and value are preserved. Units are not merged with a building-level record."
          }
        }
      },
      "Job": {
        "type": "object",
        "required": [
          "id",
          "workspaceId",
          "status",
          "kind",
          "total",
          "completed",
          "matched",
          "charged",
          "counts",
          "createdAt",
          "completedAt",
          "expiresAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "workspaceId": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "completed"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "single",
              "batch"
            ]
          },
          "total": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000
          },
          "completed": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of rows in a terminal status, including duplicates and unsuccessful rows."
          },
          "matched": {
            "type": "integer",
            "minimum": 0,
            "description": "Rows with a matched owner name, including duplicate rows."
          },
          "charged": {
            "type": "integer",
            "minimum": 0,
            "description": "New successful canonical property matches charged by this job."
          },
          "counts": {
            "type": "object",
            "additionalProperties": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Row counts keyed by item status."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "Thirty days after creation. Capacity retention can remove the oldest completed jobs earlier after 500 completed jobs."
          },
          "replayed": {
            "type": "boolean",
            "description": "Present and true when an idempotency key replay returns a retained job."
          }
        }
      },
      "Item": {
        "type": "object",
        "required": [
          "index",
          "input",
          "normalization",
          "status",
          "duplicateOf",
          "charged",
          "result",
          "error"
        ],
        "properties": {
          "index": {
            "type": "integer",
            "minimum": 0,
            "maximum": 999
          },
          "input": {
            "type": [
              "string",
              "null"
            ]
          },
          "normalization": {
            "$ref": "#/components/schemas/Normalization"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "duplicate_pending",
              "invalid",
              "needs_review",
              "matched",
              "not_found",
              "ambiguous",
              "owner_unavailable",
              "error",
              "suppressed"
            ]
          },
          "duplicateOf": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Index of the first equivalent normalized address within this job. The duplicate receives the same terminal result without another charge."
          },
          "charged": {
            "type": "boolean"
          },
          "result": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PropertyRecord"
              },
              {
                "type": "null"
              }
            ]
          },
          "error": {
            "anyOf": [
              {
                "type": "object",
                "required": [
                  "code",
                  "message"
                ],
                "properties": {
                  "code": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "PropertyRecord": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "address",
          "owner",
          "property",
          "source",
          "match"
        ],
        "properties": {
          "address": {
            "type": "string",
            "description": "The standardized address reported by the matching source."
          },
          "owner": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Owner"
              },
              {
                "type": "null"
              }
            ]
          },
          "property": {
            "$ref": "#/components/schemas/PropertyFacts"
          },
          "source": {
            "$ref": "#/components/schemas/SourceDetails",
            "description": "Owner-source summary, or the primary source if no owner is available. It is not the provenance of every property field. Unknown county dates stay null; primary-source dates are not substituted."
          },
          "sourceByField": {
            "type": "object",
            "description": "Field-level provenance for newly projected results. Keys are address, owner (covering its name, source-reported relationship and any actual ownership-event date), and property.<fieldName>. Only present fields have entries. A mixed-source result can have different names and dates for different fields. Older retained jobs may omit this additive map.",
            "propertyNames": {
              "enum": [
                "address",
                "owner",
                "property.parcelId",
                "property.countyFips",
                "property.propertyType",
                "property.bedrooms",
                "property.bathrooms",
                "property.buildingAreaSqFt",
                "property.yearBuilt",
                "property.lotAreaAcres",
                "property.taxMarketValue",
                "property.taxAmount"
              ]
            },
            "additionalProperties": {
              "$ref": "#/components/schemas/SourceDetails"
            }
          },
          "match": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "verified",
              "method"
            ],
            "properties": {
              "verified": {
                "const": true
              },
              "method": {
                "const": "source_address"
              }
            }
          },
          "sales": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SaleRecord"
            },
            "description": "Available source sale records. A prior event may contain a date only; missing historic parties are not inferred."
          },
          "mortgages": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/MortgageRecord"
            }
          },
          "valuation": {
            "$ref": "#/components/schemas/Valuation"
          },
          "photos": {
            "type": "array",
            "maxItems": 12,
            "items": {
              "$ref": "#/components/schemas/PropertyPhoto"
            }
          },
          "ownershipStatus": {
            "type": "string",
            "enum": [
              "conflicting_records"
            ]
          }
        }
      },
      "SourceDetails": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "updatedAt",
          "retrievedAt"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "The supplying dataset's reported update date or timestamp, if known. Not a deed date or a guarantee that the individual field changed then."
          },
          "retrievedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When this supplying record or snapshot was retrieved, if known. Not an ownership-event date and not proof the underlying fact is current."
          }
        }
      },
      "Owner": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "relationship",
          "label",
          "recordedAt",
          "source"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 500,
            "description": "The source's name string, preserving source order. It is not split into inferred people. Several distinct county owner labels, including multiple fields in one record, produce an ambiguous conflicting_records item rather than silently selecting one name."
          },
          "relationship": {
            "type": "string",
            "enum": [
              "recorded_owner",
              "recorded_buyer"
            ]
          },
          "label": {
            "type": "string",
            "enum": [
              "Recorded owner",
              "Recorded buyer"
            ]
          },
          "recordedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "An actual reported sale-record date for a recorded buyer. Null for county owner labels when no ownership event date is supplied. Dataset refresh dates appear under source.updatedAt."
          },
          "source": {
            "type": "string"
          }
        }
      },
      "PropertyFacts": {
        "type": "object",
        "additionalProperties": false,
        "description": "Only available property facts are included. No mailing/contact details, seller names, borrowers, mortgages, equity, lenders, agents or personal enrichment are exposed.",
        "properties": {
          "parcelId": {
            "type": [
              "string",
              "number"
            ]
          },
          "countyFips": {
            "type": [
              "string",
              "number"
            ]
          },
          "propertyType": {
            "type": [
              "string",
              "number"
            ]
          },
          "bedrooms": {
            "type": [
              "string",
              "number"
            ]
          },
          "bathrooms": {
            "type": [
              "string",
              "number"
            ]
          },
          "buildingAreaSqFt": {
            "type": [
              "string",
              "number"
            ]
          },
          "yearBuilt": {
            "description": "Only supplied when the input record contains an actual year-built field. Never inferred from a sale or dataset date.",
            "type": [
              "string",
              "number"
            ]
          },
          "lotAreaAcres": {
            "description": "Only supplied when the input record contains lot area explicitly measured in acres. Never inferred from building area.",
            "type": [
              "string",
              "number"
            ]
          },
          "taxMarketValue": {
            "description": "Source-reported tax total market value (tax_total_market_value), not assessed or taxable value. These values can differ.",
            "type": [
              "string",
              "number"
            ]
          },
          "taxAmount": {
            "type": [
              "string",
              "number"
            ]
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": [
          "period",
          "limit",
          "used",
          "reserved",
          "remaining",
          "resetsAt"
        ],
        "properties": {
          "period": {
            "type": "string",
            "description": "Opaque quota period. Free credits use one lifetime period for the owner account across its workspaces; paid credits use a workspace billing period."
          },
          "limit": {
            "type": "integer",
            "minimum": 0
          },
          "used": {
            "type": "integer",
            "minimum": 0
          },
          "reserved": {
            "type": "integer",
            "minimum": 0,
            "description": "Credits held by queued or running work."
          },
          "remaining": {
            "type": "integer",
            "minimum": 0,
            "description": "Available credits after both successful lookups and queued or running reservations. Clamped at zero when retained historical usage exceeds the current allowance."
          },
          "scope": {
            "type": "string",
            "enum": [
              "account",
              "workspace"
            ],
            "description": "Free credits are pooled across workspaces owned by one account. Paid credits belong to the individual workspace."
          },
          "lifetime": {
            "type": "boolean",
            "description": "True for the five one-time free credits. Free credits do not renew when a workspace is created or a subscription changes."
          },
          "resetsAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Subscription period end, or null for the five lifetime free credits. There are no automatic overage charges."
          }
        }
      },
      "JobResults": {
        "type": "object",
        "required": [
          "job",
          "items",
          "pagination",
          "usage"
        ],
        "properties": {
          "job": {
            "$ref": "#/components/schemas/Job"
          },
          "items": {
            "type": "array",
            "maxItems": 1000,
            "items": {
              "$ref": "#/components/schemas/Item"
            }
          },
          "pagination": {
            "type": "object",
            "required": [
              "offset",
              "limit",
              "total"
            ],
            "properties": {
              "offset": {
                "type": "integer",
                "minimum": 0
              },
              "limit": {
                "type": "integer",
                "minimum": 1,
                "maximum": 1000
              },
              "total": {
                "type": "integer",
                "minimum": 1,
                "maximum": 1000
              }
            }
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          }
        }
      },
      "RecordSource": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "retrievedAt"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "retrievedAt": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "SaleRecord": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "source"
        ],
        "properties": {
          "recordedAt": {
            "type": "string",
            "description": "Source-reported ISO date or timestamp. Omitted when not supplied."
          },
          "buyer": {
            "type": "string",
            "maxLength": 500,
            "description": "Recorded buyer, not proof of current title."
          },
          "seller": {
            "type": "string",
            "maxLength": 500
          },
          "price": {
            "type": "number",
            "minimum": 0
          },
          "source": {
            "$ref": "#/components/schemas/RecordSource"
          }
        }
      },
      "MortgageRecord": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "recordIndex",
          "status",
          "source"
        ],
        "properties": {
          "recordIndex": {
            "type": "integer",
            "minimum": 0
          },
          "borrower": {
            "type": "string",
            "maxLength": 500,
            "description": "Source-reported borrower. This does not establish ownership."
          },
          "lender": {
            "type": "string",
            "maxLength": 500
          },
          "originalLender": {
            "type": "string",
            "maxLength": 500
          },
          "recordedAt": {
            "type": "string",
            "description": "Source-reported ISO date or timestamp. Omitted when not supplied."
          },
          "originalAmount": {
            "type": "number",
            "minimum": 0
          },
          "reportedBalance": {
            "type": "number",
            "minimum": 0,
            "description": "Source-reported balance, not a verified current debt or payoff. Check balanceAsOf."
          },
          "balanceAsOf": {
            "type": "string",
            "description": "Source-reported ISO date or timestamp. Omitted when not supplied."
          },
          "interestRate": {
            "type": "number",
            "minimum": 0
          },
          "interestRateEstimated": {
            "type": "boolean"
          },
          "termMonths": {
            "type": "number",
            "minimum": 0
          },
          "termYears": {
            "type": "number",
            "minimum": 0
          },
          "status": {
            "type": "string",
            "enum": [
              "reported_active",
              "reported_inactive",
              "unknown"
            ],
            "description": "Source flag only; not independent confirmation of an active debt or payoff."
          },
          "duplicateOf": {
            "type": "integer",
            "minimum": 0,
            "description": "Zero-based recordIndex of an earlier repeated source record. Rows are retained, not silently removed."
          },
          "source": {
            "$ref": "#/components/schemas/RecordSource"
          }
        }
      },
      "Valuation": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "source"
        ],
        "properties": {
          "estimate": {
            "type": "number",
            "minimum": 0
          },
          "low": {
            "type": "number",
            "minimum": 0
          },
          "high": {
            "type": "number",
            "minimum": 0
          },
          "estimatedAt": {
            "type": "string",
            "description": "Source-reported ISO date or timestamp. Omitted when not supplied."
          },
          "source": {
            "$ref": "#/components/schemas/RecordSource"
          }
        }
      },
      "PropertyPhoto": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "HTTPS source photo URL. Attribution remains with the image. Google Street View is served separately, never archived or included in these URLs."
          },
          "attribution": {
            "type": "string",
            "maxLength": 500
          },
          "caption": {
            "type": "string",
            "maxLength": 500
          }
        }
      }
    },
    "responses": {
      "Error400": {
        "description": "Invalid request.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error401": {
        "description": "A valid session or API key is required.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error402": {
        "description": "A paid plan or more included credits is required. No automatic overages are billed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error403": {
        "description": "Workspace access, membership, scope or request-origin requirements were not met.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error404": {
        "description": "The workspace or job is not visible to this identity, or no longer exists.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error409": {
        "description": "Idempotency conflict or another conflicting request.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error410": {
        "description": "The job's results have expired.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error413": {
        "description": "Request body exceeds the allowed size (1 MiB for lookup/batch submission, 128 KiB for normalization).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error415": {
        "description": "Use application/json for request bodies.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error429": {
        "description": "Too many requests or too many pending jobs. Retry-After may indicate a delay. A workspace can submit up to 30 jobs per minute and have at most 10 pending jobs.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error503": {
        "description": "Service, configured global queue capacity or source daily budget is unavailable. Individual source failures are represented in item statuses after job submission.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
