Signed deploy.failed / app.crashed / domain.expiring
Percher POSTs signed JSON payloads to your webhook URL when events happen. Useful for Discord/Slack bots, on-call pagers, or custom dashboards.
deploy.failed — any deploy fails (build, health, canary)app.crashed — the watchdog detects a crash (process exit / OOM)app.unhealthy — external uptime probe failed 3 times in a row (Starter+; billing)app.recovered — app responds again after an unhealthy windowdomain.expiring — custom-domain SSL cert is within 7 days of expiryapp.unhealthy is gated by a 15-minute cooldown so a flapping app doesn't pager-spam your receiver. app.recovered only fires after an unhealthy event that was actually delivered — if the matching unhealthy was suppressed by the cooldown, the recovery is suppressed too, so every recovered event you see pairs with an unhealthy event you saw.
app.crashed fires on container-level failures (the process exited); app.unhealthy fires when the app is reachable from inside the cluster but external probes can't hit it (DNS / Caddy / TLS / 5xx). They're independent — both can fire for the same incident.
Settings → Notifications → paste your receiver URL. A signing secret is generated and shown once — copy it into your receiver's PERCHER_WEBHOOK_SECRET env var. Changing the URL rotates the secret; clearing the URL clears the secret.
Every delivery carries X-Percher-Signature: sha256=<hex> — HMAC-SHA256 of ${timestamp}.${body}. Reject anything older than a few minutes for replay protection.
// Node / Bun receiver
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, req, body) {
const sig = req.headers.get("X-Percher-Signature") ?? "";
const ts = req.headers.get("X-Percher-Timestamp") ?? "";
const expected = createHmac("sha256", secret)
.update(`${ts}.${body}`)
.digest("hex");
const provided = sig.replace(/^sha256=/, "");
if (expected.length !== provided.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(provided));
}Deliveries are best-effort with a 5-second timeout. 5xx responses are logged but not retried — queue events in your receiver if you need retries or ordering guarantees.