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
| Endpoint | Access | Parameters | Contents |
|---|---|---|---|
GET /api/v1/alerts | key | since limit | Alerts raised by verified readings, newest first. |
GET /api/v1/briefings/date | key | lang | The daily briefing of a date (Europe/Helsinki), as HTML and plain text. |
GET /api/v1/events | open | pillar type from | Events |
GET /api/v1/events/event_id | key | One event with its revisions, Finnish title and why it matters. | |
GET /api/v1/forecast/latest | key | The no-news projection for the next 7 days (SPEC §7.7 part 1): where the index drifts if nothing new happens. | |
GET /api/v1/latest | key | The newest reading straight from the source: interim readings included, full pillar detail, no CDN delay. | |
GET /api/v1/readings | open | from to vintage | Readings |
GET /api/v1/status | open | Status |
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.