{
  "openapi": "3.0.3",
  "info": {
    "title": "Property Bot public info API",
    "description": "Unauthenticated product facts for Property Bot. Canonical URL path versioning is under /api/v1/; unversioned /api/health and /api/info remain aliases of the same GET-only surface. Markdown twins (/api/info.md, /api/health.md, /api/v1/info.md, /api/v1/health.md) return the same facts as text/markdown. Versioning and Deprecation/Sunset policy: https://property.bot/docs/versioning.md \u2014 current v1 has no sunset. This surface does not expose people, needs, rooms, or private MCP tools. Humans call or text Grahm at +1 (385) 442-9768. Authenticated matching is Bearer-gated at https://mcp.property.bot/mcp and is first-person only. Unknown /api/* paths return application/json errors shaped as {\"error\":{\"code\",\"message\",\"hint\"}} (schema ErrorResponse). Health and info are GET-only \u2014 there are no public write APIs.",
    "version": "1.3.0",
    "contact": {
      "name": "Property Bot",
      "url": "https://property.bot/contact"
    }
  },
  "servers": [
    {
      "url": "https://property.bot",
      "description": "Public static site (python site server + Cloudflare Tunnel)"
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Health check",
        "description": "Returns a simple OK payload so agents can verify the public origin is reachable. Alias of /api/v1/health. Does not inspect Postgres or MCP auth. Markdown twin: GET /api/health.md.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Service reachable",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                },
                "example": {
                  "ok": true
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                },
                "example": "# Property Bot public health API\n"
              }
            }
          },
          "400": {
            "description": "Malformed request (JSON ErrorResponse)",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "bad_request",
                    "message": "Malformed request.",
                    "hint": "Available routes: GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown /api path or missing resource (JSON ErrorResponse)",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "Unknown public API path: /api/does-not-exist",
                    "hint": "Available routes: GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info. There is no public people-search API."
                  }
                }
              }
            }
          },
          "405": {
            "description": "Non-GET method on a public API path (JSON ErrorResponse)",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "example": "GET, HEAD, OPTIONS"
                },
                "description": "Methods allowed on this path"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "method_not_allowed",
                    "message": "Method POST is not allowed on /api/health.",
                    "hint": "Public API routes are GET-only: GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info."
                  }
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error (JSON ErrorResponse)",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "internal_error",
                    "message": "Unexpected server error.",
                    "hint": "Retry later. Public routes remain GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/info": {
      "get": {
        "operationId": "getInfo",
        "summary": "Product info",
        "description": "Returns public Property Bot facts: name, description, phone, MCP URL, docs, OpenAPI, API document version, and when_to_use guidance. Alias of /api/v1/info. Does not return people or match data. Markdown twin: GET /api/info.md.",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Optional response hint. Only json is supported on this path; use /api/info.md for markdown.",
            "schema": {
              "type": "string",
              "enum": [
                "json"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Public product info",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InfoResponse"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                },
                "example": "# Property Bot public info API\n"
              }
            }
          },
          "400": {
            "description": "Malformed request (JSON ErrorResponse)",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "bad_request",
                    "message": "Malformed request.",
                    "hint": "Available routes: GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown /api path or missing resource (JSON ErrorResponse)",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "Unknown public API path: /api/does-not-exist",
                    "hint": "Available routes: GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info. There is no public people-search API."
                  }
                }
              }
            }
          },
          "405": {
            "description": "Non-GET method on a public API path (JSON ErrorResponse)",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "example": "GET, HEAD, OPTIONS"
                },
                "description": "Methods allowed on this path"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "method_not_allowed",
                    "message": "Method POST is not allowed on /api/health.",
                    "hint": "Public API routes are GET-only: GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info."
                  }
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error (JSON ErrorResponse)",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "internal_error",
                    "message": "Unexpected server error.",
                    "hint": "Retry later. Public routes remain GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/health": {
      "get": {
        "operationId": "getHealthV1",
        "summary": "Health check (v1)",
        "description": "URL-path-versioned health check. Same payload as GET /api/health. Markdown twin: GET /api/v1/health.md. v1 has no sunset.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Service reachable",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                },
                "example": {
                  "ok": true
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                },
                "example": "# Property Bot public health API (v1)\n"
              }
            }
          },
          "400": {
            "description": "Malformed request (JSON ErrorResponse)",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "bad_request",
                    "message": "Malformed request.",
                    "hint": "Available routes: GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown /api path or missing resource (JSON ErrorResponse)",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "Unknown public API path: /api/does-not-exist",
                    "hint": "Available routes: GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info. There is no public people-search API."
                  }
                }
              }
            }
          },
          "405": {
            "description": "Non-GET method on a public API path (JSON ErrorResponse)",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "example": "GET, HEAD, OPTIONS"
                },
                "description": "Methods allowed on this path"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "method_not_allowed",
                    "message": "Method POST is not allowed on /api/health.",
                    "hint": "Public API routes are GET-only: GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info."
                  }
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error (JSON ErrorResponse)",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "internal_error",
                    "message": "Unexpected server error.",
                    "hint": "Retry later. Public routes remain GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/info": {
      "get": {
        "operationId": "getInfoV1",
        "summary": "Product info (v1)",
        "description": "URL-path-versioned product info. Same payload as GET /api/info. Markdown twin: GET /api/v1/info.md. v1 has no sunset.",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Optional response hint. Only json is supported on this path; use /api/v1/info.md for markdown.",
            "schema": {
              "type": "string",
              "enum": [
                "json"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Public product info",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InfoResponse"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                },
                "example": "# Property Bot public info API (v1)\n"
              }
            }
          },
          "400": {
            "description": "Malformed request (JSON ErrorResponse)",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "bad_request",
                    "message": "Malformed request.",
                    "hint": "Available routes: GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown /api path or missing resource (JSON ErrorResponse)",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "Unknown public API path: /api/does-not-exist",
                    "hint": "Available routes: GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info. There is no public people-search API."
                  }
                }
              }
            }
          },
          "405": {
            "description": "Non-GET method on a public API path (JSON ErrorResponse)",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "example": "GET, HEAD, OPTIONS"
                },
                "description": "Methods allowed on this path"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "method_not_allowed",
                    "message": "Method POST is not allowed on /api/health.",
                    "hint": "Public API routes are GET-only: GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info."
                  }
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error (JSON ErrorResponse)",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "internal_error",
                    "message": "Unexpected server error.",
                    "hint": "Retry later. Public routes remain GET /api/health, GET /api/info, GET /api/v1/health, and GET /api/v1/info."
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "RateLimit": {
        "description": "IETF RateLimit header for the current window (quota; remaining; seconds until reset).",
        "schema": {
          "type": "string",
          "example": "600;r=600;t=60"
        }
      },
      "RateLimitPolicy": {
        "description": "IETF RateLimit-Policy for the public read budget (quota; window seconds).",
        "schema": {
          "type": "string",
          "example": "600;w=60"
        }
      }
    },
    "schemas": {
      "HealthResponse": {
        "type": "object",
        "required": [
          "ok"
        ],
        "additionalProperties": false,
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "True when the public origin answered."
          }
        }
      },
      "InfoResponse": {
        "type": "object",
        "required": [
          "name",
          "description",
          "phone",
          "mcp",
          "docs",
          "openapi",
          "version",
          "when_to_use"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "description": "Product display name."
          },
          "description": {
            "type": "string",
            "description": "Short product description."
          },
          "phone": {
            "type": "string",
            "description": "Public E.164 phone for calling or texting Grahm."
          },
          "mcp": {
            "type": "string",
            "format": "uri",
            "description": "Bearer-gated MCP endpoint URL."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "Human and agent documentation URL."
          },
          "openapi": {
            "type": "string",
            "format": "uri",
            "description": "OpenAPI document URL for this public surface."
          },
          "version": {
            "type": "string",
            "description": "Public info API document version (matches OpenAPI info.version). Prefer /api/v1/*; unversioned /api/* paths are aliases. See /docs/versioning.md \u2014 v1 has no sunset."
          },
          "when_to_use": {
            "type": "string",
            "description": "Guidance for when an agent should use Property Bot."
          }
        }
      },
      "ErrorBody": {
        "type": "object",
        "required": [
          "code",
          "message",
          "hint"
        ],
        "additionalProperties": false,
        "properties": {
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code (for example not_found, method_not_allowed, bad_request, internal_error)."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation of what failed."
          },
          "hint": {
            "type": "string",
            "description": "Resolution hint: which routes exist and what this surface does not provide."
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "additionalProperties": false,
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorBody"
          }
        }
      }
    }
  }
}
