Skip to main content

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.

EventFires when
cert.issuedA certificate is issued for the first time.
cert.renewedA renewal completes successfully.
cert.renewal_failedA renewal attempt fails.
cert.expiring_soonA certificate crosses the expiry warning window.
cert.expiredA certificate has expired.
cert.revokedA certificate is revoked from CertAutoPilot.
cert.revocation_detectedThe CA revoked a certificate out-of-band (detected by the revocation sweep).
cert.revocation_failedA revocation attempt failed terminally at the CA — the certificate may still be trusted.
distribution.successA distribution completes successfully (one event per distribution, including fan-out).
distribution.failedA distribution fails or completes partially.
distribution.rollback_successA distribution rollback succeeds.
distribution.rollback_failedA distribution rollback fails (or cannot be enqueued).
job.failedA background job exhausts its retries.
domain.expiring_soonA tracked domain registration is close to expiry.
domain.expiredA tracked domain registration has expired.
domain.dangling_dnsDNS for a tracked domain points at a resource that no longer exists (takeover risk).
domain.dnssec_missingA tracked domain has no DNSSEC at the registrar.
domain.no_dmarcA tracked domain has no DMARC policy.
approval.requestedAn operation requiring approval is requested.
approval.approvedAn approval request is approved.
approval.rejectedAn approval request is rejected.
approval.executedAn approved operation is executed.
approval.expiredAn approval request expires unanswered.
discovery.new_certA discovery scan finds a certificate it has not seen before.
discovery.cert_changedA discovery scan sees a different certificate on a known endpoint.
discovery.endpoint_goneA previously-scanned endpoint stops answering.
Discovery events

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.

Plain HTTP is allowed — deliberately

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. warning filters out info noise.
  • 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:

EventBucket
cert.expiring_soonone per day
domain expiry / DNS-health checksone per week (or per day for dangling DNS)
job.failedone per job type per hour
approval eventsone 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.

See also