{
  "openapi": "3.1.0",
  "info": {
    "title": "SavePropTax Agent API",
    "version": "1.0.0",
    "description": "Open endpoints for AI agents to check whether a California home qualifies for a Proposition 8 decline-in-value property tax reduction and to start a filing the owner completes themselves. The check is free and keyless. The signing link is always emailed to the owner and never returned to the calling agent; the owner personally signs and pays a flat $29 before anything is filed. Human-readable reference: https://saveproptax.com/agents",
    "contact": { "email": "support@saveproptax.com", "url": "https://saveproptax.com/agents" }
  },
  "servers": [{ "url": "https://saveproptax.com" }],
  "paths": {
    "/api/agent/check": {
      "post": {
        "operationId": "checkPropertyTaxSavings",
        "summary": "Free qualification check for a property (keyless)",
        "description": "Runs the identical valuation pipeline the SavePropTax website runs: county records, recent comparable sales, and the same qualification gates. Results are cached 24 hours per address. Optional x-agent-key header (from /api/agent/register) raises fresh-check limits and names your traffic.",
        "parameters": [{ "$ref": "#/components/parameters/AgentKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["address"],
                "properties": {
                  "address": { "type": "string", "description": "Street address including the city", "example": "123 Main St, Walnut Creek" },
                  "unit": { "type": "string", "description": "Unit number for multi-unit buildings", "example": "Unit 12" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Check result. The status field carries the outcome; message is a relayable one-sentence explanation.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CheckResult" } } }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/api/agent/prepare": {
      "post": {
        "operationId": "startFiling",
        "summary": "Prepare the county filing and email the signing link to the owner",
        "description": "Requires a continueToken from a qualifying check (valid 24 hours). Prepares the county's own review form plus the comparables worksheet, creates the e-sign document, and emails the signing link to ownerEmail. The response NEVER contains the signing link: signature and the flat $29 payment are performed by the owner. Limited to 3 prepares per owner email per day.",
        "parameters": [{ "$ref": "#/components/parameters/AgentKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["continueToken", "ownerName", "ownerPhone", "ownerEmail"],
                "properties": {
                  "continueToken": { "type": "string", "description": "From a qualifying check result" },
                  "ownerName": { "type": "string", "description": "Owner name or names exactly as on title" },
                  "ownerPhone": { "type": "string", "description": "Owner daytime phone with area code" },
                  "ownerEmail": { "type": "string", "format": "email", "description": "The signing link is delivered here" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Filing prepared and the signing link emailed to the owner.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["docId", "status", "statusUrl", "ownerEmailSent"],
                  "properties": {
                    "docId": { "type": "string", "description": "Use with /api/agent/status" },
                    "status": { "type": "string", "enum": ["awaiting_signature"] },
                    "statusUrl": { "type": "string", "format": "uri" },
                    "ownerEmailSent": { "type": "boolean" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "409": { "$ref": "#/components/responses/Error" },
          "422": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/api/agent/status": {
      "get": {
        "operationId": "getFilingStatus",
        "summary": "Coarse filing status by document id",
        "description": "Returns only a coarse state and a message. No personal information, no addresses, no amounts. Unknown or invalid ids return status unknown.",
        "parameters": [
          { "name": "doc", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The docId from /api/agent/prepare" }
        ],
        "responses": {
          "200": {
            "description": "Coarse status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["status", "message"],
                  "properties": {
                    "status": { "type": "string", "enum": ["awaiting_signature", "awaiting_payment", "filed", "delivered", "unknown"] },
                    "updatedAt": { "type": "string" },
                    "message": { "type": "string" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/agent/register": {
      "post": {
        "operationId": "registerAgent",
        "summary": "Instant API key (optional)",
        "description": "Returns a key immediately, no approval step. The key is identity for telemetry and higher limits, not permission: anonymous use works without it. Send it as the x-agent-key header on check and prepare calls.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["agentName", "contactEmail"],
                "properties": {
                  "agentName": { "type": "string", "minLength": 2, "maxLength": 60 },
                  "contactEmail": { "type": "string", "format": "email" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Your key, returned once; store it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["key", "agentName", "limits"],
                  "properties": {
                    "key": { "type": "string" },
                    "agentName": { "type": "string" },
                    "created": { "type": "string" },
                    "limits": { "type": "string", "description": "Plain-English description of what the key buys" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Error" }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "AgentKey": {
        "name": "x-agent-key",
        "in": "header",
        "required": false,
        "schema": { "type": "string" },
        "description": "Optional registered key from /api/agent/register: higher fresh-check limits and named attribution."
      }
    },
    "responses": {
      "Error": {
        "description": "Error with a relayable message.",
        "content": {
          "application/json": {
            "schema": { "type": "object", "required": ["error"], "properties": { "error": { "type": "string" } } }
          }
        }
      }
    },
    "schemas": {
      "CheckResult": {
        "type": "object",
        "required": ["status", "message", "cached", "checkedAt"],
        "properties": {
          "status": {
            "type": "string",
            "enum": ["qualifies", "fair_assessment", "not_enough_data", "not_residential", "window_closed", "county_not_served", "already_filed", "address_not_found", "needs_more_info", "busy"],
            "description": "qualifies: defensible case, savings above $500 a year, county window open, continueToken included. fair_assessment: comparable sales do not support a lower value this year. not_enough_data: too little closely matched sales evidence for a defensible filing. not_residential: parcel is not residential; we only value homes. window_closed: the county's review window is closed; filing.reopens says when it returns. county_not_served: county not in the pilot set yet. already_filed: a review was already filed for this property this year. address_not_found: the address could not be located; include the city. needs_more_info: more input needed, for example a unit number. busy: heavy load; cached answers still serve, retry fresh checks shortly."
          },
          "message": { "type": "string", "description": "One-sentence relayable explanation" },
          "property": {
            "type": "object",
            "properties": { "address": { "type": "string" }, "county": { "type": ["string", "null"] } }
          },
          "estimate": {
            "type": "object",
            "description": "Present when a valuation was computed (qualifies and fair_assessment).",
            "properties": {
              "assessment": { "type": "integer", "description": "Current roll value, USD" },
              "opinion": { "type": "integer", "description": "Our opinion of value from comparable sales, USD" },
              "estimatedAnnualSavings": { "type": "integer", "description": "Estimated annual tax savings, USD" }
            }
          },
          "filing": {
            "type": "object",
            "description": "Present when filing terms are relevant (qualifies and window_closed).",
            "properties": {
              "feeUsd": { "type": "integer", "const": 29 },
              "deadline": { "type": ["string", "null"] },
              "reopens": { "type": "string" }
            }
          },
          "continueToken": { "type": "string", "description": "Only on qualifies; pass to /api/agent/prepare within 24 hours" },
          "cached": { "type": "boolean", "description": "True when served from the 24-hour per-address cache" },
          "checkedAt": { "type": "string" }
        }
      }
    }
  }
}
