{
  "openapi": "3.0.3",
  "info": {
    "title": "FireDash Public API",
    "description": "FireDash Public API v3. Live fire-hydrant data — hydrant lookup, flow data, operational status, residential account data, and risk-assessment tools — served from the FireDash platform.\n\n**Endpoints are flat path functions** — `/hydrants`, `/v3/status`, `/risk`, `/residential`, `/about`, `/service` — not nested REST resources. An optional version prefix is accepted on every path (`/v3/hydrants` and `/hydrants` are the same call).\n\n**Authentication** — send your API key in the `fd-api-key` request header. Generate one from the [Developer Portal](https://developer.firedash.us/hub/) by creating a Consumer Application. `/about`, `/risk`, `/service`, and status *reads* need no key.\n\n**Envelope** — every JSON response is wrapped: `{\"response\": <result>}` on success (HTTP 200), `{\"response\": {\"error\": \"…\"}}` on failures (4xx/5xx). Some domain-level failures (e.g. an unknown hydrant id) return HTTP 200 with an `errorMessage` field inside the envelope instead.\n\n**Request formats** — `GET` endpoints read query-string parameters; `POST`/`PATCH` endpoints accept either a JSON object body or `application/x-www-form-urlencoded` fields, interchangeably.\n\n**Rate limits** — 60 requests/min per IP and 300 requests/min per API key. Over-limit responses are HTTP 429 with a `Retry-After` header.\n\nEvery response carries an `X-API-Version: v3` header.",
    "version": "3.0.0",
    "termsOfService": "https://firedash.us/legal/?content=developer",
    "contact": {
      "name": "FireDash Developer Support",
      "email": "developer@firedash.us",
      "url": "https://developer.firedash.us/"
    },
    "license": {
      "name": "FireDash API Terms of Use",
      "url": "https://firedash.us/legal/?content=developer"
    }
  },
  "servers": [
    { "url": "https://api.firedash.us", "description": "Production" },
    { "url": "https://firedash.us/api", "description": "Production (alias used by the FireDash mobile apps)" }
  ],
  "security": [
    { "ApiKeyAuth": [] }
  ],
  "tags": [
    { "name": "Hydrants",    "description": "Fire-hydrant records: coordinate-radius search with flow data, optional inspection / maintenance / note history." },
    { "name": "Status",      "description": "Per-hydrant operational status — keyless reads, keyed writes." },
    { "name": "Risk",        "description": "Risk-assessment tool data (suite catalog, weather)." },
    { "name": "Residential", "description": "FireDash Residential account data." },
    { "name": "Service",     "description": "Service metadata and liveness checks for monitors and uptime probes." }
  ],
  "paths": {
    "/hydrants": {
      "get": {
        "tags": ["Hydrants"],
        "summary": "Search hydrants (query string)",
        "description": "Returns hydrant records within `distance` feet of a `latitude`/`longitude` pair, nearest first, up to 500 records. For fhm-v2 compatibility the camelCase parameter spellings (`hydrantId`, `includeInspections`, `includeMaintenance`, `includeNotes`) are also accepted.",
        "operationId": "searchHydrantsGet",
        "parameters": [
          { "$ref": "#/components/parameters/latitude" },
          { "$ref": "#/components/parameters/longitude" },
          { "$ref": "#/components/parameters/distance" },
          { "$ref": "#/components/parameters/hydrant_id" },
          { "$ref": "#/components/parameters/include_inspections" },
          { "$ref": "#/components/parameters/include_maintenance" },
          { "$ref": "#/components/parameters/include_notes" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/HydrantsOk" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["Hydrants"],
        "summary": "Search hydrants (request body)",
        "description": "Identical to the GET form; parameters travel in the request body as JSON or form fields.",
        "operationId": "searchHydrantsPost",
        "requestBody": {
          "required": true,
          "content": {
            "application/json":                  { "schema": { "$ref": "#/components/schemas/HydrantsRequest" } },
            "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/HydrantsRequest" } }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/HydrantsOk" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v3/status": {
      "post": {
        "tags": ["Status"],
        "summary": "Read a hydrant's operational status",
        "description": "Reads the current operational status of one hydrant. **No API key required** — fhm-v2 served status reads keyless and legacy consumer apps depend on that.\n\nAn unknown `hydrant_id` returns HTTP 200 with an `errorMessage` and `status: -1`.\n\n*Why the `/v3` prefix here?* On the production host the bare `/status` path collides with the status-dashboard directory and answers POST with a 301 redirect. Use this version-prefixed form (the prefix is accepted on every endpoint) or `/status/` with a trailing slash.",
        "operationId": "readStatus",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["hydrant_id"],
                "properties": {
                  "hydrant_id": { "type": "string", "description": "The hydrant to read.", "example": "hydrant-48C154A0" }
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": ["hydrant_id"],
                "properties": {
                  "hydrant_id": { "type": "string", "description": "The hydrant to read.", "example": "hydrant-48C154A0" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/StatusOk" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "patch": {
        "tags": ["Status"],
        "summary": "Update a hydrant's operational status",
        "description": "Sets a hydrant in service (`1`) or out of service (`0`). Requires an API key. Marking a hydrant out of service notifies the owning agency's managers by email and FireDash Messenger, attributed to your consumer application.\n\nDomain failures return HTTP 200 with an `errorMessage` in the envelope: unknown hydrant, a status value other than 0/1, or a no-op write (\"already marked …\").",
        "operationId": "updateStatus",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/StatusUpdateRequest" }
            },
            "application/x-www-form-urlencoded": {
              "schema": { "$ref": "#/components/schemas/StatusUpdateRequest" }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/StatusOk" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/risk": {
      "get": {
        "tags": ["Risk"],
        "summary": "Retrieve risk-assessment data",
        "description": "Risk-assessment tool data. No API key required. `tool_id=suite` (the default) returns the Risk Assessment Suite catalog; `tool_id=weather` returns weather conditions for a `postal_code`.",
        "operationId": "riskGet",
        "security": [],
        "parameters": [
          { "name": "tool_id",     "in": "query", "schema": { "type": "string", "enum": ["suite", "weather"], "default": "suite" }, "description": "Which tool to query." },
          { "name": "postal_code", "in": "query", "schema": { "type": "string" }, "description": "Postal code — required by the weather tool." },
          { "name": "bundle_id",   "in": "query", "schema": { "type": "string" }, "description": "Calling application bundle id." },
          { "name": "platform",    "in": "query", "schema": { "type": "string" }, "description": "OS platform of the caller." }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/RiskOk" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["Risk"],
        "summary": "Retrieve risk-assessment data (request body)",
        "description": "Identical to the GET form; parameters travel in the request body as JSON or form fields.",
        "operationId": "riskPost",
        "security": [],
        "requestBody": {
          "content": {
            "application/json":                  { "schema": { "$ref": "#/components/schemas/RiskRequest" } },
            "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/RiskRequest" } }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/RiskOk" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/residential": {
      "post": {
        "tags": ["Residential"],
        "summary": "Retrieve a residential account",
        "description": "Returns a FireDash Residential user's account data — the account record (password withheld) plus the resident's device, hydrant, report, referral, and survey collections. Returns `{\"response\": null}` when no account matches.",
        "operationId": "residentialGet",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["resident_id"],
                "properties": {
                  "resident_id": { "type": "string", "format": "email", "description": "The resident's email address." }
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": ["resident_id"],
                "properties": {
                  "resident_id": { "type": "string", "format": "email", "description": "The resident's email address." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Residential account data, or `null` when the resident does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "response": {
                      "type": "object",
                      "nullable": true,
                      "description": "Account record plus `devices`, `hydrants`, `reports`, `referrals`, and `surveys` collections.",
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/about": {
      "get": {
        "tags": ["Service"],
        "summary": "API and application information",
        "description": "Service metadata. No API key required.",
        "operationId": "about",
        "security": [],
        "responses": {
          "200": {
            "description": "Service metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "response": {
                      "type": "object",
                      "properties": {
                        "application_name":   { "type": "string", "example": "Fire Hydrant Manager" },
                        "pproduct_owner":     { "type": "string", "example": "Wrights Creative Services, L.L.C.", "description": "Product owner. Key name documented exactly as the live API returns it." },
                        "product_name":       { "type": "string", "example": "FireDash" },
                        "public_api_version": { "type": "string", "example": "v3" }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/service": {
      "get": {
        "tags": ["Service"],
        "summary": "Liveness probe",
        "description": "Returns the plain-text body `1` when the API is up. Used by monitors and uptime probes. No API key required, no JSON envelope.",
        "operationId": "service",
        "security": [],
        "responses": {
          "200": {
            "description": "Service is healthy.",
            "content": { "text/plain": { "schema": { "type": "string", "example": "1" } } }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "fd-api-key",
        "description": "Mint a key from the [Developer Portal](https://developer.firedash.us/hub/?page=apps). Development keys are free; production keys require an active subscription (HTTP 402 otherwise)."
      }
    },
    "parameters": {
      "latitude": {
        "name": "latitude",
        "in": "query",
        "schema": { "type": "number", "format": "double" },
        "example": 32.546471,
        "description": "Center-point latitude for a radius search."
      },
      "longitude": {
        "name": "longitude",
        "in": "query",
        "schema": { "type": "number", "format": "double" },
        "example": -94.371426,
        "description": "Center-point longitude for a radius search."
      },
      "distance": {
        "name": "distance",
        "in": "query",
        "schema": { "type": "integer", "default": 2640, "maximum": 2640 },
        "example": 2640,
        "description": "Search radius in **feet** around `latitude`/`longitude`. Defaults to — and is capped server-side at — 2,640 ft (half a mile); larger values are clamped."
      },
      "hydrant_id": {
        "name": "hydrant_id",
        "in": "query",
        "schema": { "type": "string" },
        "example": "hydrant-48C154A0",
        "description": "Restrict the result set to a single hydrant id (`hydrant-XXXXXXXX`). **Currently must be combined with a `latitude`/`longitude` pair** — an id-only call returns an empty `hydrants` array."
      },
      "include_inspections": {
        "name": "include_inspections",
        "in": "query",
        "schema": { "type": "string" },
        "example": "1",
        "description": "Attach each hydrant's inspection records as an `inspections` array. Truthiness is loose: **any non-empty value (including `false`) enables the flag** — send `1` to enable, omit the parameter to disable."
      },
      "include_maintenance": {
        "name": "include_maintenance",
        "in": "query",
        "schema": { "type": "string" },
        "example": "1",
        "description": "Attach maintenance records as a `maintenance_records` array. Same loose truthiness as `include_inspections`."
      },
      "include_notes": {
        "name": "include_notes",
        "in": "query",
        "schema": { "type": "string" },
        "example": "1",
        "description": "Attach notes as a `notes` array. Same loose truthiness as `include_inspections`."
      }
    },
    "responses": {
      "HydrantsOk": {
        "description": "Matching hydrants, nearest first (up to 500 records).",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "response": { "$ref": "#/components/schemas/HydrantsResult" }
              }
            }
          }
        }
      },
      "StatusOk": {
        "description": "Status read/write result. Domain failures (unknown hydrant, invalid or unchanged status) also arrive as HTTP 200 with an `errorMessage`.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "response": { "$ref": "#/components/schemas/StatusResult" }
              }
            },
            "examples": {
              "read": {
                "summary": "Successful read",
                "value": { "response": { "hydrantId": "hydrant-48C154A0", "statusString": "In Service", "status": "1" } }
              },
              "notFound": {
                "summary": "Unknown hydrant id",
                "value": { "response": { "hydrantId": "hydrant-FFFFFFFF", "errorMessage": "Fire Hydrant with ID (hydrant-FFFFFFFF) Not Found.", "status": -1 } }
              }
            }
          }
        }
      },
      "RiskOk": {
        "description": "Tool data; the shape varies by `tool_id`.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "response": {
                  "type": "object",
                  "description": "`suite`: the Risk Assessment Suite catalog (title, description, tool listings). `weather`: current conditions for the supplied postal code.",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, invalid, or suspended `fd-api-key`.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "response": { "error": "A valid FireDash API Key was not provided." } }
          }
        }
      },
      "PaymentRequired": {
        "description": "Production API key without an active subscription.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "response": { "error": "Production API access requires an active subscription. Subscribe at https://developer.firedash.us/hub/?page=subscribe" } }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded — 60 requests/min per IP, 300 requests/min per API key.",
        "headers": {
          "Retry-After": {
            "schema": { "type": "integer" },
            "description": "Seconds until the current window resets."
          }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "response": { "error": "Rate limit exceeded. Slow down and retry shortly." } }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "response": {
            "type": "object",
            "properties": {
              "error": { "type": "string", "example": "The endpoint does not support this method." }
            }
          }
        },
        "description": "Error envelope. Also used for HTTP 405 (unknown endpoint or unsupported method), 413 (body over 1 MB), and 501 (no function supplied)."
      },
      "HydrantsRequest": {
        "type": "object",
        "properties": {
          "latitude":            { "type": "number",  "format": "double", "example": 32.546471, "description": "Center-point latitude." },
          "longitude":           { "type": "number",  "format": "double", "example": -94.371426, "description": "Center-point longitude." },
          "distance":            { "type": "integer", "default": 2640, "maximum": 2640, "example": 2640, "description": "Radius in **feet**; capped server-side at 2,640 ft (half a mile)." },
          "hydrant_id":          { "type": "string",  "example": "hydrant-48C154A0", "description": "Single-hydrant filter — currently must be combined with `latitude`/`longitude`." },
          "include_inspections": { "type": "string",  "example": "1", "description": "Send `1` to attach inspection records; omit to disable (any non-empty value enables)." },
          "include_maintenance": { "type": "string",  "example": "1", "description": "Send `1` to attach maintenance records; omit to disable." },
          "include_notes":       { "type": "string",  "example": "1", "description": "Send `1` to attach notes; omit to disable." }
        }
      },
      "StatusUpdateRequest": {
        "type": "object",
        "required": ["hydrant_id", "status"],
        "properties": {
          "hydrant_id": { "type": "string", "example": "hydrant-48C154A0", "description": "The hydrant to update." },
          "status":     { "type": "integer", "enum": [0, 1], "description": "1 = in service, 0 = out of service. `-1` (unknown) cannot be written through the public API." }
        }
      },
      "StatusResult": {
        "type": "object",
        "properties": {
          "hydrantId":    { "type": "string", "example": "hydrant-48C154A0" },
          "status":       { "description": "Stored status — `\"1\"` in service, `\"0\"` out of service (strings on reads); `-1` (integer) when the hydrant was not found.", "example": "1" },
          "statusString": { "type": "string", "example": "In Service", "description": "Human-readable status label. Present on success." },
          "errorMessage": { "type": "string", "description": "Present instead of `statusString` on domain failures (unknown id, invalid or unchanged status)." }
        }
      },
      "HydrantsResult": {
        "type": "object",
        "properties": {
          "distance":      { "description": "Echo of the applied search radius (after the 2,640 ft cap). Present only when a `distance` parameter was sent.", "example": "2640" },
          "distance_unit": { "type": "string", "example": "ft", "description": "Always feet. Present only alongside `distance`." },
          "hydrants": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Hydrant" }
          }
        },
        "required": ["hydrants"]
      },
      "Hydrant": {
        "type": "object",
        "description": "A fire-hydrant record. The field set mirrors the stored row (frozen fhm-v2 `SELECT *` contract — partner apps decode it strictly), plus the computed fields `distance_in_feet`, `icon`, `address`, and `serviceString`. Most stored values are nullable strings.",
        "additionalProperties": true,
        "properties": {
          "id":               { "type": "string", "example": "119", "description": "Internal row id." },
          "agencyId":         { "type": "string", "example": "FIRE_STATION_01", "description": "Owning fire department / agency." },
          "stationId":        { "type": "string", "example": "FIRE_STATION_01", "description": "Legacy spelling of `agencyId`; both keys are returned with the same value." },
          "hydrantId":        { "type": "string", "example": "hydrant-48C154A0", "description": "Unique hydrant identifier." },
          "locationId":       { "type": "string", "nullable": true },
          "deviceId":         { "type": "string", "nullable": true },
          "userId":           { "type": "string", "nullable": true },
          "residentId":       { "type": "string", "nullable": true },
          "barcode":          { "type": "string", "nullable": true },
          "comments":         { "type": "string", "nullable": true },
          "gpm":              { "type": "string", "example": "499", "description": "Rated flow in gallons per minute." },
          "pressure_flow":    { "type": "string", "example": "10",  "description": "Residual (flow) pressure, PSI." },
          "pressure_static":  { "type": "string", "example": "0",   "description": "Static pressure, PSI." },
          "status":           { "type": "string", "enum": ["-1", "0", "1"], "example": "1", "description": "-1 unknown, 0 out of service, 1 in service." },
          "tags":             { "type": "string", "nullable": true },
          "type":             { "type": "string", "nullable": true, "example": "Dry Barrel" },
          "manufacturer":     { "type": "string", "nullable": true },
          "ownership":        { "type": "string", "nullable": true },
          "size_inch":        { "type": "string", "nullable": true, "example": "2.5" },
          "water_main_size":  { "type": "string", "example": "6" },
          "water_source":     { "type": "string", "nullable": true },
          "latitude":         { "type": "string", "example": "32.546471" },
          "longitude":        { "type": "string", "example": "-94.371426" },
          "neighborhood":     { "type": "string", "nullable": true },
          "county":           { "type": "string", "nullable": true },
          "street1":          { "type": "string", "nullable": true, "example": "W Rusk St" },
          "street2":          { "type": "string", "nullable": true },
          "city":             { "type": "string", "nullable": true, "example": "Marshall" },
          "state":            { "type": "string", "nullable": true, "example": "Texas" },
          "zip":              { "type": "string", "nullable": true, "example": "75670" },
          "country":          { "type": "string", "nullable": true, "example": "United States" },
          "last_updated":     { "type": "string", "nullable": true, "example": "2025-05-06 06:27:37" },
          "date":             { "type": "string", "example": "2023-06-14 10:59:52", "description": "Record creation timestamp." },
          "distance_in_feet": { "type": "string", "example": "52.02853994685471", "description": "Computed great-circle distance from the query point, in feet." },
          "icon":             { "type": "string", "example": "hydrant-status-inadequate", "description": "Computed map-icon name derived from status + flow class." },
          "address":          { "type": "string", "example": "W Rusk St<br />Marshall, Texas 75670", "description": "Computed display address (HTML fragment with a <br /> separator)." },
          "serviceString":    { "type": "string", "example": "In Service", "description": "Computed human-readable status label." },
          "inspections":          { "type": "array", "items": { "type": "object", "additionalProperties": true }, "description": "Inspection history — present when `include_inspections` is sent." },
          "maintenance_records":  { "type": "array", "items": { "type": "object", "additionalProperties": true }, "description": "Maintenance history — present when `include_maintenance` is sent." },
          "notes":                { "type": "array", "items": { "type": "object", "additionalProperties": true }, "description": "Notes — present when `include_notes` is sent." }
        },
        "required": ["agencyId", "hydrantId", "status", "latitude", "longitude"]
      },
      "RiskRequest": {
        "type": "object",
        "properties": {
          "tool_id":     { "type": "string", "enum": ["suite", "weather"], "default": "suite", "description": "Which tool to query." },
          "postal_code": { "type": "string", "description": "Postal code — required by the weather tool." },
          "bundle_id":   { "type": "string", "description": "Calling application bundle id." },
          "platform":    { "type": "string", "description": "OS platform of the caller." }
        }
      }
    }
  }
}
