---
title: "Webhook-Payload-Referenz"
description: "Das exakte JSON, das Uptimeify an einen generischen Webhook-Kanal sendet, die mitgeschickten Request-Header und die Prüfung der HMAC-Signatur."
---

Ein **Webhook**-Benachrichtigungskanal stellt jeden Alarm, jede Entwarnung, jede Erinnerung und
jede Warnung eines Kunden an einen HTTP-Endpoint zu, den du kontrollierst. Diese Seite beschreibt
die Nutzlast des generischen Webhook-Kanals: den JSON-Body, die Request-Header und die
Signaturprüfung.

<Callout type="info">
  Chat- und On-Call-Kanäle (Slack, Discord, Microsoft Teams, PagerDuty, Opsgenie, …) senden
  stattdessen ein Format, das der jeweilige Anbieter erwartet. Der hier dokumentierte Body ist das,
  was ein **Webhook**-Kanal ohne eigenes Body-Template sendet.
</Callout>

## Der Request

| Eigenschaft | Wert |
|---|---|
| Methode | `POST` (je Kanal einstellbar) |
| Content-Type | `application/json` |
| Zeitlimit | 30 Sekunden (je Kanal einstellbar) |

Ein Endpoint, der wiederholt nicht einmal eine Verbindung annimmt, bekommt pro Versuch ein
kürzeres Zeitlimit. Die Zustellung wird dadurch nicht eingestellt, nur der Preis eines
scheiternden Versuchs sinkt.

### Request-Header

| Header | Bedeutung |
|---|---|
| `User-Agent` | Weist die Anfrage als Uptimeify-Monitoring aus |
| `X-Webhook-Timestamp` | Zeitpunkt, zu dem der Request gebaut wurde, ISO 8601 (UTC) |
| `X-Webhook-Attempt` | Versuchszähler dieser Benachrichtigung, beginnend bei `1` |
| `X-Webhook-Signature` | HMAC-Signatur des Bodys, nur wenn ein Secret hinterlegt ist |
| `X-Webhook-Signature-Algorithm` | `sha256`, wird zusammen mit der Signatur gesendet |

Eigene Header, die am Kanal konfiguriert sind, werden ergänzt und stechen die Vorgaben.

## Signatur prüfen

Hinterlegst du am Kanal ein Secret, signieren wir **die exakten Bytes des Request-Bodys** mit
HMAC-SHA256 und schicken das Ergebnis hex-kodiert in `X-Webhook-Signature`. Prüfe gegen den rohen
Body, bevor du ihn parst und neu serialisierst, sonst kippt der Vergleich schon an einem
Leerzeichen.

```js
import crypto from 'node:crypto'

function isValidSignature(rawBody, headerValue, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
  const a = Buffer.from(expected, 'utf8')
  const b = Buffer.from(String(headerValue ?? ''), 'utf8')
  // Längenprüfung zuerst: timingSafeEqual wirft bei ungleicher Länge.
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}
```

## Nutzlast

```json
{
  "type": "alert",
  "check_degraded": false,
  "incident_id": 8094,
  "message": "Website Acme Shop is DOWN",
  "expected_response_error": null,
  "ssl": null,
  "website": {
    "id": 10597,
    "name": "Acme Shop",
    "url": "https://shop.example.com",
    "custom_fields": {}
  },
  "customer": {
    "id": 42,
    "name": "Acme GmbH",
    "email": "ops@example.com",
    "custom_fields": {}
  },
  "incident": {
    "type": "http_status",
    "started_at": "19.08.2026, 17:32:04",
    "resolved_at": "N/A",
    "status": "open",
    "ssl": null,
    "error_details": {
      "check_degraded": false,
      "status_code": 503,
      "error_message": "Unexpected status code (503) (expected: 200)",
      "expected_response_error": null,
      "response_time_ms": 412,
      "blocking_response": {
        "status_code": 503,
        "vendor": "Cloudflare",
        "headers": {
          "server": "cloudflare",
          "cf-ray": "a2daec8259183caa-CDG"
        }
      }
    },
    "locations": ["de-fra", "fi-hel", "pl-waw"],
    "screenshot": null,
    "screenshot_url": "https://…",
    "diagnostics": null
  }
}
```

### Felder auf oberster Ebene

| Feld | Typ | Anmerkung |
|---|---|---|
| `type` | string | `alert`, `recovery`, `reminder`, `warning`, `unstable` oder `slow` |
| `check_degraded` | boolean | `true`, wenn die Prüfung zu keinem Urteil kam, im Unterschied zu einem bestätigten Ausfall |
| `incident_id` | number | Bleibt über Alarm, Erinnerung und Entwarnung hinweg gleich |
| `message` | string | Eine lesbare Zeile, direkt weiterverwendbar |
| `expected_response_error` | string \| null | Bequeme Kopie von `incident.error_details.expected_response_error` |
| `ssl` | object \| null | Zertifikatsdetails, nur bei SSL-Vorfällen gesetzt |
| `website` | object | `id`, `name`, `url`, `custom_fields` |
| `customer` | object | `id`, `name`, `email`, `custom_fields` |
| `incident` | object | Siehe unten |

