Notifications
26 subscribable event types across Email, Slack, Microsoft Teams and generic webhooks. Per-event templates with Go variables, delivery via the job queue, and per-project routing.
Event catalog
Every notifiable event has a stable name. Subscribe channels to event names with optional severity filters.
| Event | Fires when |
|---|---|
cert.issued | A certificate is issued for the first time. |
cert.renewed | A renewal completes successfully. |
cert.renewal_failed | A renewal attempt fails. |
cert.expiring_soon | A certificate crosses the expiry warning window. |
cert.expired | A certificate has expired. |
cert.revoked | A certificate is revoked from CertAutoPilot. |
cert.revocation_detected | The CA revoked a certificate out-of-band (detected by the revocation sweep). |
cert.revocation_failed | A revocation attempt failed terminally at the CA — the certificate may still be trusted. |
distribution.success | A distribution completes successfully (one event per distribution, including fan-out). |
distribution.failed | A distribution fails or completes partially. |
distribution.rollback_success | A distribution rollback succeeds. |
distribution.rollback_failed | A distribution rollback fails (or cannot be enqueued). |
job.failed | A background job exhausts its retries. |
domain.expiring_soon | A tracked domain registration is close to expiry. |
domain.expired | A tracked domain registration has expired. |
domain.dangling_dns | DNS for a tracked domain points at a resource that no longer exists (takeover risk). |
domain.dnssec_missing | A tracked domain has no DNSSEC at the registrar. |
domain.no_dmarc | A tracked domain has no DMARC policy. |
approval.requested | An operation requiring approval is requested. |
approval.approved | An approval request is approved. |
approval.rejected | An approval request is rejected. |
approval.executed | An approved operation is executed. |
approval.expired | An approval request expires unanswered. |
discovery.new_cert | A discovery scan finds a certificate it has not seen before. |
discovery.cert_changed | A discovery scan sees a different certificate on a known endpoint. |
discovery.endpoint_gone | A previously-scanned endpoint stops answering. |
Discovery also surfaces its results as findings; the three
discovery.* events above additionally fire during scans and can be subscribed to in
rules like any other event.
Channels
Configure under Settings → Notifications → Channels.
- Email — SMTP host, port, STARTTLS / TLS, auth. Per-recipient lists.
- Slack — incoming webhook URL. Bot OAuth tokens are not supported.
- Microsoft Teams — Workflow URL (Power Automate) or legacy connector.
- Webhook — any endpoint that accepts a JSON POST. Optional HMAC signing
(
X-Signature-256), bearer auth, and custom headers. Distinct from the Webhook distribution target, which delivers certificates — this channel delivers event notifications.
Webhook delivery format
Every delivery is a POST with Content-Type: application/json:
{
"event_type": "cert.renewal_failed",
"severity": "critical",
"subject": "Certificate renewal failed: example.com",
"message": "Renewal for example.com failed: ...",
"project_id": "…",
"certificate_id": "…",
"timestamp": "2026-08-17T10:00:00Z",
"data": { "domain": "example.com", "error": "…" }
}
Any 2xx response counts as delivered; a 4xx (other than 429) is treated as
permanent and not retried; 429/5xx/transport errors get up to 3 delivery attempts in total.
Redirects are never followed — auth headers must not travel to a location a
receiver chooses.
Headers on every request: User-Agent: CertAutoPilot/1.0, X-Timestamp
(RFC3339), your custom headers (Host, proxy and hop-by-hop headers are ignored), and — when a
signing secret is configured — X-Signature-256: sha256=<hex> computed as
HMAC-SHA256 of the raw request body. Verify it like a GitHub webhook:
const crypto = require('node:crypto');
const expect = 'sha256=' + crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expect), Buffer.from(req.headers['x-signature-256']));
Once set, the signing secret and bearer token cannot be cleared in place — a blank value on edit means "keep the stored one" (the same rule every masked field follows). To turn signing or bearer auth off, recreate the channel without them.
Unlike Slack/Teams, a webhook channel URL may use http:// so internal
receivers work without TLS. On plain HTTP the signing secret's signature,
bearer token and payload travel unencrypted on your network — prefer HTTPS
whenever the receiver supports it. Link-local and cloud-metadata addresses are
always refused, and URLs may not embed credentials.
Toggling a channel off stops all delivery through it — rules pointing at a disabled channel are skipped (recorded in History as channel is disabled), even if the rules themselves stay enabled.
Templates
Each event has a built-in default template; org-level custom templates can override it per channel type. Variables are exposed via Go templating as flat, pre-formatted strings — for example:
Subject: Certificate Expiring Soon: {{.primary_domain}}
<p>Certificate <b>{{.primary_domain}}</b> is expiring soon.</p>
<p>Expires: {{.expires_at}}</p>
<p>Certificate: {{.certificate_id}}</p>
{{.event_type}}, {{.certificate_id}}, and {{.job_id}} are available on every event; the rest ({{.primary_domain}}, {{.error}}, {{.expires_at}}, {{.domain_name}}, …) vary per event. No template functions are registered, and missing variables render as empty strings. Custom templates are per channel type (email HTML vs Slack/Teams plain text). See Notification templates for the full per-event variable reference.
Routing
A rule binds one or more event types to a channel:
- Severity at least (
severity_min) — e.g.warningfilters outinfonoise. - Project — rules are project-scoped; a rule only matches events from its project.
- Recipients — set on the rule, not the channel. For SMTP rules at least one address is required (the channel config carries none), each must be a valid address, and the list is capped at 50.
- Message Template (optional) — a specific template to render this rule's events. Leave empty to use the channel's default / built-in per-event message. Its channel type must match the rule's channel. Assigning different templates to different rules is how you get different messages for different events.
Multiple rules can match the same event; each fires independently (so avoid unintended overlap). A rule listing several events renders one template for all of them — split into separate rules, or guard event-specific variables with {{if}}, when the messages should differ.
Delivery & retries
Each matched rule enqueues a notification.send job. Delivery is retried by the job
queue (up to 3 attempts with backoff); after the final failure the job is marked
failed. Sent and failed deliveries surface under Settings → Notifications →
History with their status and last error — there is no separate dead-letter buffer
beyond the job queue itself. A delivery to a channel that has since been disabled is
recorded with status skipped. History rows are kept for 90 days.
Why a persistent condition only alerts once
Deliveries are de-duplicated. Before sending, the job reserves a history row keyed on (rule, certificate, event type, time bucket) behind a unique index, so a second event falling in the same bucket is dropped rather than delivered. The bucket depends on the event:
| Event | Bucket |
|---|---|
cert.expiring_soon | one per day |
| domain expiry / DNS-health checks | one per week (or per day for dangling DNS) |
job.failed | one per job type per hour |
| approval events | one per request |
So a certificate that stays inside its expiry window alerts once a day, not once per scheduler sweep. If you expect an alert and do not get one, check History for an existing row in the current bucket before suspecting the channel.
Testing a channel
Each channel has a Send test button — fires a fixture event and reports the channel's response. Always click it after creating or editing a channel.