Notifications

Triggers

TriggerWhen
New issueThe first event of a new issue.
RegressionA resolved issue gets a new event (see Issues).
UnmuteA mute expires or its volume threshold is hit.
SpikeThis 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.

Email

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:

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.