{"openapi":"3.0.3","info":{"title":"SAMOWESP Research API","version":"1.0.0","description":"Read access to the SAMOWESP wildfire situation platform for research and agency use: active-fire detections (NASA FIRMS/GWIS), burnt-area perimeters (EFFIS), fire weather index time series (EFFIS/GWIS) and the ground sensor network (raw series).\n\n## Stability promise (v1)\nThe `/api/v1` prefix is a reproducibility commitment to ongoing theses and long-running scripts. Within v1: existing fields, parameters, enum values and error codes are never removed, renamed or changed in meaning — extensions are strictly additive. Timestamps are always ISO 8601 UTC; research data never switches to a display time zone. Rows from simulated demo stations always carry `simulated: true`. A breaking change means a new prefix (/api/v2) with v1 kept alive for a documented transition period.\n\n## Honesty rules\nEvery JSON response carries `meta.observation` — whether the feeding import ran at all and how old its last success is. An empty collection means: looked, nothing there. HTTP 503 means: did not look. Do not conflate the two. Detection counts are pixels, not fires.\n\n## Authentication\nAll data endpoints require `Authorization: Bearer ffw_…`. Keys are issued by the samoLabs operator (see /api-docs). 401 = no usable key, 403 = key known but revoked/expired, 429 = rate limited (120 requests/minute/key). This includes /overpasses, which was open until v0.13.0: its data is harmless (open CelesTrak orbital elements, nothing about agencies or operations), but each fresh reference point costs ~240 ms of blocking SGP4 computation in a single-threaded process — an open endpoint with that price tag is a denial-of-service lever, not a courtesy. Browser clients inside SAMOWESP authenticate by session cookie instead of a key.","contact":{"name":"samoLabs","email":"info@samolabs.de"}},"servers":[{"url":"/api/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API key issued by the operator, format ffw_<40 url-safe characters>. Scope AGENCY (agency machine access) or RESEARCH (student/research access) — both read the same data today; the scope is recorded for future differentiation."}},"schemas":{"Error":{"type":"object","required":["status","code","apiVersion","generatedAt"],"properties":{"status":{"type":"string","enum":["error"]},"code":{"type":"string","enum":["missing_token","invalid_token","key_revoked","key_expired","rate_limited","not_found","bad_request"]},"detail":{"type":"string"},"apiVersion":{"type":"string","enum":["v1"]},"generatedAt":{"type":"string","format":"date-time"}}},"FeatureCollection":{"type":"object","description":"RFC 7946 FeatureCollection with an additional `meta` member (generatedAt, returned, limit, truncated, matched, window, observation, endpoint-specific fields). /fire-danger may deliver features with `geometry: null` (values without an imported boundary).","required":["type","features","meta"],"properties":{"type":{"type":"string","enum":["FeatureCollection"]},"features":{"type":"array","items":{"type":"object"}},"meta":{"type":"object"}}},"Overpass":{"type":"object","description":"One overpass: the moment the sub-satellite track comes closest to the reference point.","required":["satellite","noradId","sensor","purpose","retirementFrom","timeIso","dayNight","quality","distanceKm"],"properties":{"satellite":{"type":"string","description":"Proper name, never translated (e.g. 'Sentinel-2C')."},"noradId":{"type":"integer","description":"NORAD catalogue number. Verified against CelesTrak per satellite; scripts/fetch-tle.ts re-checks the returned catalogue name on every run, so a mistyped number fails loudly instead of silently propagating a foreign orbit."},"sensor":{"type":"string","enum":["VIIRS","MODIS","MSI","SLSTR","TROPOMI"]},"purpose":{"type":"string","enum":["fire","detail","gas","none"],"description":"Which question this overpass answers. 'none' = flies over, not evaluated here (Aqua, Terra)."},"retirementFrom":{"type":"string","nullable":true,"description":"Announced end of operations as YYYY-MM, else null. Sourced from the spacecraft registry so the figure cannot drift from the one shown in /statistics."},"timeIso":{"type":"string","format":"date-time","description":"Time of closest approach, ISO 8601 UTC."},"dayNight":{"type":"string","enum":["day","night"],"description":"Sun above the horizon at the REFERENCE POINT, not below the satellite. Sun-dependent sensors never report 'night'."},"quality":{"type":"string","enum":["good","edge"],"description":"edge = reference point at the swath edge, coarser pixels."},"distanceKm":{"type":"integer","description":"Ground distance between sub-satellite track and reference point."}}}},"parameters":{"format":{"name":"format","in":"query","description":"`csv` returns RFC-4180 CSV (comma separator, header row, UTF-8, ISO 8601 UTC timestamps) readable by pandas.read_csv and readr::read_csv without extra options. Anything else returns GeoJSON.","schema":{"type":"string","enum":["geojson","csv"],"default":"geojson"}},"from":{"name":"from","in":"query","description":"Window start, ISO 8601. A bare date (YYYY-MM-DD) is read as UTC midnight. For reproducible extracts ALWAYS pass explicit from/to — the effective window is echoed in meta.window.","schema":{"type":"string"},"example":"2026-07-01"},"to":{"name":"to","in":"query","description":"Window end, ISO 8601. Defaults to now.","schema":{"type":"string"},"example":"2026-08-01T00:00:00Z"},"bbox":{"name":"bbox","in":"query","description":"Spatial filter as west,south,east,north (WGS84). Defaults to the project working area 13.3,40.8,23.1,46.9.","schema":{"type":"string"},"example":"17.5,43.5,19.0,44.5"},"country":{"name":"country","in":"query","description":"Comma-separated ISO country filter over the seven target states (SI, HR, BA, RS, ME, MK, XK). Foreign spellings for Kosovo (KS, XKO, XKX) are accepted; unmatched entries are reported in meta.filters.rejectedCountry, never silently dropped.","schema":{"type":"string"},"example":"BA,ME"},"limit":{"name":"limit","in":"query","description":"Row cap. If the result was cut, meta.truncated is true and meta.matched carries the full count — narrow the window and page by time, the API has no offset parameter.","schema":{"type":"integer"}}}},"paths":{"/overpasses":{"get":{"summary":"Upcoming satellite overpasses (SGP4 forecast)","description":"Next passes of the eleven tracked spacecraft over a reference point, computed from CelesTrak TLEs for the next 96 hours. Grouped by the question each answers (`purpose`): `fire` — VIIRS on Suomi-NPP/NOAA-20/NOAA-21 plus SLSTR on Sentinel-3A/3B; `detail` — MSI on Sentinel-2A/2B/2C (10 m, 290 km swath); `gas` — TROPOMI on Sentinel-5P; `none` — Aqua and Terra, which still fly over the area but whose MODIS data is on no retrieval schedule here (see /statistics). Sun-dependent sensors (MSI, TROPOMI) report daylight passes only — a night overpass of an optical imager produces no image. Requires a key like every other endpoint (open until v0.13.0 — see Authentication). The reference point is snapped to a 0.05° grid (~5.5 km, two orders of magnitude below the narrowest swath) so that repeated queries hit the cache; `center` in the response is the grid point actually computed for. `available: false` means no usable orbital data — an empty list then does NOT mean 'no overpasses'. This endpoint is also the system's honesty statement: it shows the observation gap (~15:00–01:00 local time) during the Mediterranean afternoon fire maximum.","parameters":[{"name":"limit","in":"query","description":"Maximum number of passes returned (default 10, max 50).","schema":{"type":"integer","default":10,"maximum":50}},{"name":"lat","in":"query","description":"Latitude of the reference point (WGS84). Defaults to the centre of the working area; a point outside the working area falls back to that default. Matters most for Sentinel-2: at 290 km swath the overpass is a local event, so 'when is the next 10 m image' has a different answer for Bihać than for Skopje.","schema":{"type":"number"}},{"name":"lng","in":"query","description":"Longitude of the reference point (WGS84). See `lat`.","schema":{"type":"number"}}],"responses":{"200":{"description":"Forecast with UTC timestamps, ascending by time of closest approach.","content":{"application/json":{"schema":{"type":"object","required":["generatedAt","from","to","available","matched","passes"],"properties":{"generatedAt":{"type":"string","format":"date-time"},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"available":{"type":"boolean","description":"false = no orbital data; do not read an empty list as 'no passes'."},"center":{"type":"object","description":"Reference point the forecast was computed for (lat/lng, WGS84).","properties":{"lat":{"type":"number"},"lng":{"type":"number"}}},"tle":{"type":"object","description":"Age and coverage of the TLE set used (fetchedAt, ageHours, used, missing)."},"matched":{"type":"integer"},"next":{"type":"object","description":"The next pass per purpose — the direct answer to 'when is the next fire detection / detail image / gas measurement'. Computed from the FULL remaining list, so it survives the `limit` cut: the next Sentinel-2 pass regularly sits behind twenty VIIRS passes. `null` for a purpose means none within the forecast window — which is not the same as 'no orbital data' (see `available`).","properties":{"fire":{"$ref":"#/components/schemas/Overpass","nullable":true},"detail":{"$ref":"#/components/schemas/Overpass","nullable":true},"gas":{"$ref":"#/components/schemas/Overpass","nullable":true},"none":{"$ref":"#/components/schemas/Overpass","nullable":true}}},"passes":{"type":"array","items":{"$ref":"#/components/schemas/Overpass"}}}}}}}}}},"/hotspots":{"get":{"summary":"Active-fire detections (raw source columns)","description":"Satellite fire detections with the full raw columns (both brightness temperatures, pixel size, product/version, externalId). One fire produces many detections — never count rows as fires. Default window: last 7 days; max span 366 days.","parameters":[{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"},{"$ref":"#/components/parameters/bbox"},{"$ref":"#/components/parameters/country"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/format"},{"name":"static","in":"query","description":"Include permanent industrial heat sources (steel works, power plants) — excluded by default.","schema":{"type":"boolean","default":false}},{"name":"retired","in":"query","description":"Include detections later retracted by NASA (RT/URT superseded by NRT) — excluded by default.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Detections, ascending by acquiredAt.","content":{"application/geo+json":{"schema":{"$ref":"#/components/schemas/FeatureCollection"}},"text/csv":{"schema":{"type":"string"}}}},"401":{"description":"No usable bearer token. `missing_token`: no or malformed Authorization header. `invalid_token`: unknown key. Obtain a key from the operator.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is known but no longer authorized. `key_revoked`: revoked by the operator. `key_expired`: past its expiry date. Contact the operator for a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (120 requests per minute per key). Honour the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database unavailable — the service did NOT look. Distinguish this from an empty collection, which means: looked, nothing there.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/burnt-areas":{"get":{"summary":"Burnt-area perimeters (EFFIS)","description":"Mapped fire perimeters with full source attributes. Day-resolution time axis (FIREDATE). Geometry only in the GeoJSON variant; CSV is the attribute table. Default window: last 366 days.","parameters":[{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"},{"$ref":"#/components/parameters/bbox"},{"$ref":"#/components/parameters/country"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/format"},{"name":"simplify","in":"query","description":"Douglas-Peucker tolerance in degrees for GeoJSON geometry (default 0.0002 ≈ 22 m, the Sentinel-2 origin resolution). 0 returns original geometry.","schema":{"type":"number"}},{"name":"undated","in":"query","description":"Include perimeters without a fire date (otherwise removed by the time filter).","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Perimeters, chronological.","content":{"application/geo+json":{"schema":{"$ref":"#/components/schemas/FeatureCollection"}},"text/csv":{"schema":{"type":"string"}}}},"401":{"description":"No usable bearer token. `missing_token`: no or malformed Authorization header. `invalid_token`: unknown key. Obtain a key from the operator.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is known but no longer authorized. `key_revoked`: revoked by the operator. `key_expired`: past its expiry date. Contact the operator for a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (120 requests per minute per key). Honour the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database unavailable — the service did NOT look. Distinguish this from an empty collection, which means: looked, nothing there.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/fire-danger":{"get":{"summary":"Fire Weather Index time series","description":"Daily FWI values incl. all Canadian FWI system sub-indices (ffmc, dmc, dc, isi, bui). Each row carries its reference level: `country` (coarse country hull) or `adminArea` (polygon intersect) — never aggregate across both. Models are never mixed; one response carries exactly one model. Default window: last 30 days.","parameters":[{"name":"model","in":"query","description":"FWI model: effis (Meteo-France 0.1°, default) or gwis (ECMWF 0.0714°).","schema":{"type":"string","enum":["effis","gwis"],"default":"effis"}},{"name":"forecastDay","in":"query","description":"0 = analysis day, 1..9 = forecast day (EFFIS ends at 3).","schema":{"type":"integer","minimum":0,"maximum":9,"default":0}},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"},{"$ref":"#/components/parameters/country"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/format"},{"name":"simplify","in":"query","description":"Geometry tolerance in degrees (GeoJSON only, default 0.01).","schema":{"type":"number"}}],"responses":{"200":{"description":"Daily rows ascending by validOn. GeoJSON features may carry geometry: null; meta.withoutGeometry counts them.","content":{"application/geo+json":{"schema":{"$ref":"#/components/schemas/FeatureCollection"}},"text/csv":{"schema":{"type":"string"}}}},"401":{"description":"No usable bearer token. `missing_token`: no or malformed Authorization header. `invalid_token`: unknown key. Obtain a key from the operator.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is known but no longer authorized. `key_revoked`: revoked by the operator. `key_expired`: past its expiry date. Contact the operator for a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (120 requests per minute per key). Honour the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database unavailable — the service did NOT look. Distinguish this from an empty collection, which means: looked, nothing there.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/sensor-stations":{"get":{"summary":"Ground sensor station registry","description":"Master data for /sensor-readings: station id, location, type (AIR/WATER), status and the `simulated` flag. Join locally against readings via stationId.","parameters":[{"name":"type","in":"query","description":"Filter by station type.","schema":{"type":"string","enum":["AIR","WATER"]}},{"$ref":"#/components/parameters/bbox"},{"$ref":"#/components/parameters/country"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/format"}],"responses":{"200":{"description":"Stations, alphabetical.","content":{"application/geo+json":{"schema":{"$ref":"#/components/schemas/FeatureCollection"}},"text/csv":{"schema":{"type":"string"}}}},"401":{"description":"No usable bearer token. `missing_token`: no or malformed Authorization header. `invalid_token`: unknown key. Obtain a key from the operator.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is known but no longer authorized. `key_revoked`: revoked by the operator. `key_expired`: past its expiry date. Contact the operator for a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (120 requests per minute per key). Honour the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database unavailable — the service did NOT look. Distinguish this from an empty collection, which means: looked, nothing there.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/sensor-readings":{"get":{"summary":"Ground sensor measurement series (raw values)","description":"The append-only raw series of the sensor network (PM1/PM2.5/PM10, temperature, humidity, battery, charging, RSSI), 10-minute cycle. EVERY row carries `simulated`. `series=corrected` currently returns the raw values with `correctionModel: null` (no correction applied); server-side versioned correction models (humidity bias, per-box co-location factors) will populate `correctionModel` with a model id later WITHOUT changing the response shape — scripts written against `corrected` today keep working. Default window: last 7 days; max span 366 days.","parameters":[{"name":"station","in":"query","description":"Restrict to one station id (404 if unknown).","schema":{"type":"string"}},{"name":"series","in":"query","description":"raw = untouched measurements (default). corrected = correction layer, see above.","schema":{"type":"string","enum":["raw","corrected"],"default":"raw"}},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/format"}],"responses":{"200":{"description":"Readings ordered by station, then time (stable, diff-friendly).","content":{"application/geo+json":{"schema":{"$ref":"#/components/schemas/FeatureCollection"}},"text/csv":{"schema":{"type":"string"}}}},"401":{"description":"No usable bearer token. `missing_token`: no or malformed Authorization header. `invalid_token`: unknown key. Obtain a key from the operator.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is known but no longer authorized. `key_revoked`: revoked by the operator. `key_expired`: past its expiry date. Contact the operator for a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown station id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (120 requests per minute per key). Honour the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database unavailable — the service did NOT look. Distinguish this from an empty collection, which means: looked, nothing there.","content":{"application/json":{"schema":{"type":"object"}}}}}}}}}