> ## Documentation Index
> Fetch the complete documentation index at: https://docs.use.observer/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook payload reference

> The JSON body, headers, and event types a generic webhook receives.

Webhooks are created on the **Alerts** page as a **Generic webhook**
destination. See [Slack, Discord, Teams, and webhooks](https://docs.use.observer/docs/notifications/chat-and-webhooks.md#generic-webhook)
for setup. Every delivery is a POST with a JSON body.

## Headers

```text theme={null}
Content-Type: application/json
User-Agent: observer-notify/1.0
X-Observer-Signature:        sha256=<hex>    (when a signing secret is set)
X-Observer-Signature-Legacy: sha256=<hex>    (when a signing secret is set)
X-Observer-Timestamp:        <unix-millis>   (when a signing secret is set)
```

## Body

```json theme={null}
{
  "event": "incident.published",
  "event_id": "<uuid>",
  "incident_id": "<uuid>",
  "org_id": "org_...",
  "page": {
    "id": "<uuid>",
    "name": "Acme Cloud",
    "subdomain": "acme"
  },
  "title": "Investigating: Elevated errors on checkout",
  "body": "We are investigating elevated error rates.",
  "severity": "major",
  "status": "investigating",
  "url": "https://acme.use.observer/...",
  "occurred_at": "<ISO8601>"
}
```

| Field | Meaning |
| - | - |
| `event` | The event type. See [Event types](#event-types). |
| `event_id` | Stable id for this event. Retries reuse it. |
| `incident_id` | The durable entity the event belongs to: the incident or maintenance id, the metric id for metric events, the burn id for SLO events, or the statement id for SLA statement events. `null` for agent and digest events. |
| `org_id` | Your organization id. |
| `page` | The status page for incident and maintenance events. For other events, `page.name` carries the subject (the agent, metric, SLO, or customer name) and `page.subdomain` is empty. |
| `title`, `body` | Human-readable summary and detail. |
| `severity` | `minor`, `major`, `critical`, `maintenance`, or `null`. |
| `status` | Event-specific state, described per event below. |
| `url` | A link to the incident, metric, or console page, when one applies. |
| `occurred_at` | When the event happened. |

## 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.

| Event | Fires when | `status` |
| - | - | - |
| `incident.published` | An incident is published on a status page. | `investigating` |
| `incident.message_added` | An update is posted on a published incident. | The update's message type, for example `Identified`. |
| `incident.resolved` | An incident is resolved. | `resolved` |
| `maintenance.scheduled` | A maintenance window is published. | `scheduled` |
| `maintenance.starting_soon` | A scheduled maintenance is about to start. | `starting` |
| `maintenance.completed` | A maintenance window completes. | `completed` |
| `metric.status_changed` | A metric's status changes. | The new status: `healthy`, `degraded`, `unhealthy`, or `no_data`. A resolve reads `healthy` (see below). |
| `metric.no_data` | A metric stops reporting. Carries the reason in `body`. | `no_data` |
| `agent.offline` | An agent stops sending heartbeats, and again when it is back. | `open` or `cleared` |
| `agent.lag_high` | An agent falls behind on collection, and again when it catches up. | `open` or `cleared` |
| `agent.uptime_degraded` | An agent's uptime degrades, and again when it recovers. | `open` or `cleared` |
| `agent.duplicate_key` | Two agent processes share one key, and again once the stale one goes quiet. | `open` or `cleared` |
| `check.unmeasurable` | No probe location can measure a hosted check because the target answers with a bot-protection challenge page, and again when a real result arrives. | `open` or `cleared` |
| `slo.burn_started` | An SLO starts burning error budget. | `open` |
| `slo.burn_resolved` | The burn ends. | `cleared` |
| `sla_statement.ready_for_review` | A customer's SLA statement is ready for review. | `in_review` |
| `sla_statement.delivered` | An SLA statement is delivered to the customer. | `delivered` |
| `alert.digest` | Once a day, for destinations with the daily digest turned on. `body` lists the batched alerts. | `null` |

### 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:

```text theme={null}
X-Observer-Signature:        sha256=<hex>
X-Observer-Signature-Legacy: sha256=<hex>
X-Observer-Timestamp:        <unix-millis>
```

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.

```js Node.js theme={null}
import crypto from "node:crypto";

// rawBody: the request body as a string, exactly as received.
export function verifyObserver(rawBody, headers, secret) {
  const ts = headers["x-observer-timestamp"];
  const sig = headers["x-observer-signature"] ?? "";
  if (!ts || Math.abs(Date.now() - Number(ts)) > 5 * 60_000) return false;
  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  const a = Buffer.from(sig);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

> [!NOTE]
>
> **Idempotency**
>
> Use `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`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.