Notifications
Triggers
| Trigger | When |
|---|---|
| New issue | The first event of a new issue. |
| Regression | A resolved issue gets a new event (see Issues). |
| Unmute | A mute expires or its volume threshold is hit. |
| Spike | This hour has at least 20 events and more than 10× the median of the previous 24 hourly counts. At most once per issue per 6 hours. |
Each person gets at most one email and one push per issue per trigger per hour.
SMTP settings, the Amazon SES preset, test emails and the send log are on Email. Email also powers invites and password resets.
Preferences
Each user picks email and push per trigger in Settings → Notifications, for all projects or per project. A project setting beats the all-projects one. With no setting, both are on.
The same thing over the API:
curl -X PUT https://errors.example.com/api/v1/me/notifications \
-H "Authorization: Bearer of_..." -H "Content-Type: application/json" \
-d '[{"project_id": null, "kind": "new_issue", "email": true, "push": false}]'
kind is new_issue, regression, unmuted or spike. The PUT replaces all your rows.
Webhooks
Set per project in Settings → Projects → Webhooks. Six kinds:
- Slack: an incoming-webhook URL. Sent as blocks.
- Discord: a channel webhook URL. Sent as an embed.
- Mattermost: an incoming-webhook URL. Sent as
textplus one attachment (title link, then level, events, users and release fields). - Teams: a Teams Workflows (Power Automate) “post to a channel when a webhook request is received” URL. Sent as an Adaptive Card 1.4 with an Open issue button.
- Telegram:
https://api.telegram.org/bot<bot token>/sendMessage?chat_id=<chat id>. The bot token stays in the URL; thechat_idis taken out of the URL on save and stored separately. Sent as HTML with link previews off. - Generic: JSON to any HTTPS URL.
Each has a test button and shows its last error. Failed deliveries retry 3 times with backoff and stay in the project’s delivery log for 24 hours.
Webhook URLs must be https:// and must not point at private, local or loopback addresses (see Security). Set OPENFAULT_ALLOW_PRIVATE_WEBHOOKS=1 to allow them.
Generic payload
{
"kind": "new_issue",
"project": "web",
"issue": {
"id": 42, "title": "TypeError: x is undefined", "level": "error",
"url": "https://errors.example.com/issues/42",
"events": 12, "users": 3, "first_seen": 1760900000, "last_seen": 1760900400,
"release": "1.4.0"
}
}
Signing
Give a webhook a secret and every request carries a timestamp and an HMAC over it plus the body:
X-OpenFault-Timestamp: <unix seconds>
X-OpenFault-Signature: sha256=<hex HMAC-SHA256(secret, "{timestamp}.{raw body}")>
Verify over the timestamp header, a ., and the raw body bytes. Reject requests whose timestamp is more than about 5 minutes old, so a captured request cannot be replayed.
import crypto from "node:crypto";
function verify(req, rawBody, secret) {
const ts = req.headers["x-openfault-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const mac = crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const want = Buffer.from(`sha256=${mac}`);
const got = Buffer.from(req.headers["x-openfault-signature"] ?? "");
return got.length === want.length && crypto.timingSafeEqual(got, want);
}
Push
See Push & iOS app.