Webhooks
Three places in Wakora can post JSON to a URL of yours: a personal notification channel, a team-level destination in Integrations, and a public status page. All three sign the post the same way, so one verification routine covers them.
The signature
Every post carries two headers:
X-Wakora-Timestamp: 1757083845
X-Wakora-Signature: sha256=8f3c...
The signature is an HMAC-SHA256 over the string <timestamp>.<raw body>, keyed with the
secret shown next to the webhook in the console. Verify it before you parse the body, and
reject a timestamp older than a few minutes to stop replays:
import hmac, hashlib, time
def verify(headers, body, secret, tolerance=300):
ts = headers["X-Wakora-Timestamp"]
if abs(time.time() - int(ts)) > tolerance:
return False
want = "sha256=" + hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, headers["X-Wakora-Signature"])
The body must be the raw bytes as received; re-serialising the JSON changes the bytes and
breaks the check. The User-Agent is Wakora-Alerts.
Incident posts
A personal webhook (My account, Notifications) and the team webhook (Integrations) receive the same shape: one post per digest, so a burst of incidents arrives as one document listing them.
{
"team": "Acme production",
"teamId": 42,
"ts": 1757083845,
"count": 2,
"incidents": [
{ "id": 9137, "severity": "critical", "rule": "disk_critical",
"title": "disk almost full", "host": "db-1", "entity": "/var" },
{ "id": 9138, "severity": "warning", "rule": "cert_expiring",
"title": "certificate expiring", "host": "web-1", "entity": "shop.example.com" }
]
}
id is a stable identifier of the incident for correlating repeated posts about the same
one; it is not the number the console shows. When the rule has a
runbook, the incident object also carries a runbooks array
(title, url, text, matched). To act on an incident from your receiver,
look it up through the REST API by rule, host and entity, or
switch the receiver to a Slack or Teams integration, which carries acknowledge buttons of
its own.
Delivery honours the channel's severity floor and quiet hours like any other personal channel, except for escalation steps, which always post. A receiver that fails is not retried; the post is fire-and-forget, so keep the endpoint fast and acknowledge with any 2xx before doing work.
Status page posts
A webhook added to a status page (the page's Subscribers tab) receives every public post and every state change of that page:
{
"event": "status.update",
"page": "Acme status",
"slug": "acme",
"url": "https://acme.status.wakora.io",
"update": {
"kind": "incident",
"state": "investigating",
"title": "Checkout is slow",
"body": "We are looking into elevated latency on checkout.",
"startsAt": "",
"durationMin": 0,
"postedAt": "2026-09-05T13:20:11Z"
}
}
kind is incident or maintenance; a maintenance post carries startsAt and
durationMin. Its secret is shown once on the Subscribers tab when the webhook is added.