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

# Payload reference

> The exact JSON Tallwatch POSTs to your webhook URL. Every field and type, with an example for each event.

This is the canonical webhook payload: the body you receive when no Handlebars template is set. If you've templated the body, this is still the variable set you interpolate from.

## Headers

Every POST carries:

| Header                  | Value                                                                      | Notes                                                                         |
| ----------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `Content-Type`          | `application/json`                                                         | `text/plain` instead if your template renders a non-JSON body                 |
| `User-Agent`            | `Tallwatch-Webhook/1.0`                                                    | Stable string for receiver-side allowlisting                                  |
| `X-Tallwatch-Event`     | `incident.opened`, `incident.acknowledged`, `incident.resolved`, or `test` | Mirrors `event` in the body                                                   |
| `X-Tallwatch-Timestamp` | Unix seconds at signing time                                               | Also embedded in the signature; use it to reject replays                      |
| `X-Tallwatch-Signature` | `t=<ts>,v1=<hmac>`                                                         | HMAC-SHA256 over the timestamp and raw body. See [Signing](/webhooks/signing) |

## Top-level body

```json theme={null}
{
  "event": "incident.opened",
  "occurred_at": "2026-06-02T14:23:01.829Z",
  "org": { },
  "monitor": { },
  "incident": { },
  "check": { }
}
```

<ResponseField name="event" type="string" required>
  `incident.opened`, `incident.acknowledged`, `incident.resolved`, or `test`. Always present.
</ResponseField>

<ResponseField name="occurred_at" type="string (ISO 8601)" required>
  When the event happened, UTC, millisecond precision.
</ResponseField>

<ResponseField name="org" type="object" required>
  The workspace that owns the monitor.
</ResponseField>

<ResponseField name="monitor" type="object" required>
  The monitor the event is about. Always present. For `test`, it's a synthetic demo monitor.
</ResponseField>

<ResponseField name="incident" type="object | null">
  The incident this event belongs to. Present on `incident.opened`, `incident.acknowledged`, and `incident.resolved`. `null` on `test`.
</ResponseField>

<ResponseField name="check" type="object | null">
  The latest check result behind the event. Present on incident events, `null` on `test`.
</ResponseField>

## `org`

```json theme={null}
"org": {
  "id": "9b8c7d6e-3a4b-5c6d-7e8f-9a0b1c2d3e4f",
  "name": "Acme Corp",
  "slug": "acme"
}
```

<ResponseField name="org.id" type="string (UUID)" required>
  Stable identifier for the workspace.
</ResponseField>

<ResponseField name="org.name" type="string" required>
  The workspace name as shown in the dashboard.
</ResponseField>

<ResponseField name="org.slug" type="string" required>
  The URL-safe slug. Resolves to `<slug>.tallwatch.com` for the public status page.
</ResponseField>

## `monitor`

```json theme={null}
"monitor": {
  "id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "name": "API healthcheck",
  "type": "http",
  "url": "https://api.acme.com/health"
}
```

<ResponseField name="monitor.id" type="string (UUID)" required>
  Stable identifier for the monitor. Use this to correlate the opened and resolved events for one outage.
</ResponseField>

<ResponseField name="monitor.name" type="string" required>
  The display name the user gave the monitor.
</ResponseField>

<ResponseField name="monitor.type" type="string" required>
  One of `http`, `tcp`, `ping`, `dns`, `ssl`, `keyword`, `heartbeat`.
</ResponseField>

<ResponseField name="monitor.url" type="string">
  Present for `http` and `keyword` monitors. Omitted for the others, which carry no URL. For protocol-specific details on other types, query the API rather than expecting them here.
</ResponseField>

## `incident`

```json theme={null}
"incident": {
  "id": "4720133",
  "status": "open",
  "opened_at": "2026-06-02T14:21:30.500Z",
  "acknowledged_at": null,
  "resolved_at": null,
  "duration_sec": null,
  "failing_regions": ["fra1", "iad1", "sgp1"]
}
```

<ResponseField name="incident.id" type="string" required>
  Identifies this specific incident event. The opened and resolved events for one outage carry different ids, so use it as a dedup key, not to correlate the two. The same id is never delivered to the same channel twice.
</ResponseField>

<ResponseField name="incident.status" type="string" required>
  `open`, `acknowledged`, or `resolved`. Tracks `event`: `open` on `incident.opened`, `acknowledged` on `incident.acknowledged`, `resolved` on `incident.resolved`.
</ResponseField>

<ResponseField name="incident.opened_at" type="string (ISO 8601)" required>
  When the incident first opened. Present on both events.
</ResponseField>

