Run alerts
Get notified when durable scans complete, fail, or find new findings compared to the previous run. Configure alerts in Settings—email and webhook channels are independent.
On this page
Overview
Run alerts fire after a durable crawl reaches a terminal state (Complete or Failed).
Ephemeral try-it scans and anonymous jobs do not send owner alerts.
- Who receives alerts — The organization owner only. Members with monitor access do not receive separate copies.
- Which runs qualify — Manual dashboard scans, schedules, and CI triggers that persist run history.
- Regressions — A scan compares against the previous run for the same monitor. The first scan has no baseline, so regression alerts wait until a prior run exists. See Baselines and diffs.
- Idempotency — At most one email per channel and one webhook POST per job, even if the API retries delivery.
Open Settings to configure email and webhook preferences.
Email notifications
In Settings → Email notifications, enter a notification address and choose which events to receive. Save before using Send test email—the test uses your saved address, not unsaved form values.
| Setting | When an email is sent |
|---|---|
| Email me when a scan finds regressions | Scan completes with new findings vs the previous run (runDiff.newFindingCount > 0) |
| Email me when a scan fails | Crawl ends in Failed status (timeout, unreachable sitemap, etc.) |
| Email me after every completed scan | Every successful completion. When regression email also applies, regression takes precedence—you receive one email per scan. |
You must enter a valid email address and enable at least one checkbox. All email flags default to off for new accounts.
Regression emails include a headline, top new findings, and a link to the scan in the dashboard. Every scan email also includes an unsubscribe link in the footer.
Unsubscribe from scan emails
Scan notification emails include a signed Unsubscribe from scan emails link in the footer.
Mail clients that support one-click unsubscribe may also show an unsubscribe action via
List-Unsubscribe headers on the message.
- What it does — Turns off all three email alert checkboxes (regressions, failed scans, and after every completion). Your notification email address stays on file; webhook alerts are unchanged.
- No sign-in required — The link opens a confirmation page in your browser. You can turn alerts back on anytime in Settings → Email notifications.
- Link validity — The link is tied to the recipient address on the email. If you change your notification email in Settings after the message was sent, use Settings to manage alerts instead—the old link may show an error.
Test emails from Send test email include the same footer and unsubscribe link as real scan notifications.
Webhook notifications
In Settings → Webhook notifications, paste an HTTPS endpoint URL, choose a payload format, and enable the events you care about. Signal Diff generates a signing secret when you first save a URL or change it—copy it immediately; it is not shown again after you leave the page.
Event types
| Setting | When a webhook is sent | X-SignalDiff-Event |
|---|---|---|
| Notify when a scan finds regressions | Scan completes with new findings vs baseline | crawl.completed |
| Notify when a run fails | Crawl ends in Failed status |
crawl.completed |
| Always notify on complete | Every successful completion (including zero new findings) | crawl.completed |
HTTP request
Signal Diff sends an HTTP POST with:
Content-Type: application/jsonX-SignalDiff-Event—crawl.completedfor real scans;testfor Send test webhookX-SignalDiff-Signature—t={unixSeconds},v1={hex}(see Verify signatures)- Body — JSON payload (format depends on your Payload format selection)
Generic JSON payload
When Generic JSON (custom endpoints) is selected, the body is a camelCase
CrawlCompletedPayload with fields such as:
{
"jobId": "abc123",
"sitemapUrl": "https://example.com/sitemap.xml",
"status": "Complete",
"totalPages": 120,
"errorCount": 0,
"warningCount": 3,
"infoCount": 8,
"completedAt": "2026-06-25T12:00:00Z",
"reportUrl": "https://signaldiff.dev/api/jobs/abc123/download",
"scanUrl": "https://signaldiff.dev/monitor/example.com?job=abc123",
"triggerKind": "Schedule",
"runDiff": {
"newFindingCount": 2,
"resolvedFindingCount": 1,
"headline": "2 new findings vs previous run",
"topNewFindings": [ /* … */ ]
}
}
CI-triggered runs may also include ci and codeChangeSummary objects.
Failed runs set status to Failed and may include errorMessage.
Signing secret lifecycle
- First save — A secret is generated and shown once in Settings.
- URL change — Saving a new URL generates a new secret.
- Rotate signing secret — Issues a new secret without changing the URL; update your receiver before rotating in production.
- Send test webhook — POSTs a sample payload using your saved URL and secret (requires save first).
Slack and Teams
For chat channels, create an incoming webhook in Slack or a connector in Microsoft Teams, then paste that URL into Settings → Webhook notifications.
Slack
- In Slack, open Apps → Incoming Webhooks (or create one from a channel's integrations).
- Copy the webhook URL (
https://hooks.slack.com/services/…). - In Signal Diff, set Payload format to Slack incoming webhook, or leave Generic JSON—Slack URLs are auto-detected at delivery.
- Enable the event checkboxes you need and save.
Slack receives a Block Kit message with scan status, site, counts, and links to view the scan or download the report.
Microsoft Teams
- In Teams, add an Incoming Webhook connector to the target channel (or use a Power Automate HTTP trigger URL).
- Copy the webhook URL (
https://….webhook.office.com/…or similar). - In Signal Diff, set Payload format to Microsoft Teams connector, or leave Generic JSON—Teams URLs are auto-detected.
- Enable events and save.
Teams receives a MessageCard-style payload with title, facts, and scan links.
Incoming webhooks do not verify X-SignalDiff-Signature; use a custom HTTPS endpoint if you need HMAC verification.
Verify signatures
Custom endpoints should verify every POST before acting on the body.
Read the raw request body as UTF-8 text, parse X-SignalDiff-Signature, and compute HMAC-SHA256 over
{t}.{rawJsonBody} using your signing secret as the UTF-8 key.
Compare the lowercase hex digest to v1. Reject requests whose timestamp t is outside your replay tolerance (for example ±5 minutes).
C# example
using System.Security.Cryptography;
using System.Text;
static bool VerifySignalDiffWebhook(
string signingSecret,
string signatureHeader,
string rawJsonBody,
TimeSpan tolerance)
{
// Header format: t=1719324000,v1=abc123...
var parts = signatureHeader.Split(',', StringSplitOptions.TrimEntries);
var timestampPart = parts.FirstOrDefault(p => p.StartsWith("t=", StringComparison.Ordinal));
var signaturePart = parts.FirstOrDefault(p => p.StartsWith("v1=", StringComparison.Ordinal));
if (timestampPart is null || signaturePart is null)
return false;
if (!long.TryParse(timestampPart["t=".Length..], out var unixSeconds))
return false;
var expectedV1 = signaturePart["v1=".Length..];
var signedPayload = $"{unixSeconds}.{rawJsonBody}";
var keyBytes = Encoding.UTF8.GetBytes(signingSecret);
var payloadBytes = Encoding.UTF8.GetBytes(signedPayload);
using var hmac = new HMACSHA256(keyBytes);
var actualV1 = Convert.ToHexString(hmac.ComputeHash(payloadBytes)).ToLowerInvariant();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(actualV1),
Encoding.UTF8.GetBytes(expectedV1)))
return false;
var age = DateTimeOffset.UtcNow - DateTimeOffset.FromUnixTimeSeconds(unixSeconds);
return age.Duration() <= tolerance;
}
Node.js example
import crypto from "node:crypto";
function verifySignalDiffWebhook(signingSecret, signatureHeader, rawJsonBody, toleranceMs = 300_000) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.trim().split("=", 2))
);
const t = Number(parts.t);
const expectedV1 = parts.v1;
if (!t || !expectedV1) return false;
const signedPayload = `${t}.${rawJsonBody}`;
const actualV1 = crypto
.createHmac("sha256", signingSecret)
.update(signedPayload, "utf8")
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(actualV1), Buffer.from(expectedV1)))
return false;
const ageMs = Math.abs(Date.now() - t * 1000);
return ageMs <= toleranceMs;
}Troubleshooting
- No regression email on the first scan
- Expected—there is no previous run to compare. Run the monitor again; the second run can trigger a regression alert.
- Saved settings but test email/webhook fails
- Click Save first. Test actions use persisted preferences, not the current form.
- Webhook returns 4xx from Slack or Teams
- Confirm the payload format matches the URL (Slack vs Teams vs generic). Try re-creating the incoming webhook URL in the chat app.
- Signature verification fails on my API
- Use the exact raw JSON body bytes from the request. Do not re-serialize parsed JSON. Confirm you copied the signing secret shown after save or rotate.
- Unsubscribe link says the link is not valid
- The token may be incomplete, expired, or no longer matches your saved notification email. Open Settings and adjust email alerts directly.
- Member ran a scan but I did not get an alert
- Alerts go to the organization owner only. Sign in as the owner account that created the organization.