Alerts & channels

Where a detected incident actually goes: email, Slack, or your own webhook — and how to keep the noisy ones quiet without going blind.

A detector firing becomes an incident. An incident becomes a message somewhere you actually look. That second step is what alert channels configure.

Channels are per project

Each project routes its own alerts. One client’s incidents never land in another client’s Slack, which is the whole point when you run agents for a book of clients: one project per client, one Slack channel per project.

Add one from Alerts on the project, or from a coding agent with rendfly_add_alert_channel (email only over MCP — a Slack or webhook URL is a secret and does not belong in a chat transcript).

ChannelAvailable onWhat arrives
Emailevery tier, including FreeThe incident, its evidence, and a link to the trace
Slackevery tier, including FreeSame, as a message to the webhook’s channel
WebhookPro and upA JSON POST to your endpoint
WhatsAppTeam and upA short message to a number you own

What a webhook receives

An HTTPS POST, Content-Type: application/json, user agent rendfly-alerts/1:

{
  "event": "alert.opened",
  "project": "Support Bot",
  "trigger_kind": "retry_storm",
  "opened_at": "2026-08-06T09:20:01Z",
  "summary": { "count_below": 3, "window_size": 20, "threshold": 0.2 },
  "dashboard_url": "https://rendfly.com/app/workspaces/.../incidents/..."
}

event is one of alert.opened, alert.resolved, or alert.test. The payload never carries conversation contentsummary holds numeric aggregates only. To read the run itself, follow dashboard_url or pull the trace over MCP.

Verifying the signature

Turn on signing when you create the webhook and rendfly returns a signing secret once. Every request then carries:

X-Rendfly-Signature: sha256=<hex>
X-Rendfly-Timestamp: <unix seconds>

The HMAC is computed over "<timestamp>.<raw body>" with SHA-256 — the timestamp is inside the signed material, so a captured request cannot be replayed with a fresh one. Verify against the raw body before parsing it, and reject anything with a timestamp outside your tolerance window:

import hashlib, hmac, time

def verify(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:      # 5-minute tolerance
        return False
    expected = hmac.new(
        secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature)

Destination URLs are validated against SSRF at creation and again at send time: HTTPS only, no private or link-local address ranges, and redirects are refused rather than followed.

Delivery

Alerts are queued, not sent inline with detection: the sweep that raises an incident writes the deliveries, and the dispatcher sends them. A failing destination is retried up to five attempts before the delivery is left failed — a Slack outage delays your alert, it does not lose it.

Use Send test on a channel to prove the path end to end. It sends an alert.test event through the real delivery machinery, so a green test means the real thing will arrive too.

When an alert is noise

Some incidents are correct and uninteresting. An agent that legitimately retries trips retry_storm. A nightly batch job trips cost_spike. The detector is right; the behaviour is normal for that agent.

Mute the incident instead of resolving it. A mute covers the whole grouping key — project, detector, failure class — so the same shape of incident stays quiet in the future. Occurrences keep being recorded and the incident is still readable; only the alerts stop. Unmute puts it back.

That distinction matters:

  • Resolve — “this was a real problem and I fixed it.” If it fires again, the incident reopens as regressed.
  • Mute — “this is normal here.” Nothing pages you again for this shape.

Neither one turns a detector off. There is no per-project detector toggle, on purpose: a detector you disabled six months ago is a failure you stopped being able to see.

Updated 2026-08-06