Uptimeify Docs

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

EigenschaftWert
MethodePOST (je Kanal einstellbar)
Content-Typeapplication/json
Zeitlimit30 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

HeaderBedeutung
User-AgentWeist die Anfrage als Uptimeify-Monitoring aus
X-Webhook-TimestampZeitpunkt, zu dem der Request gebaut wurde, ISO 8601 (UTC)
X-Webhook-AttemptVersuchszähler dieser Benachrichtigung, beginnend bei 1
X-Webhook-SignatureHMAC-Signatur des Bodys, nur wenn ein Secret hinterlegt ist
X-Webhook-Signature-Algorithmsha256, 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

FeldTypAnmerkung
typestringalert, recovery, reminder, warning, unstable oder slow
check_degradedbooleantrue, wenn die Prüfung zu keinem Urteil kam, im Unterschied zu einem bestätigten Ausfall
incident_idnumberBleibt über Alarm, Erinnerung und Entwarnung hinweg gleich
messagestringEine lesbare Zeile, direkt weiterverwendbar
expected_response_errorstring | nullBequeme Kopie von incident.error_details.expected_response_error
sslobject | nullZertifikatsdetails, nur bei SSL-Vorfällen gesetzt
websiteobjectid, name, url, custom_fields
customerobjectid, name, email, custom_fields
incidentobjectSiehe unten

incident

FeldTypAnmerkung
typestringSiehe Vorfalltypen unten
started_atstringTT.MM.JJJJ, HH:MM:SS in Europe/Berlin, sonst "N/A"
resolved_atstringGleiches Format, "N/A" solange der Vorfall offen ist
statusstringVorfallstatus wie gespeichert, etwa open oder resolved
sslobject | nullDasselbe Objekt wie ssl auf oberster Ebene
error_detailsobjectSiehe unten
locationsstring[]Die Standorte, die diese Benachrichtigung abdeckt
screenshotstring | nullBase64-JPEG, nur wenn ein Screenshot entstanden ist
screenshot_urlstring | nullLink auf denselben Screenshot
diagnosticsobject | nullRohdiagnose der fehlgeschlagenen Prüfung, Inhalt je nach Fehlerbild

incident.error_details

FeldTypAnmerkung
check_degradedbooleanDerselbe Wert wie oben
status_codenumber | nullHTTP-Status; 0 heißt, es kam gar keine Antwort
error_messagestring | nullDer Fehlschlag, wie er aufgezeichnet wurde
expected_response_errorstring | nullGesetzt, wenn eine „Erwartete Antwort"-Regel gerissen ist
response_time_msnumber | nullAntwortzeit der fehlgeschlagenen Prüfung
blocking_responseobject | nullWer 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.

FeldTypAnmerkung
status_codenumberDer Status, der die Aufzeichnung ausgelöst hat
vendorstring | nullBenannter Anbieter, etwa Cloudflare, Sucuri, Imperva, DataDome, Akamai, Amazon CloudFront, Fastly, Bunny, Varnish, Google. null, wenn kein Header einen nennt, wir raten nicht
headersobjectHeadername 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.

Auf dieser Seite