<ResponseField name="incident.acknowledged_at" type="string (ISO 8601) | null">
  When a user acknowledged in the dashboard, or `null` if no one has yet. Set on the `incident.acknowledged` event and carried through on the resolved event.
</ResponseField>

<ResponseField name="incident.resolved_at" type="string (ISO 8601) | null">
  When the incident resolved. `null` on `incident.opened`, set on `incident.resolved`.
</ResponseField>

<ResponseField name="incident.duration_sec" type="number | null">
  Seconds between `opened_at` and `resolved_at`. `null` on `incident.opened`.
</ResponseField>

<ResponseField name="incident.failing_regions" type="string[]">
  Regions that voted the monitor down when the event fired. Codes are `fra1`, `iad1`, `hil1`, `sgp1`, `syd1`, `gru1`. Empty on `incident.resolved`.
</ResponseField>

## `check`

```json theme={null}
"check": {
  "last_response_ms": 8123,
  "last_error_class": "timeout",
  "last_error_message": "Request timed out after 8s"
}
```

<ResponseField name="check.last_response_ms" type="number | null">
  Latency of the latest probe, in milliseconds, or `null` if none recorded.
</ResponseField>

<ResponseField name="check.last_error_class" type="string | null">
  A stable error category like `timeout`, `dns_resolve`, `tls_cert_invalid`, or `http_status`. `null` on resolve.
</ResponseField>

<ResponseField name="check.last_error_message" type="string | null">
  A human-readable detail string. `null` on resolve.
</ResponseField>

## Example: `incident.opened`

```json theme={null}
{
  "event": "incident.opened",
  "occurred_at": "2026-06-02T14:23:01.829Z",
  "org": {
    "id": "9b8c7d6e-3a4b-5c6d-7e8f-9a0b1c2d3e4f",
    "name": "Acme Corp",
    "slug": "acme"
  },
  "monitor": {
    "id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "name": "API healthcheck",
    "type": "http",
    "url": "https://api.acme.com/health"
  },
  "incident": {
    "id": "4720133",
    "status": "open",
    "opened_at": "2026-06-02T14:21:30.500Z",
    "acknowledged_at": null,
    "resolved_at": null,
    "duration_sec": null,
    "failing_regions": ["fra1", "iad1", "sgp1"]
  },
  "check": {
    "last_response_ms": 8123,
    "last_error_class": "timeout",
    "last_error_message": "Request timed out after 8s"
  }
}
```

## Example: `incident.resolved`

```json theme={null}
{
  "event": "incident.resolved",
  "occurred_at": "2026-06-02T14:38:47.012Z",
  "org": {
    "id": "9b8c7d6e-3a4b-5c6d-7e8f-9a0b1c2d3e4f",
    "name": "Acme Corp",
    "slug": "acme"
  },
  "monitor": {
    "id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "name": "API healthcheck",
    "type": "http",
    "url": "https://api.acme.com/health"
  },
  "incident": {
    "id": "4720208",
    "status": "resolved",
    "opened_at": "2026-06-02T14:21:30.500Z",
    "acknowledged_at": null,
    "resolved_at": "2026-06-02T14:38:47.012Z",
    "duration_sec": 1037,
    "failing_regions": []
  },
  "check": {
    "last_response_ms": 214,
    "last_error_class": null,
    "last_error_message": null
  }
}
```

## Example: `test`

Fires when you click **Send test alert** on the channel form. The monitor is a synthetic demo, and `incident` and `check` are `null`, so your receiver can branch on `event === "test"` and short-circuit.

```json theme={null}
{
  "event": "test",
  "occurred_at": "2026-06-02T14:00:00.000Z",
  "org": {
    "id": "9b8c7d6e-3a4b-5c6d-7e8f-9a0b1c2d3e4f",
    "name": "Acme Corp",
    "slug": "acme"
  },
  "monitor": {
    "id": "00000000-0000-0000-0000-000000000000",
    "name": "Test alert from Acme Corp",
    "type": "http",
    "url": "https://example.com"
  },
  "incident": null,
  "check": null
}
```

## Deduplicating on your side

`alert_dispatches` guarantees Tallwatch sends each `(incident_event_id, channel_id)` once. But an inline retry after your receiver times out mid-response can still land the same POST twice, so make your receiver idempotent.

* **Cheap**: keep the last N `incident.id` values in a TTL'd set and reject repeats.
* **Proper**: upsert on `incident.id` so any number of retries converge to one row.

The proper option is the right shape for production.

## With a Handlebars template

The body becomes whatever your template renders. The variables mirror this shape (`{{monitor.name}}`, `{{incident.failing_regions}}`, and so on). See [Templates](/webhooks/templates).
