Triggers
Per-monitor automations that call external systems on status transitions or payload matches. Outbound calls are validated and capped to prevent unsafe targets and runaway loops.
TRIGGER TYPES
- Webhook: fixed JSON schema with HMAC-SHA256 signature headers. Pick this when the receiver expects KEEPitALIVE format.
- HTTP Request: fully custom method, headers, and body template with placeholder variables. Pick this for bespoke integrations.
- Notify: sends a notification through your configured alert channels (Email, Discord, Slack, etc.) with a custom title/body template.
- Incident: opens an outage/degraded incident when a payload condition matches, then auto-resolves it when a later event clears the condition (or when you disable/delete the trigger). A fully silent source will not auto-resolve - see Conditions. The incident notifies like any other: everyone on the monitor is reached through their own channels and destinations, and each person can mute trigger notifications per channel in their notification settings.

EVENTS
Nine events: down, recovered, degraded, flapping_start, flapping_stop, maintenance_start, maintenance_end, signal, and payload. signal fires for Event-Receiver heartbeat beats. payload fires for checks that captured a response payload, especially API monitors.

TEMPLATE VARIABLES
Use these in HTTP Request body/headers/URL and Notify title templates:
{{monitor.id}} {{monitor.ref}} {{monitor.name}} {{monitor.url}} {{monitor.type}}
{{event.id}} - stable per-fire id (same value across the retry; use it to dedupe)
{{event.type}} {{event.status}} {{event.latency_ms}}
{{event.status_code}} {{event.error}} {{event.timestamp}}
{{check.id}} - id of the originating check (omitted for synthetic events)
{{payload}} - full check payload as raw string
{{payload.KEY}} - dot-path into the JSON payload (e.g. {{payload.status}})Note: monitor.id and monitor.ref both resolve to the public monitor reference - never an internal database id. Key your downstream systems on it.
{
"source": "keepitalive",
"monitor": "{{monitor.name}}",
"event": "{{event.type}}",
"status": "{{event.status}}",
"latency_ms": "{{event.latency_ms}}",
"error": "{{event.error}}",
"at": "{{event.timestamp}}"
}CONDITIONS
Triggers can gate firing with an expression against the stored check payload. Grammar: ==, !=, >, <, >=, <=, contains, exists, and, or, not, parens, dot paths. Example: payload.failed_jobs > 0 and payload.status == "error". Missing fields compare as null. A path that is not in the payload behaves as if it held null, so payload.status == null is true when status is absent - and by the same rule payload.status != "none" is ALSO true when status is absent. Write payload.status exists and payload.status != "none" when you mean the field must be present. This matters when a payload changes shape: a renamed field makes a bare != match on data that is no longer there. contains behaves the opposite way and is false on a missing path, so an AND of contains and != goes quiet instead of firing. Incident triggers require a condition. They open the incident while the expression matches and resolve it when the expression stops matching. How resolution works: the incident auto-resolves when a later check or payload re-evaluates the same trigger and the expression is now false. It also closes immediately if you disable or delete the trigger. Important: silence is not recovery. If the source stops sending payloads/heartbeats entirely, there is no event to clear the condition, so the incident stays open until a new event arrives or you resolve it manually. This is intentional - we never treat "no data" as "recovered", because for a payload/signal source silence may be the real outage.

Heartbeat / event receiver - your posted JSON stays at the top:
payload.failed_jobs, payload.status
What we observed sits under beat, so it cannot shadow yours:
payload.beat.received_at, payload.beat.exit_code,
payload.beat.duration_ms
(those three are also copied flat when you do not use the name)
API monitor - the response is wrapped:
payload.body.<what the server returned>
payload.status_code, payload.headers, payload.assertions
So an API check of a status endpoint reads
payload.body.status.indicator - not payload.status.indicator.Open degraded incident when:
payload.failed_jobs > 0 or payload.status == "error"
Auto-resolves when a later event makes that expression false,
or when you disable/delete the trigger.
A source that goes fully silent will NOT auto-resolve.WEBHOOK PAYLOAD SHAPE
Webhook triggers POST this fixed JSON body. Key your handler on event_id (dedupe) and monitor.ref (identity):
{
"event_id": "9f2c1ab4e5d6...", // stable per fire, repeats on retry
"event": "down",
"monitor": {
"id": "mon_abc123", "ref": "mon_abc123",
"name": "API", "url": "https://api.example.com", "type": "http"
},
"check": {
"id": 84213, "status": "down",
"latency_ms": 234, "status_code": 503, "error": "...",
"payload": { }
},
"timestamp": "2026-06-27T12:00:00Z"
}SIGNATURE VERIFICATION (WEBHOOK)
When a webhook trigger has a secret, each delivery carries X-Timestamp and X-Signature-256. Recompute and compare to reject forged calls:
signed = X-Timestamp + "." + raw_request_body
expected = "sha256=" + hex(hmac_sha256(secret, signed))
reject unless constant_time_equals(expected, X-Signature-256)
// optional: reject if X-Timestamp is older than a few minutesDELIVERY SEMANTICS
- Idempotency: X-KEEPitALIVE-Event-ID (also event_id in the body / {{event.id}}) is generated once per fire and reused across retries. Dedupe on it so a retry that lands after a slow first attempt is not processed twice.
- Retry policy: network triggers can retry 0-3 times with a 5-600 second delay. Notify triggers do not retry to avoid duplicate user-visible notifications.
- Redirects are NOT followed: a 3xx response counts as a delivery failure. Point the URL at the final destination.
- Loop breaker: outbound deliveries carry X-KEEPitALIVE-Depth. A beat posted back into an event-receiver propagates depth+1, and network triggers stop firing at depth 3 - so trigger -> event-receiver chains are capped at 3 hops.
INTEGRATION RECIPES
n8n / Make / Zapier: create a Webhook (n8n) or Custom Webhook catch hook (Make/Zapier), paste its URL into an HTTP Request or Webhook trigger. For Webhook triggers, verify X-Signature-256 in a Function/Code step before acting; branch on {{event.type}} and dedupe on event_id.
Method: POST
URL: https://api.cloudflare.com/client/v4/zones/<ZONE_ID>/purge_cache
Headers: Authorization: Bearer <API_TOKEN>
Content-Type: application/json
Body: { "purge_everything": true }
// Tip: gate on {{event.type}} == recovered to purge after a deploy recovers.Method: POST
URL: https://<HA_HOST>/api/webhook/<WEBHOOK_ID>
Body: { "monitor": "{{monitor.name}}", "status": "{{event.status}}",
"event": "{{event.type}}", "event_id": "{{event.id}}" }Method: POST
URL: https://ops.example.com/hooks/restart
Headers: X-Auth-Token: <TOKEN>
Body: { "service": "{{monitor.name}}", "reason": "{{event.error}}",
"ref": "{{monitor.ref}}", "event_id": "{{event.id}}" }
// Make the receiver idempotent on event_id so a retry never double-restarts.SAFETY
- Circuit breaker disables a trigger after 10 consecutive failures (owner gets an in-app notification).
- Flap policy: the same 4-in-10-min rule applies per trigger.
- Notify triggers log a per-channel delivery result (delivered / partial / failed) in the execution log.
- Webhook secret is shown once on create/rotate; it is redacted in all subsequent responses.