{
  "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.",
    "termsOfService": "https://stormstream.io/terms/",
    "contact": {
      "name": "Storm Stream Support",
      "email": "support@stormstream.io",
      "url": "https://stormstream.io/"
    }
  },
  "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, resolved nationwide. 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"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid query area (need polygon, point, address, zip, metro, or bbox).",
            "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. Raise the cap via POST /v1/keys/cap or wait for the next billing period.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpendCap"
                }
              }
            }
          },
          "403": {
            "description": "API key has been revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The address or ZIP could not be geocoded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The geocoder is briefly unavailable; retry.",
            "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"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid query area (need polygon, point, address, zip, metro, or bbox).",
            "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"
                }
              }
            }
          },
          "403": {
            "description": "API key has been revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The address or ZIP could not be geocoded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The geocoder is briefly unavailable; retry.",
            "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"
                }
              }
            }
          },
          "403": {
            "description": "API key has been revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "402": {
            "description": "Monthly spend cap or free ceiling reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpendCap"
                }
              }
            }
          },
          "403": {
            "description": "API key has been revoked.",
            "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"
                }
              }
            }
          },
          "402": {
            "description": "Monthly spend cap or free ceiling reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpendCap"
                }
              }
            }
          },
          "403": {
            "description": "API key has been revoked.",
            "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": {
      "get": {
        "operationId": "listWatches",
        "summary": "List your watches",
        "description": "Returns the watches owned by the authenticated key. webhookUrl is not echoed back (it can embed a delivery secret).",
        "responses": {
          "200": {
            "description": "Your watches.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "watches"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "watches": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "zoneId": {
                            "type": "string"
                          },
                          "owner": {
                            "type": "string"
                          },
                          "createdAt": {
                            "type": "integer"
                          },
                          "lastFiredAt": {
                            "type": "integer"
                          },
                          "deliverFails": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Monthly spend cap or free ceiling reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpendCap"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "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 zone, zoneId, zips, or polygon."
                  },
                  "zone": {
                    "type": "string",
                    "description": "A saved zone id (zon_...) to watch. Alternative to zoneId."
                  },
                  "zoneId": {
                    "type": "string",
                    "description": "A saved zone id (zon_...) to watch. Alternative to zone."
                  },
                  "polygon": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/LatLng"
                    },
                    "minItems": 3,
                    "description": "Polygon ring as [lat,lng] pairs to watch."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Watch created. The watch id is at watch.id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "watch"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "watch": {
                      "type": "object",
                      "description": "The created watch.",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Watch id (wat_...). Pass to DELETE /v1/watch/{id} to stop it."
                        },
                        "zoneId": {
                          "type": "string"
                        },
                        "owner": {
                          "type": "string"
                        },
                        "createdAt": {
                          "type": "integer",
                          "description": "Creation time, unix epoch milliseconds."
                        },
                        "lastFiredAt": {
                          "type": "integer",
                          "description": "Last delivery time, unix epoch milliseconds (0 until the first fire)."
                        }
                      }
                    },
                    "zone": {
                      "type": "object",
                      "description": "The resolved zone being watched (id, name, geometry, areaKm2).",
                      "additionalProperties": true
                    },
                    "note": {
                      "type": "string",
                      "description": "Human-readable confirmation."
                    }
                  }
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "404": {
            "description": "Saved zone not found (unknown id, or not owned by this key).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "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",
                          "type",
                          "storm"
                        ],
                        "properties": {
                          "event": {
                            "type": "string",
                            "enum": [
                              "storm.detected",
                              "storm.updated"
                            ],
                            "description": "The event name. Match on this."
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "storm.detected",
                              "storm.updated"
                            ],
                            "description": "Alias of event (same value)."
                          },
                          "product": {
                            "type": "string",
                            "enum": [
                              "storm"
                            ]
                          },
                          "subject": {
                            "type": "string",
                            "description": "The watched zone name."
                          },
                          "watchId": {
                            "type": "string",
                            "description": "The watch that matched."
                          },
                          "zone": {
                            "type": "object",
                            "description": "The watched zone.",
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "name": {
                                "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/watch/{id}": {
      "delete": {
        "operationId": "deleteWatch",
        "summary": "Stop a watch",
        "description": "Deletes a watch by id. Only the owner (or the founder) can delete it.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Watch id (wat_...)."
          }
        ],
        "responses": {
          "200": {
            "description": "Watch deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "deleted": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Monthly spend cap or free ceiling reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpendCap"
                }
              }
            }
          },
          "403": {
            "description": "Not your watch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No watch 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/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, that calendar day only, resolved from NOAA NCEI radar-hail records when available (source: noaa). If false, a modeled multi-year reconstruction for the point back from the date (source: demo), for context, not a certified record. Check the response source field."
          }
        ],
        "responses": {
          "200": {
            "description": "Storm history for the point. Each event carries the response source (noaa or demo).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HistoryResult"
                }
              }
            }
          },
          "400": {
            "description": "lat and lng are required (geocode the city/state/zip first), or the date is invalid.",
            "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"
                }
              }
            }
          },
          "403": {
            "description": "API key has been revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "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. Public and unmetered (no API key required); lightly rate limited per IP. Storm Stream is about the event, not the forecast; this endpoint exists so integrators do not need a second vendor for basic outlook context.",
        "security": [],
        "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": "Flat outlook payload for the point (ok plus outlook fields such as level, label, headline, hours, peak).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "place": {
                      "type": "string"
                    },
                    "level": {
                      "type": "integer",
                      "description": "Outlook level for the point."
                    },
                    "label": {
                      "type": "string",
                      "description": "Human-readable outlook label."
                    },
                    "headline": {
                      "type": "string"
                    },
                    "hours": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      },
                      "description": "Per-hour outlook."
                    },
                    "peak": {
                      "type": "object",
                      "nullable": true,
                      "additionalProperties": true,
                      "description": "The peak window, or null on a quiet point with no risk hour."
                    }
                  },
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "lat and lng are required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (per IP).",
            "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. Public and unmetered (no API key required); lightly rate limited per IP.",
        "security": [],
        "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": "radiusKm",
            "in": "query",
            "schema": {
              "type": "number"
            },
            "description": "Search radius in kilometers (default 120)."
          }
        ],
        "responses": {
          "200": {
            "description": "Flat fire-conditions payload for the point (ok plus fields such as level, label, watch, fires).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "level": {
                      "type": "integer"
                    },
                    "label": {
                      "type": "string"
                    },
                    "watch": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      },
                      "description": "Red-flag / fire-weather watch entries (empty array when none)."
                    },
                    "fires": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      },
                      "description": "Nearby active fire detections."
                    }
                  },
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "lat and lng are required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (per IP).",
            "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. A verification email is sent; the key is revealed after it is confirmed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "sent"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "sent": {
                      "type": "boolean",
                      "description": "True when the verification email was dispatched."
                    },
                    "to": {
                      "type": "string",
                      "description": "The email the verification was sent to."
                    },
                    "mailer": {
                      "type": "string",
                      "description": "Which mail transport handled it."
                    }
                  }
                }
              }
            }
          },
          "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": 100,
                    "maximum": 10000000,
                    "description": "Monthly spend cap in cents, from 100 ($1) to 10000000 ($100,000). Past the cap, metered endpoints return 402 until it is raised or the period resets."
                  }
                }
              }
            }
          }
        },
        "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": "Live service status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "product",
                    "at"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "product": {
                      "type": "string",
                      "enum": [
                        "storm"
                      ]
                    },
                    "provider": {
                      "type": "string",
                      "description": "The active data source (for example nws or demo)."
                    },
                    "live": {
                      "type": "boolean",
                      "description": "True when serving a live feed rather than demo data."
                    },
                    "at": {
                      "type": "integer",
                      "description": "Server time, unix epoch milliseconds."
                    },
                    "health": {
                      "type": "object",
                      "properties": {
                        "p95ms": {
                          "type": "number"
                        },
                        "errorRate": {
                          "type": "number"
                        }
                      }
                    },
                    "usage": {
                      "type": "object",
                      "properties": {
                        "apiCalls": {
                          "type": "integer"
                        },
                        "stormsIngested": {
                          "type": "integer"
                        },
                        "zonesWatched": {
                          "type": "integer"
                        },
                        "coverageKm2": {
                          "type": "number"
                        }
                      }
                    },
                    "mail": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": [
                            "boolean",
                            "null"
                          ]
                        }
                      }
                    },
                    "security": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "ok": {
                          "type": [
                            "boolean",
                            "null"
                          ]
                        },
                        "at": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "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",
        "description": "A 402 body. A cap-based 402 carries capCents and usedCents; the free-ceiling 402 carries callsUsed instead. Both carry ok, error, and upgrade.",
        "required": [
          "ok",
          "error"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "error": {
            "type": "string",
            "description": "Explains that the spend cap or the free ceiling was reached."
          },
          "upgrade": {
            "type": "string",
            "description": "The suggested next tier, for example api or full."
          },
          "capCents": {
            "type": "integer",
            "description": "The configured monthly cap in cents (present on a cap-based 402)."
          },
          "usedCents": {
            "type": "integer",
            "description": "Usage so far this period in cents (present on a cap-based 402)."
          },
          "callsUsed": {
            "type": "integer",
            "description": "Metered calls used this month (present on the free-ceiling 402)."
          }
        }
      },
      "HistoryEvent": {
        "type": "object",
        "description": "One historical storm event at a point. Radar-derived (source: noaa) or a modeled reconstruction (source: demo).",
        "required": [
          "id",
          "type",
          "severity",
          "dateMs",
          "source"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable event id (hist_... for modeled, noaa_... for radar-derived)."
          },
          "type": {
            "type": "string",
            "enum": [
              "hail",
              "wind",
              "tornado"
            ]
          },
          "maxHail": {
            "type": "number",
            "minimum": 0,
            "description": "Maximum hail size in inches (hail events)."
          },
          "gust": {
            "type": "integer",
            "minimum": 0,
            "description": "Peak wind gust in mph (wind/tornado events)."
          },
          "ef": {
            "type": "string",
            "description": "EF rating for tornado events, for example EF2 (empty otherwise)."
          },
          "severity": {
            "type": "string",
            "enum": [
              "moderate",
              "severe",
              "extreme"
            ]
          },
          "confidence": {
            "type": "integer",
            "minimum": 0,
            "maximum": 99
          },
          "color": {
            "type": "string",
            "description": "Suggested display color, hex string."
          },
          "dateMs": {
            "type": "integer",
            "description": "The event date (midnight UTC), unix epoch milliseconds."
          },
          "startMin": {
            "type": "integer",
            "description": "Local start time as minutes past midnight."
          },
          "durationMin": {
            "type": "integer",
            "description": "Event duration in minutes."
          },
          "magnitude": {
            "type": "string",
            "description": "Human-readable magnitude, for example 2.15\" hail."
          },
          "headline": {
            "type": "string"
          },
          "place": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "track": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LatLng"
            },
            "description": "Storm center track as [lat,lng] pairs."
          },
          "swath": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LatLng"
            },
            "description": "Impact swath ring as [lat,lng] pairs."
          },
          "source": {
            "type": "string",
            "enum": [
              "noaa",
              "demo"
            ],
            "description": "noaa = radar-derived record; demo = modeled reconstruction."
          }
        }
      },
      "HistoryResult": {
        "type": "object",
        "required": [
          "ok",
          "source",
          "count",
          "events"
        ],
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "place": {
            "type": "string"
          },
          "exact": {
            "type": "boolean",
            "description": "True when the result is for a single calendar day."
          },
          "date": {
            "type": "integer",
            "description": "The resolved day (present when exact), unix epoch milliseconds."
          },
          "from": {
            "type": "integer",
            "description": "Window start (present when not exact), unix epoch milliseconds."
          },
          "to": {
            "type": "integer",
            "description": "Window end (present when not exact), unix epoch milliseconds."
          },
          "source": {
            "type": "string",
            "enum": [
              "noaa",
              "demo"
            ],
            "description": "The data source backing these events."
          },
          "count": {
            "type": "integer",
            "minimum": 0
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HistoryEvent"
            }
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "API recipes and runnable examples",
    "url": "https://stormstream.io/recipes/"
  }
}
