# Alert channels

A channel is a destination for alerts. cronpad supports email and webhooks; webhook channels can format their body for Slack or Discord incoming webhooks, or send a generic signed JSON payload for your own systems. Every channel has a **Send test** button (`POST /api/v1/alert-channels/{id}/test`).

## Email

```json
{"kind": "email", "name": "On-call", "config": {"to": ["oncall@example.com"]}}
```

- Verification links expire after 24 hours. Use **Resend verification** (`POST /api/v1/alert-channels/{id}/verify`) if a link expires. Changing the recipient list requires every address to verify again.
- Every address must be verified: cronpad emails a confirmation link, and the channel only sends after it's clicked. This prevents cronpad from being used to email people who didn't ask for it.
- Remote SMTP servers must support STARTTLS; loopback SMTP relays may use plaintext.
- Self-hosted servers need SMTP settings in [`[alerts.email]`](../self-hosting/configuration.md#alertsemail).

## Slack and Discord

Create an incoming webhook in Slack or Discord, then:

```json
{"kind": "webhook", "name": "#ops", "config": {"url": "https://hooks.slack.com/services/T000/B000/XXXX", "format": "slack"}}
```

Use `"format": "discord"` for Discord webhook URLs. Messages include the event, job name, run time, attempts, last error and a link to the run in the dashboard.

## Generic webhook

```json
{"kind": "webhook", "name": "PagerDuty bridge", "config": {"url": "https://alerts.example.com/cronpad", "format": "generic"}}
```

cronpad sends a `POST` with this body:

```json
{
  "event": "run.failed",
  "occurred_at": "2026-11-10T03:31:12Z",
  "workspace_id": "ws_01J9X…",
  "job": {"id": "job_01J9ZK3Q…", "name": "nightly-report", "tags": ["reports"]},
  "run": {
    "id": "run_01J9ZM1A…",
    "scheduled_at": "2026-11-10T03:30:00Z",
    "status": "failed",
    "attempts": 3,
    "last_error": "HTTP 503: upstream unavailable"
  },
  "suppressed_count": 0,
  "url": "https://app.cronpad.com/runs/run_01J9ZM1A…"
}
```

Requests are signed exactly like job webhooks (Standard Webhooks headers), with a per-channel signing secret shown when you create the channel. See [Verify signatures](../sinks/webhooks.md#verify-signatures).

## Delivery

- Deliveries use a stable `webhook-id` across retries. Receivers should deduplicate this ID: a restart after delivery but before recording success can repeat a request.
- Alerts are sent from a durable outbox and retried with backoff up to 8 times if the channel is down.
- `suppressed_count` tells you how many identical alerts were held back by the rule's cooldown since the last one was sent.