Skip to main content
Webhooks are created on the Alerts page as a Generic webhook destination. See Slack, Discord, Teams, and webhooks for setup. Every delivery is a POST with a JSON body.

Headers

Body

Event types

Each destination chooses which of these it receives. Agent health, hosted check, SLO, and SLA Ledger events are only delivered to org-wide destinations.

Metric alerts and resolves

A metric alert that went out is always closed by a resolve, so an incident opened downstream (PagerDuty, Jira, your own system) never stays open.
  • A resolve goes only to destinations that received the alert. Nobody gets “recovered” for an alert they never saw.
  • Resolves pass every filter, snooze and page binding, so what was opened can close.
  • A resolve’s status is healthy and its severity is null, including when an outage ended in no data.
  • The title says why it closed: recovered, or alert closed: followed by deleted, paused (plan), monitoring changed, agent removed, removed from page or no data.
  • An alert can be sent again with the same metric when it is still true after a snooze ends or monitoring resumes. The body says why, for example “Still unhealthy after the snooze ended.”

Signature verification

When a signing secret is configured, every delivery carries three headers:
The primary X-Observer-Signature signs the timestamp and the raw body together: hex is HMAC-SHA-256(secret, "${timestamp}.${rawBody}"), where timestamp is the value in X-Observer-Timestamp. Recompute this on the receiving side and compare in constant time. Reject deliveries whose signatures do not match, and reject deliveries whose timestamp is more than five minutes from the receiver’s clock to bound replay. X-Observer-Signature-Legacy signs the body only: HMAC-SHA-256(secret, rawBody). It is deprecated and exists for receivers that have not migrated to the timestamp-prefixed scheme. Verify against X-Observer-Signature for new receivers. Hash the raw request bytes, not a re-serialized copy of the parsed JSON.
Node.js
IdempotencyUse event_id from the body as the idempotency key when persisting events. Retried deliveries reuse the same id, so a unique-key check prevents double-processing.

Legacy webhook subscriptions

Webhook subscriptions created before Alerts existed keep delivering. They can no longer be created or edited; new webhooks are added on the Alerts page. A legacy subscription receives the event’s raw JSON rather than the body above: event_type, event_id, occurred_at, org_id, and event-specific fields. Incident and maintenance events also repeat those fields under data. Requests are signed with the same three headers. A legacy subscription whose URL is a Slack incoming webhook receives a Slack-formatted message instead. For metric.status_changed, old_status and new_status are the raw statuses the check recorded. old_verdict and new_verdict are the statuses after the metric’s no-data treatment, which is what the status page shows. A resolve carries resolve: true. When it closed for a reason other than recovery it also carries resolve_reason (for example metric_deleted) or cause (for example snooze_ended). Legacy subscriptions can carry events that Alerts does not deliver, such as incident.created, incident.updated, incident.deleted, maintenance.started, maintenance.canceled, page.status_changed, and the auto-incident events below.

incident.auto_drafted

Observer created a draft incident from an unhealthy metric change. The draft is not visible on the status page until it is published. Fields: org_id, incident_id, metric_id, metric_title, severity, trigger_reason, value, threshold, affected_service_ids, url.

incident.auto_published

An auto-drafted incident was published from the email link. incident.published fires for the same publish. Fields: org_id, incident_id, title, severity, state, published_at, affected_service_ids, url.

incident.auto_dismissed

An auto-drafted incident was dismissed from the email link, or expired after 24 hours with no action. reason is operator_dismiss or auto_expired. Fields: org_id, incident_id, title, dismissed_at, reason.