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 |
|---|---|---|
| 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:
{
"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 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:
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": …}:
{"text": "$MESSAGE ($URL)"}