### `incident`

| Feld | Typ | Anmerkung |
|---|---|---|
| `type` | string | Siehe Vorfalltypen unten |
| `started_at` | string | `TT.MM.JJJJ, HH:MM:SS` in `Europe/Berlin`, sonst `"N/A"` |
| `resolved_at` | string | Gleiches Format, `"N/A"` solange der Vorfall offen ist |
| `status` | string | Vorfallstatus wie gespeichert, etwa `open` oder `resolved` |
| `ssl` | object \| null | Dasselbe Objekt wie `ssl` auf oberster Ebene |
| `error_details` | object | Siehe unten |
| `locations` | string[] | Die Standorte, die diese Benachrichtigung abdeckt |
| `screenshot` | string \| null | Base64-JPEG, nur wenn ein Screenshot entstanden ist |
| `screenshot_url` | string \| null | Link auf denselben Screenshot |
| `diagnostics` | object \| null | Rohdiagnose der fehlgeschlagenen Prüfung, Inhalt je nach Fehlerbild |

### `incident.error_details`

| Feld | Typ | Anmerkung |
|---|---|---|
| `check_degraded` | boolean | Derselbe Wert wie oben |
| `status_code` | number \| null | HTTP-Status; `0` heißt, es kam gar keine Antwort |
| `error_message` | string \| null | Der Fehlschlag, wie er aufgezeichnet wurde |
| `expected_response_error` | string \| null | Gesetzt, wenn eine „Erwartete Antwort"-Regel gerissen ist |
| `response_time_ms` | number \| null | Antwortzeit der fehlgeschlagenen Prüfung |
| `blocking_response` | object \| null | Wer die Anfrage abgewiesen hat, siehe unten |

### `incident.type`

`http_status`, `downtime` (wird als `down` ausgeliefert), `response_time`, `https_redirect`,
`keyword_check`, `page_size`, `ssl_handshake`, `ssl_expiry`, `ssl_warning`, `domain_expiry`,
`domain_expiry_warning`, `dns`.

Bei einem Dienst-Monitor wird `downtime` stattdessen als Protokoll ausgeliefert: `icmp`, `smtp`,
`ssh`, `ftp` oder `imap-pop`.

## `blocking_response`: wer die Anfrage abgewiesen hat

Ein `403`, `406`, `429` oder `503` sagt, dass etwas die Anfrage abgelehnt hat. Es sagt nicht, ob
deine Seite aus ist oder ob eine Web Application Firewall unsere Prüfung aussortiert hat, bevor sie
deinen Server erreichte. Trägt die Antwort Header, die den Absender benennen, reichen wir sie in
diesem Feld durch. Bei jedem anderen Status ist es `null`, ebenso wenn die Antwort niemanden
benannt hat.

| Feld | Typ | Anmerkung |
|---|---|---|
| `status_code` | number | Der Status, der die Aufzeichnung ausgelöst hat |
| `vendor` | string \| null | Benannter Anbieter, etwa `Cloudflare`, `Sucuri`, `Imperva`, `DataDome`, `Akamai`, `Amazon CloudFront`, `Fastly`, `Bunny`, `Varnish`, `Google`. `null`, wenn kein Header einen nennt, wir raten nicht |
| `headers` | object | Headername auf Wert, Namen kleingeschrieben |

Die aufgezeichneten Header sind eine feste Liste: `server`, `via`, `retry-after`, `x-cache`,
`x-served-by`, `cf-ray`, `cf-cache-status`, `cf-mitigated`, `x-sucuri-id`, `x-sucuri-block`,
`x-iinfo`, `x-cdn`, `x-akamai-transformed`, `akamai-grn`, `x-amz-cf-id`, `x-amz-cf-pop`,
`x-datadome`, `x-request-id`, `x-waf-event-id`, `cdn-pullzone`, `cdn-requestid`. Cookies sowie
Anmeldeheader werden nie aufgezeichnet und nie versendet, und jeder Wert wird bei 200 Zeichen
gekappt.

Damit lassen sich zwei Lagen unterscheiden, die am Statuscode allein gleich aussehen:

```js
const blocked = payload.incident?.error_details?.blocking_response
if (blocked) {
  // Unsere Prüfung wurde am Rand abgewiesen, der Ursprung kann völlig gesund sein.
  notifyOps(`${blocked.vendor ?? 'Ein Edge'} hat die Prüfung mit ${blocked.status_code} abgewiesen`)
}
```

## Bedingungen und Body-Templates

Ein Webhook-Kanal kann Bedingungen tragen, dann wird nur zugestellt, wenn die Nutzlast dazu passt,
und ein eigenes JSON-Body-Template, dann ersetzt dein Template den oben beschriebenen Body. Beides
stellst du am Kanal in der App ein.
