{
  "openapi": "3.0.3",
  "info": {
    "title": "Storm Stream API",
    "version": "1.0.0",
    "description": "Severe-weather event intelligence, as an API. Storm Stream tells you what actually happened on the ground: the event, not the forecast. Query verified hail and wind events by point, address, zip, or polygon; pull tracks, swaths, and GeoJSON overlays; watch zones for new storms via webhooks; and look up historical impact for any location and date."
  },
  "servers": [
    { "url": "https://stormstream.io" }
  ],
  "security": [
    { "bearerAuth": [] }
  ],
  "paths": {
    "/v1/query": {
      "get": {
        "operationId": "queryStorms",
        "summary": "Query storm events at a location",
        "description": "The primary endpoint. Provide a location as lat/lng, zip, address, or bbox, plus an optional time window, and get back the severe-weather events that hit it.",
        "parameters": [
          { "name": "lat", "in": "query", "schema": { "type": "number", "minimum": -90, "maximum": 90 }, "description": "Latitude. Use together with lng." },
          { "name": "lng", "in": "query", "schema": { "type": "number", "minimum": -180, "maximum": 180 }, "description": "Longitude. Use together with lat." },
          { "name": "zip", "in": "query", "schema": { "type": "string" }, "description": "US ZIP code. Alternative to lat/lng." },
          { "name": "address", "in": "query", "schema": { "type": "string" }, "description": "Street address to geocode. Alternative to lat/lng." },
          { "name": "bbox", "in": "query", "schema": { "type": "string" }, "description": "Bounding box as minLng,minLat,maxLng,maxLat. Alternative to a point." },
          { "name": "from", "in": "query", "schema": { "type": "integer" }, "description": "Window start, unix epoch milliseconds." },
          { "name": "to", "in": "query", "schema": { "type": "integer" }, "description": "Window end, unix epoch milliseconds." }
        ],
        "responses": {
          "200": {
            "description": "Matching storm events.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/QueryResult" }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "402": {
            "description": "Monthly spend cap reached. Raise the cap via POST /v1/keys/cap or wait for the next billing period.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpendCap" } } }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      },
      "post": {
        "operationId": "queryStormsByPolygon",
        "summary": "Query storm events inside a polygon",
        "description": "POST form of query for arbitrary polygons that do not fit in a query string.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["polygon"],
                "properties": {
                  "polygon": {
                    "type": "array",
                    "description": "Polygon ring as [lat,lng] pairs.",
                    "items": { "$ref": "#/components/schemas/LatLng" },
                    "minItems": 3
                  },
                  "window": {
                    "type": "object",
                    "description": "Optional time window.",
                    "properties": {
                      "from": { "type": "integer", "description": "Window start, unix epoch milliseconds." },
                      "to": { "type": "integer", "description": "Window end, unix epoch milliseconds." }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Matching storm events.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/QueryResult" } } }
          },
          "401": { "description": "Missing or invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "Monthly spend cap reached.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpendCap" } } } },
          "429": { "description": "Rate limit exceeded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/storms": {
      "get": {
        "operationId": "listStorms",
        "summary": "List storm events",
        "description": "Browse storm events, optionally filtered by bounding box and time window.",
        "parameters": [
          { "name": "bbox", "in": "query", "schema": { "type": "string" }, "description": "Bounding box as minLng,minLat,maxLng,maxLat." },
          { "name": "from", "in": "query", "schema": { "type": "integer" }, "description": "Window start, unix epoch milliseconds." },
          { "name": "to", "in": "query", "schema": { "type": "integer" }, "description": "Window end, unix epoch milliseconds." }
        ],
        "responses": {
          "200": {
            "description": "Storm events.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/QueryResult" } } }
          },
          "401": { "description": "Missing or invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "Monthly spend cap reached.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpendCap" } } } },
          "429": { "description": "Rate limit exceeded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/storms/{id}": {
      "get": {
        "operationId": "getStorm",
        "summary": "Get a single storm event",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Storm event id." }
        ],
        "responses": {
          "200": {
            "description": "The storm event.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "storm": { "$ref": "#/components/schemas/StormEvent" }
                  }
                }
              }
            }
          },
          "401": { "description": "Missing or invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "No storm with that id.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/storms/{id}/overlay.geojson": {
      "get": {
        "operationId": "getStormOverlay",
        "summary": "Get a storm swath as GeoJSON",
        "description": "Returns the storm swath and track as a GeoJSON FeatureCollection, ready to drop onto any map. Note that GeoJSON coordinates are [lng,lat] order, while StormEvent geometry uses [lat,lng] pairs.",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Storm event id." }
        ],
        "responses": {
          "200": {
            "description": "GeoJSON FeatureCollection for the storm.",
            "content": {
              "application/geo+json": {
                "schema": { "$ref": "#/components/schemas/GeoJSONFeatureCollection" }
              }
            }
          },
          "401": { "description": "Missing or invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "No storm with that id.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/watch": {
      "post": {
        "operationId": "createWatch",
        "summary": "Watch a zone for new storms",
        "description": "Registers a webhook. When a new severe-weather event is detected inside the watched area, Storm Stream POSTs a storm.detected payload to your webhookUrl. The URL must be https.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["webhookUrl"],
                "properties": {
                  "webhookUrl": { "type": "string", "format": "uri", "pattern": "^https://", "description": "HTTPS endpoint that receives storm.detected events. Plain http is rejected." },
                  "zips": { "type": "array", "items": { "type": "string" }, "description": "ZIP codes to watch. Provide zips, zoneId, or polygon." },
                  "zoneId": { "type": "string", "description": "A saved zone id to watch." },
                  "polygon": { "type": "array", "items": { "$ref": "#/components/schemas/LatLng" }, "minItems": 3, "description": "Polygon ring as [lat,lng] pairs to watch." }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Watch created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "watchId": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid body, for example a non-https webhookUrl or no area given.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Missing or invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "Monthly spend cap reached.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpendCap" } } } },
          "429": { "description": "Rate limit exceeded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        },
        "callbacks": {
          "stormDetected": {
            "{$request.body#/webhookUrl}": {
              "post": {
                "summary": "storm.detected webhook",
                "description": "Fired when a new storm event is detected inside the watched area.",
                "requestBody": {
                  "required": true,
                  "content": {
                    "application/json": {
                      "schema": {
                        "type": "object",
                        "required": ["event", "storm"],
                        "properties": {
                          "event": { "type": "string", "enum": ["storm.detected"] },
                          "watchId": { "type": "string" },
                          "at": { "type": "integer", "description": "Delivery time, unix epoch milliseconds." },
                          "storm": { "$ref": "#/components/schemas/StormEvent" }
                        }
                      }
                    }
                  }
                },
                "responses": {
                  "200": { "description": "Return any 2xx to acknowledge delivery. Non-2xx responses are retried with backoff." }
                }
              }
            }
          }
        }
      }
    },
    "/v1/history": {
      "get": {
        "operationId": "getHistory",
        "summary": "Historical storm lookup for a point and date",
        "description": "Did severe weather hit this location on (or around) this date? Built for claims verification and lead qualification.",
        "parameters": [
          { "name": "lat", "in": "query", "required": true, "schema": { "type": "number", "minimum": -90, "maximum": 90 }, "description": "Latitude." },
          { "name": "lng", "in": "query", "required": true, "schema": { "type": "number", "minimum": -180, "maximum": 180 }, "description": "Longitude." },
          { "name": "date", "in": "query", "required": true, "schema": { "type": "string", "format": "date" }, "description": "Date of interest, YYYY-MM-DD." },
          { "name": "exact", "in": "query", "schema": { "type": "boolean", "default": false }, "description": "If true, only that calendar day; if false, a tolerance window around it." }
        ],
        "responses": {
          "200": {
            "description": "Events that affected the point.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/QueryResult" } } }
          },
          "401": { "description": "Missing or invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "Monthly spend cap reached.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpendCap" } } } },
          "429": { "description": "Rate limit exceeded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/forecast": {
      "get": {
        "operationId": "getForecast",
        "summary": "Severe-weather outlook for a point",
        "description": "Convenience outlook for a location. Storm Stream is about the event, not the forecast; this endpoint exists so integrators do not need a second vendor for basic outlook context.",
        "parameters": [
          { "name": "lat", "in": "query", "required": true, "schema": { "type": "number", "minimum": -90, "maximum": 90 }, "description": "Latitude." },
          { "name": "lng", "in": "query", "required": true, "schema": { "type": "number", "minimum": -180, "maximum": 180 }, "description": "Longitude." }
        ],
        "responses": {
          "200": {
            "description": "Outlook for the point.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "forecast": { "type": "object", "description": "Outlook payload for the point.", "additionalProperties": true }
                  }
                }
              }
            }
          },
          "401": { "description": "Missing or invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "Monthly spend cap reached.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpendCap" } } } },
          "429": { "description": "Rate limit exceeded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/fire": {
      "get": {
        "operationId": "getFire",
        "summary": "Active fire conditions for a point",
        "description": "Fire activity and risk context for a location.",
        "parameters": [
          { "name": "lat", "in": "query", "required": true, "schema": { "type": "number", "minimum": -90, "maximum": 90 }, "description": "Latitude." },
          { "name": "lng", "in": "query", "required": true, "schema": { "type": "number", "minimum": -180, "maximum": 180 }, "description": "Longitude." }
        ],
        "responses": {
          "200": {
            "description": "Fire conditions for the point.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "fire": { "type": "object", "description": "Fire conditions payload for the point.", "additionalProperties": true }
                  }
                }
              }
            }
          },
          "401": { "description": "Missing or invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "Monthly spend cap reached.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpendCap" } } } },
          "429": { "description": "Rate limit exceeded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/keys": {
      "post": {
        "operationId": "requestKey",
        "summary": "Self-serve API key request",
        "description": "Request an API key with just an email and a label. A verification email is sent; the key arrives after the email is verified. No auth required. Heavily rate limited.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email"],
                "properties": {
                  "email": { "type": "string", "format": "email", "description": "Where the verification email and key are sent." },
                  "label": { "type": "string", "description": "Human label for the key, for example the app or company name." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Request accepted. The key is delivered after email verification.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "note": { "type": "string", "description": "Human-readable next step, for example: check your email to verify and receive your key." }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid email.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/keys/cap": {
      "post": {
        "operationId": "setSpendCap",
        "summary": "Set the monthly spend cap for your key",
        "description": "Sets a hard monthly spend ceiling in cents for the authenticated key. Once usage reaches the cap, metered endpoints return 402 until the cap is raised or the period resets.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["monthlyCapCents"],
                "properties": {
                  "monthlyCapCents": { "type": "integer", "minimum": 0, "description": "Monthly spend cap in cents. 0 disables all metered usage." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cap updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "capCents": { "type": "integer", "description": "The cap now in effect, in cents." }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid cap value.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Missing or invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/status": {
      "get": {
        "operationId": "getStatus",
        "summary": "Service status",
        "description": "Public health and status endpoint. No auth required.",
        "security": [],
        "responses": {
          "200": {
            "description": "Service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "status": { "type": "string", "example": "operational" },
                    "time": { "type": "integer", "description": "Server time, unix epoch milliseconds." }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key passed as a bearer token: Authorization: Bearer <key>."
      }
    },
    "schemas": {
      "LatLng": {
        "type": "array",
        "description": "A [lat,lng] pair. Note: StormEvent geometry is [lat,lng]; GeoJSON output is [lng,lat].",
        "items": { "type": "number" },
        "minItems": 2,
        "maxItems": 2
      },
      "StormEvent": {
        "type": "object",
        "description": "A verified severe-weather event. The canonical open shape is published at https://stormstream.io/schema/stormevent.json.",
        "required": ["id", "name", "type", "severity", "confidence", "at", "swath", "source"],
        "properties": {
          "id": { "type": "string", "description": "Stable unique event id." },
          "name": { "type": "string", "description": "Human-readable event name." },
          "type": { "type": "string", "enum": ["hail", "wind"], "description": "Event kind." },
          "maxHail": { "type": "number", "minimum": 0, "description": "Maximum hail size in inches. Present for hail events." },
          "gust": { "type": "integer", "minimum": 0, "description": "Peak wind gust in mph. Present for wind events." },
          "severity": { "type": "string", "enum": ["moderate", "severe", "extreme"], "description": "Impact tier." },
          "confidence": { "type": "integer", "minimum": 0, "maximum": 99, "description": "Detection confidence, 0 to 99." },
          "color": { "type": "string", "description": "Suggested display color, hex string." },
          "at": { "type": "integer", "description": "Event time, unix epoch milliseconds." },
          "track": { "type": "array", "description": "Storm center track as [lat,lng] pairs.", "items": { "$ref": "#/components/schemas/LatLng" } },
          "swath": { "type": "array", "description": "Impact swath polygon ring as [lat,lng] pairs.", "items": { "$ref": "#/components/schemas/LatLng" } },
          "areaKm2": { "type": "number", "minimum": 0, "description": "Swath area in square kilometers." },
          "source": { "type": "string", "description": "Primary data source identifier." },
          "sources": { "type": "array", "description": "Source URLs backing the detection.", "items": { "type": "string", "format": "uri" } }
        }
      },
      "QueryResult": {
        "type": "object",
        "required": ["ok", "count", "storms"],
        "properties": {
          "ok": { "type": "boolean" },
          "count": { "type": "integer", "minimum": 0 },
          "storms": { "type": "array", "items": { "$ref": "#/components/schemas/StormEvent" } }
        }
      },
      "GeoJSONFeatureCollection": {
        "type": "object",
        "description": "GeoJSON FeatureCollection (RFC 7946). Coordinates are [lng,lat] order.",
        "required": ["type", "features"],
        "properties": {
          "type": { "type": "string", "enum": ["FeatureCollection"] },
          "features": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["type", "geometry", "properties"],
              "properties": {
                "type": { "type": "string", "enum": ["Feature"] },
                "geometry": { "type": "object", "additionalProperties": true },
                "properties": { "type": "object", "additionalProperties": true }
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": ["ok", "error"],
        "properties": {
          "ok": { "type": "boolean", "enum": [false] },
          "error": { "type": "string", "description": "Human-readable error message." }
        }
      },
      "SpendCap": {
        "type": "object",
        "required": ["ok", "error", "capCents", "usedCents"],
        "properties": {
          "ok": { "type": "boolean", "enum": [false] },
          "error": { "type": "string", "description": "Explains that the monthly spend cap was reached." },
          "capCents": { "type": "integer", "description": "The configured monthly cap in cents." },
          "usedCents": { "type": "integer", "description": "Usage so far this period in cents." }
        }
      }
    }
  }
}
