Data and API › API

API and webhooks

The open endpoints work for everyone. Business and Organisation plans add a keyed API without cache delay, and webhooks to Slack, Teams, Discord or your own system.

Authentication and limits

Create a key in the Organisation section of your account (up to 3 keys). The key is shown once, and we store only its hash. Send it in the header Authorization: Bearer fwsi_live_…. Limits per key: 60 requests a minute and 10,000 a day; over a limit the answer is 429 and Retry-After says when to continue.

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

Endpoints

EndpointAccessParametersContents
GET /api/v1/alertskeysince limit Alerts raised by verified readings, newest first.
GET /api/v1/briefings/datekeylang The daily briefing of a date (Europe/Helsinki), as HTML and plain text.
GET /api/v1/eventsopenpillar type from Events
GET /api/v1/events/event_idkeyOne event with its revisions, Finnish title and why it matters.
GET /api/v1/forecast/latestkeyThe no-news projection for the next 7 days (SPEC §7.7 part 1): where the index drifts if nothing new happens.
GET /api/v1/latestkeyThe newest reading straight from the source: interim readings included, full pillar detail, no CDN delay.
GET /api/v1/readingsopenfrom to vintage Readings
GET /api/v1/statusopenStatus

Full description in OpenAPI format: /api/openapi.json

Webhooks

Add up to 5 endpoints (public https addresses only). Events: reading.published, alert.* and briefing.published. Slack, Teams and Discord get a ready message, generic gets the JSON below. A failed delivery is retried after 1, 5, 15 and 60 minutes; an endpoint silent for 24 hours is turned off and the owner is emailed.

Example: a generic event

{
  "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/"
  }
}

Verifying the signature

Every request has the header X-FWSI-Signature: t=<time>,v1=<signature>, where the signature is HMAC-SHA256 with the webhook's secret over <time>.<request body>. Check it before trusting the content, and reject old timestamps. Sample code (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()
#       ...

Licence

API data may be used inside your organisation. Redistribution needs an Organisations contract. The open data (/data/) is licensed CC BY 4.0.