Webhook-Payload-Referenz
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.
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.
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.
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
{
"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:
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.