Docs

Monitoring and change alerts

AppSafe compares every scan of a verified site with the previous one and tells you what changed. With scheduled scans or re-scan on deploy, that turns into ongoing monitoring with alerts by email, Slack, Discord or a signed webhook.

Change alerts are included in the Pro, Agency and Team plans. Scheduled scans run daily or weekly on Pro and Agency, and hourly on Team.

What AppSafe compares

Alerts are sent for scheduled and on-deploy scans, once per issue: an issue that stays the same won't alert again on the next scan. If it goes away and comes back, you'll be alerted again.

Set up alerts

Open a verified site in your dashboard and add channels under Monitoring & alerts. Each channel has a minimum severity and a Send test alert button.

Webhook payload

Generic webhooks receive this JSON (Content-Type: application/json). Test alerts use the event monitoring.test and have scan set to null.

{
  "event": "monitoring.changes",
  "deliveryId": "5f2c9a0e41b7d3a8c6e1f0b2",
  "createdAt": "2026-10-26T12:00:00.000Z",
  "site": {
    "domain": "myapp.com"
  },
  "scan": {
    "id": "3f1c2b9e-1111-4222-8333-944455556666",
    "grade": "C",
    "score": 67,
    "reportUrl": "https://appsafe.dev/report/…"
  },
  "summary": "2 changes on myapp.com",
  "changes": [
    {
      "type": "header_removed",
      "severity": "high",
      "subject": "content-security-policy",
      "message": "content-security-policy header removed"
    },
    {
      "type": "finding_new",
      "severity": "critical",
      "subject": "baas-supabase-table-readable",
      "title": "Supabase tables readable without logging in",
      "message": "New issue: Supabase tables readable without logging in"
    }
  ]
}

Change types: finding_new, finding_resolved, header_added, header_removed, header_weakened, header_changed, tls_changed, tls_expiring (with days), script_domain_new, sri_missing, subdomain_new, subdomain_dangling, dns_changed. Severities: low, medium, high, critical. Alerts only include regressions, so finding_resolved and header_added appear in the dashboard and report but don't trigger alerts on their own.

Verify the signature

Every webhook request has an X-AppSafe-Signature header. Compute an HMAC-SHA256 with your channel secret over the timestamp, a dot and the raw request body, and compare it to v1 in constant time. Reject requests whose timestamp is more than 5 minutes old.

X-AppSafe-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
X-AppSafe-Event: monitoring.changes | monitoring.test
import { createHmac, timingSafeEqual } from "node:crypto";

// header: the X-AppSafe-Signature value, e.g. "t=1761480000,v1=5d41…"
// body:   the raw request body (string), before JSON.parse
export function verifyAppSafeSignature(secret, header, body) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const timestamp = Number(parts.t);
  if (!parts.v1 || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${timestamp}.${body}`).digest();
  const given = Buffer.from(parts.v1, "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

AppSafe sends each alert once and doesn't retry. Respond with a 2xx status within 8 seconds. The dashboard shows the channel's last delivery error.

What alerts never contain

Alert payloads contain change types, severities, finding titles, header names, script and subdomain hostnames, and a link to the report. They never contain finding details, secrets (not even redacted ones) or personal data.