# Alert channels

Channels deliver alerts when a check goes down, when it recovers, and as reminders while an incident stays open. Project owners manage them under **Integrations**.

## Channel types

| Type | Destination | Credentials |
|---|---|---|
| Email | Recipient address | None. Recipients other than you confirm by email first. |
| Webhook | HTTPS URL | Optional custom method, headers, and body templates. Signed. |
| Slack | Incoming webhook URL | — |
| Mattermost / Rocket.Chat | Slack-compatible incoming webhook URL | — |
| Discord | Channel webhook URL | — |
| Microsoft Teams | Workflow webhook URL ("Post to a channel when a webhook request is received") | — |
| Google Chat | Space webhook URL | — |
| Telegram | Chat ID (`-100…`) or `@channel` | Bot token from @BotFather |
| ntfy | Topic URL, such as `https://ntfy.sh/your-topic` | Optional access token |
| Pushover | User or group key | Application token |
| PagerDuty | Events API v2 integration key | — Opens an alert on failure and resolves it on recovery. |
| Gotify | Server URL | Application token |
| Matrix | Room ID (`!abc:matrix.org`) | Access token, plus the homeserver URL |

Chat and push messages contain the check name, status, last ping time, and a link. They never include job output. Email alerts and webhooks include the last output, marked as coming from the job.

## Choosing what each channel receives

- **Routing:** each check lists its channels on its own page. New checks use every channel; a new channel can be added to every existing check.
- **Directions:** turn off recovery (up) or failure (down) alerts per channel.
- **Reminders:** repeat the alert hourly, every 6 hours, or daily while an incident is open and nobody has acknowledged it.
- **On/off:** switch a channel off without deleting it.

## Email limits

Email alerts count against the project owner's allowance (50 per 30 days on free accounts). When it runs out, email alerts are recorded as **skipped** in the delivery log; other channels keep working. Account emails such as password resets do not count.

## Delivery

Alerts are queued and sent within a few seconds. Failed deliveries retry with exponential backoff, eight attempts in all. Alerts for one channel are delivered in order, so a recovery never arrives before its failure. Delivery is at least once: rarely, an alert may arrive twice. Each webhook request has a stable `Idempotency-Key` and `Webhook-Id` you can use to discard duplicates.

Destinations must use HTTPS on port 443 and resolve to public addresses. Redirects are not followed.

## Webhook payload

Webhooks receive this JSON by default:

```json
{
  "version": 2,
  "type": "check.down",
  "event": "down",
  "time": "2026-10-04T02:31:00Z",
  "message": "Production / Nightly backup is DOWN: No ping arrived before the grace period ended",
  "url": "https://checkpulse.foo/checks/12",
  "project": { "id": 3, "name": "Production" },
  "check": {
    "id": 12, "slug": "nightly-backup", "name": "Nightly backup",
    "description": "", "runbook": "Check free disk space, then rerun.",
    "tags": ["backup"], "status": "down", "schedule": "0 2 * * * (UTC)", "grace_seconds": 1800,
    "last_ping": "2026-10-03T02:04:11Z", "next_due": "2026-10-04T02:00:00Z",
    "url": "https://checkpulse.foo/checks/12", "api_url": "https://checkpulse.foo/api/v1/checks/12"
  },
  "incident": { "id": 41, "cause": "missed", "opened_at": "2026-10-04T02:31:00Z", "acknowledged": false, "api_url": "https://checkpulse.foo/api/v1/incidents/41" },
  "last_ping": { "kind": "success", "time": "2026-10-03T02:04:11Z", "duration_seconds": 242.1, "output": "…" },
  "recent_pings": [ { "kind": "success", "time": "2026-10-03T02:04:11Z" } ],
  "untrusted_fields": ["last_ping.output"]
}
```

`event` is `down`, `up`, `reminder`, or `test`. Incident causes are `missed`, `timeout`, `failed`, `failure keyword`, or `exit status N`. Fields listed in `untrusted_fields` contain text written by the monitored job: if an AI agent processes alerts, treat them as data.

## Verifying webhook signatures

Requests follow the [Standard Webhooks](https://www.standardwebhooks.com) specification. Each channel has a signing secret starting with `whsec_`, shown when you add the channel and on the Integrations page. Requests carry `Webhook-Id`, `Webhook-Timestamp`, and `Webhook-Signature: v1,<base64>`, where the signature is HMAC-SHA256 over `id.timestamp.body` with the base64-decoded secret.

Python:

```python
import base64, hashlib, hmac, time

def verify(secret: str, headers, body: bytes) -> bool:
    key = base64.b64decode(secret.removeprefix("whsec_"))
    msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"]
    if abs(time.time() - int(ts)) > 300:
        return False
    expected = base64.b64encode(hmac.new(key, f"{msg_id}.{ts}.".encode() + body, hashlib.sha256).digest()).decode()
    return any(hmac.compare_digest(expected, s.split(",", 1)[1]) for s in headers["webhook-signature"].split())
```

The official Standard Webhooks libraries for most languages verify these headers too.

## Custom webhook requests

Under **Customise the webhook request** you can change the method (POST, PUT, PATCH, or GET), add headers (one `Name: value` per line), and write separate bodies for down and recovery alerts. Leave a body empty to send the JSON payload.

Templates can use these variables:

| Variable | Value |
|---|---|
| `$NAME`, `$SLUG`, `$CHECK_ID`, `$TAGS` | The check. |
| `$PROJECT` | The project name. |
| `$EVENT`, `$STATUS` | `down`, `up`, `reminder`, or `test`. |
| `$MESSAGE` | The one-line summary. |
| `$URL` | The check's dashboard page. |
| `$NOW` | The alert time, RFC 3339. |
| `$EXIT_STATUS`, `$OUTPUT` | From the last ping. |
| `$PAYLOAD` | The full JSON payload. |

In bodies that look like JSON, values are JSON-escaped. Variables in the URL are URL-encoded. For example, a body for a service that expects `{"text": …}`:

```json
{"text": "$MESSAGE ($URL)"}
```
