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
- New and resolved findings.
- Security headers that were added, removed or weakened (for example a removed Content-Security-Policy or a shorter HSTS max-age).
- TLS certificate replacements, and expiry warnings 14 and 3 days before the certificate expires.
- New third-party script domains, and external scripts without Subresource Integrity.
- New subdomains of your domain in Certificate Transparency logs (crt.sh), and subdomains whose CNAME points to a deleted resource on a hosting service. These use passive DNS lookups only.
- SPF, DMARC, CAA and MX record changes.
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.
- Email: Any email address. Alerts are written in your account's language.
- Slack: An incoming webhook URL from a Slack app (https://hooks.slack.com/services/…).
- Discord: A channel webhook URL (Server settings → Integrations → Webhooks).
- Webhook: Any https endpoint. AppSafe POSTs the JSON payload below, signed with a secret shown once when you create the channel.
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.testimport { 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.