Alert channels
Where an alert goes. Each notification group can have several, each with its own minimum severity and its own quiet hours.
Available now
| CHANNEL | DESTINATION | SECRET | NOTE |
|---|
| Email | An email address | None — the central sends through its own SMTP | The fallback when a host has no notification group: ALERT_MAIL_TO. |
| SMS | A phone number | In the installation config, not per customer | Needs an SMS gateway configured on the installation. Best used with a minimum severity of critical. |
| Webhook | An https URL you control | Optional signing key, stored encrypted | Our own JSON, versioned, with the whole batch in one request and an HMAC-SHA256 signature over the raw body. This is the one that covers Discord, Teams, Zapier and n8n. |
| ntfy | https://ntfy.sh/your-topic, or your own server | Optional token | The body is the text; title, priority and tags are headers. |
| Gotify | https://gotify.example/message | Application token | Self-hosted push. Needs ALERT_WEBHOOK_ALLOW_PRIVATE if it runs on your LAN. |
| Slack | An incoming webhook URL | The URL is the secret | Shown masked everywhere, because the URL alone is write access to the channel. |
| Mattermost | An incoming webhook URL | The URL is the secret | Same body as Slack. |
| Home Assistant | /api/webhook/<id> | The webhook id is the secret | The webhook route only. We will not store a long-lived token: that is control over the whole installation. |
| Pushbullet | A device, or all of them | Access token | The only channel with no URL at all. |
The webhook payload
Our own JSON, versioned from the first release so it can be changed without breaking what you built on it. The whole batch goes in one request.
{
"version": 1,
"kind": "fired",
"sent_at": "2026-11-06T22:41:00+00:00",
"alerts": [
{
"rule": "disk.full",
"severity": "critical",
"summary": "/var on nas is 97.0% full",
"host": "nas",
"fingerprint": "9f2c…",
"started_at": "2026-11-06T22:36:00+00:00",
"resolved_at": null,
"context": {"used_percent": 97, "mountpoint": "/var"}
}
]
}
It is signed
If you set a signing key, every request carries X-SilentFailure-Signature: sha256=… over the timestamp and the raw body, and X-SilentFailure-Timestamp. Without it you cannot tell our alert from one somebody else sent to your endpoint — and an alert endpoint anyone can fire is a way to hide the real alert in noise.
Resolved is a new message
Most receivers cannot edit a message they already got, so "resolved" arrives as its own request with kind: "resolved". It is only sent if the "fired" one was.
Retries come from your group
Attempts and backoff are the notification group's own. A gateway that is down for a moment must not cost the alert.
Where we will connect, and where we will not
A webhook URL is the first field in the product that decides where the central makes an outbound connection. It is fenced, and the fence is worth knowing about before you point it somewhere.
| RULE | WHY |
|---|
| https only | An alert over plain http can be read and changed in transit — and it carries a signature meant to prove the message is ours. |
| The name is resolved, and every address is checked | Reading the string is not enough: a public-looking name can point at 10.0.0.5, and it resolves again on the next call. An A record with two answers, one of them private, is still a way in. |
| No redirects | A 302 would otherwise be the way around everything above. |
| Private addresses are refused by default | On a self-hosted central you can allow them with ALERT_WEBHOOK_ALLOW_PRIVATE, because you own that network. Loopback and the cloud metadata address stay refused either way. |
The one that is easy to get wrong
Do not route alerts through what you monitor
A Gotify in a container on the host that just died is down exactly when the alert matters, and Home Assistant on the same NAS is the same problem with a nicer interface. Keep at least one channel that does not depend on the machines being watched.