Data ja API › API

Rajapinta (API) ja webhookit

Avoimet osoitteet toimivat kaikille. Yritys- ja organisaatiotilauksiin kuuluu avaimella käytettävä rajapinta ilman välimuistiviivettä sekä webhookit Slackiin, Teamsiin, Discordiin tai omaan järjestelmään.

Tunnistautuminen ja rajat

Luo avain tilisi Organisaatio-osiossa (enintään 3 avainta). Avain näytetään vain kerran, ja me tallennamme siitä vain tiivisteen. Lähetä se otsakkeessa Authorization: Bearer fwsi_live_…. Rajat avainta kohden: 60 pyyntöä minuutissa ja 10 000 vuorokaudessa; rajan ylittyessä vastaus on 429 ja Retry-After kertoo, milloin voi jatkaa.

curl -H "Authorization: Bearer fwsi_live_…" https://sotatila.fi/api/v1/latest

Osoitteet

OsoiteKäyttöParametritSisältö
GET /api/v1/alertsavainsince limit Alerts raised by verified readings, newest first.
GET /api/v1/briefings/dateavainlang The daily briefing of a date (Europe/Helsinki), as HTML and plain text.
GET /api/v1/eventsavoinpillar type from Events
GET /api/v1/events/event_idavainOne event with its revisions, Finnish title and why it matters.
GET /api/v1/forecast/latestavainThe no-news projection for the next 7 days (SPEC §7.7 part 1): where the index drifts if nothing new happens.
GET /api/v1/latestavainThe newest reading straight from the source: interim readings included, full pillar detail, no CDN delay.
GET /api/v1/readingsavoinfrom to vintage Readings
GET /api/v1/statusavoinStatus

Koko kuvaus OpenAPI-muodossa: /api/openapi.json

Webhookit

Lisää enintään 5 osoitetta (vain julkiset https-osoitteet). Tapahtumat: reading.published, alert.* ja briefing.published. Slack, Teams ja Discord saavat valmiin viestin, generic alla olevan JSONin. Epäonnistunut toimitus yritetään uudelleen 1, 5, 15 ja 60 minuutin kuluttua; 24 tuntia vastaamatta ollut osoite poistetaan käytöstä ja omistajalle lähetetään sähköposti.

Esimerkki: generic-tapahtuma

{
  "id": "evt_a123",
  "type": "alert.ewl_up",
  "created_at": "2026-12-01T06:05:00+00:00",
  "reading": {
    "date": "2026-12-01",
    "wsi": 41.2,
    "band": "High",
    "ewl": 2,
    "ewl_name": "Elevated",
    "delta_24h": 9.1,
    "top_signals": [
      "…"
    ]
  },
  "alert": {
    "code": "ewl_up",
    "text_fi": "…",
    "text_en": "…"
  },
  "links": {
    "briefing": "https://sotatila.fi/fi/tili/katsaukset/123/",
    "dashboard": "https://sotatila.fi/fi/"
  }
}

Allekirjoituksen tarkistus

Jokaisessa pyynnössä on otsake X-FWSI-Signature: t=<aika>,v1=<allekirjoitus>, jossa allekirjoitus on HMAC-SHA256 webhookin salaisella avaimella merkkijonosta <aika>.<pyynnön runko>. Tarkista se ennen kuin luotat sisältöön, ja hylkää vanhat aikaleimat. Esimerkkikoodi (Python):

"""Verify an FWSI webhook (sample code for customers; also run by the test suite).

FWSI signs every request:  X-FWSI-Signature: t=<unix time>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>
Use the raw request body exactly as received, and reject old timestamps to stop replays.
"""

import hashlib
import hmac
import time


def verify(secret: str, signature_header: str, raw_body: bytes, tolerance_s: int = 300) -> bool:
    try:
        parts = dict(item.split("=", 1) for item in signature_header.split(","))
        t, given = int(parts["t"]), parts["v1"]
    except (ValueError, KeyError):
        return False
    if abs(time.time() - t) > tolerance_s:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, given)


# Flask example:
#   @app.post("/fwsi")
#   def fwsi_hook():
#       if not verify(SECRET, request.headers["X-FWSI-Signature"], request.get_data()):
#           abort(401)
#       event = request.get_json()
#       ...

Käyttöehdot

Rajapinnan tietoja saa käyttää organisaation omaan käyttöön. Edelleenjakelu vaatii Organisaatiot-sopimuksen. Avoin data (/data/) on CC BY 4.0 -lisenssillä.