Webhooks
Schick jeden Alarm als JSON an deinen eigenen Endpunkt. Ein Webhook ist ein Benachrichtigungskanal wie Slack oder E-Mail — alles, was du an einen Kanal routen kannst, kannst du auch an einen Webhook routen: Incidents, Warnungen, DNS-Änderungen, Zertifikatserneuerungen und sicherheitsrelevante Kontoereignisse.
Nicht die Reseller-Webhooks
Diese Seite beschreibt den Vertrag für Kundinnen und Kunden. Hoppla hat zusätzlich ein Reseller-Webhook-System mit eigenem Katalog — team.*, member.*, email.*-Unterdrückung — beschrieben unter Reseller-Webhooks. Diese Events gehen ausschließlich an Reseller-Tarife, und die Reseller-API ebenso. Alles auf DIESER Seite funktioniert in jedem kostenpflichtigen Tarif.
Einrichten
- Einstellungen → Benachrichtigungskanäle → Kanal hinzufügen, Typ Webhook.
- Trag die URL ein, die den POST empfangen soll.
- Optional ein HMAC-Secret setzen. Tu es — ohne Secret kannst du nicht nachweisen, dass eine Anfrage wirklich von Hoppla kam.
- Test senden schickt sofort eine echte Anfrage, damit du den Endpunkt prüfen kannst, bevor ein Incident davon abhängt.
- Richte eine Benachrichtigungsregel auf den Kanal, um auszuwählen, welche Events er bekommt.
Die URL muss öffentlich auflösbar sein. Private, Link-Local- und Loopback-Adressen werden beim Speichern und noch einmal bei der Zustellung abgelehnt — Hoppla ruft weder localhost noch 10.0.0.0/8 oder 169.254.0.0/16 auf. Für lokale Tests brauchst du also einen Tunnel.
Die Anfrage
POST, Content-Type: application/json, 10 Sekunden Timeout.
Nur ein 2xx gilt als zugestellt. Alles andere ist ein Fehlschlag — ein 4xx, ein 5xx oder eine Weiterleitung, der wir nicht folgen — ebenso ein Timeout oder ein Verbindungsfehler.
Was danach passiert, hängt vom Code ab. Ein 5xx, 429, 408 oder 425 sowie jeder Verbindungsfehler gelten als vorübergehendes Problem auf deiner Seite und werden wiederholt: drei Versuche nach 10 s, 60 s und 300 s. Jeder andere 4xx sagt, dass die Anfrage selbst falsch war — ein 404 für eine umgezogene URL, ein 422 für eine von deinem Handler abgelehnte Nachricht — und wird nicht wiederholt, weil die nächsten beiden Versuche identisch wären. So oder so wird die Zustellung protokolliert, samt dem Status deines Endpunkts, unter Benachrichtigungen → Protokoll. Jede Zeile dort hat einen Erneut senden-Knopf: er wiederholt die Zustellung mit derselben X-Webhook-Delivery-ID. Hat dein Endpunkt den ersten Versuch schon verarbeitet, erkennt er die Wiederholung und verwirft sie. Den Knopf zu drücken, wenn du unsicher bist, ob etwas angekommen ist, ist also unbedenklich.
Jede Anfrage trägt einen X-Webhook-Delivery-Header, im Body wiederholt als delivery_id. Er identifiziert eine Zustellung und ändert sich bei Wiederholungen nicht — genau darauf dedupliziert man. Siehe Mehrfache Zustellung.
Der User-Agent nennt die sendende Plattform, etwa Hoppla-Webhook/1.0. Auf einem White-Label-Portal steht dort stattdessen dessen Marke.
Der Body ist ein flaches JSON-Objekt — keine verschachtelten monitor- oder incident-Objekte — mit alphabetisch sortierten Schlüsseln. Jede Nachricht enthält event und timestamp, der Rest hängt vom Event ab.
{
"check_type": "uptime",
"delivery_id": "018f3c2a-7b41-7c3e-9a55-2f9b1d4e6a10",
"event": "incident.created",
"incident_id": "9f1c…",
"monitor_id": "3ab7…",
"monitor_name": "example.com",
"monitor_url": "https://example.com",
"severity": "critical",
"started_at": "2026-03-04T12:00:00+00:00",
"status": "down",
"timestamp": "2026-03-04T12:00:03+00:00",
"title": "HTTP 503 Service Unavailable"
}Die Felder stammen aus einer festen Allowlist, damit eine Nachricht nie mehr aus deinem Konto mitnimmt, als das Event braucht: incident_id, monitor_id, monitor_name, monitor_url, title, status, severity, started_at, resolved_at, check_type, event_type, timestamp, zone, nameservers, changes, nameserver_change, nameservers_changed, ssl_issuer, ssl_valid_from, ssl_valid_to, response_time_ms, status_code, cleared_warning, brand_name. security.*-Events haben einen eigenen, kuratierten Payload und enthalten nie Zugangsdaten — nur den Namen dessen, was sich geändert hat.
Signatur prüfen
Hat der Kanal ein Secret, signiert Hoppla die Anfrage:
X-Webhook-Signature: <hex>Bis zum 28. September 2026 trugen Zustellungen denselben Wert zusätzlich in X-Hoppla-Signature (und X-Hoppla-Timestamp). Dieses Paar ist entfernt. Liest dein Empfänger es noch, lies stattdessen X-Webhook-Signature (und X-Webhook-Timestamp); der Wert ist identisch.
Die Signatur ist HMAC-SHA256 über den rohen Request-Body, hex-kodiert:
signature = hmac_sha256(secret, roher_request_body)Signiere die empfangenen Bytes, nicht ein neu serialisiertes Objekt. Hoppla sendet das JSON mit nicht-escapten Schrägstrichen und sortierten Schlüsseln; Parsen und neu Kodieren verändert die Bytes und die Signatur passt dann nicht mehr.
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, $secret);
if (! hash_equals($expected, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}import { createHmac, timingSafeEqual } from 'node:crypto'
// express.raw({ type: 'application/json' }) — req.body muss ein Buffer sein
const expected = createHmac('sha256', secret).update(req.body).digest('hex')
const got = req.get('x-webhook-signature') ?? ''
const ok = expected.length === got.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(got))
if (!ok) return res.sendStatus(401)import hmac, hashlib
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Webhook-Signature", "")):
abort(401)Vergleiche in konstanter Zeit (hash_equals, timingSafeEqual, compare_digest), nicht mit ==.
Events
Jedes Event unten lässt sich an einen Webhook-Kanal routen. Dieselbe Liste steht im Produkt unter Developers und wird aus derselben Quelle erzeugt wie diese Seite.
certificate
| Event | Bedeutung |
|---|---|
certificate.renewed | Ein TLS-Zertifikat wurde durch ein neues ersetzt. |
dns
| Event | Bedeutung |
|---|---|
dns.records_changed | Ein überwachter DNS-Eintrag hat seinen Wert geändert. |
incident
| Event | Bedeutung |
|---|---|
incident.created | Für einen Monitor wurde ein Incident eröffnet. |
incident.resolved | Ein offener Incident wurde geschlossen und die Entwarnung verschickt. |
monitor
| Event | Bedeutung |
|---|---|
monitor.flapping | Ein Monitor hat in kurzer Zeit wiederholt den Zustand gewechselt. |
monitor.flapping_resolved | Ein flappender Monitor hat sich beruhigt. |
security
| Event | Bedeutung |
|---|---|
security.mfa_enabled | Ein Mitglied hat Zwei-Faktor-Authentifizierung aktiviert. |
security.mfa_disabled | Ein Mitglied hat Zwei-Faktor-Authentifizierung deaktiviert. |
security.recovery_codes_regenerated | Ein Mitglied hat neue Recovery-Codes erzeugt; die alten sind ungültig. |
security.mfa_reset | Eine Administratorin hat die Zwei-Faktor-Authentifizierung eines Mitglieds zurückgesetzt. |
security.passkey_added | Ein Mitglied hat einen Passkey registriert. |
security.passkey_removed | Ein Passkey wurde von einem Konto entfernt. |
security.password_changed | Ein Mitglied hat sein Passwort geändert. |
security.email_changed | Die E-Mail-Adresse eines Mitglieds wurde geändert. |
security.api_token_created | Ein API-Token wurde ausgestellt. |
security.api_token_revoked | Ein API-Token wurde widerrufen. |
security.session_revoked | Eine Sitzung wurde aus der Ferne abgemeldet. |
security.login_new_device | Eine Anmeldung kam von einem Gerät, das dieses Konto noch nie benutzt hat. |
security.account_blocked | Ein Konto wurde gesperrt und kann sich nicht mehr anmelden. |
security.account_unblocked | Ein gesperrtes Konto wurde wiederhergestellt. |
security.role_changed | Die Rolle eines Mitglieds hat sich geändert und damit seine Rechte. |
security.auth_policy_changed | Die Authentifizierungs-Richtlinie des Teams hat sich geändert (SSO, MFA-Pflicht). |
security.account_deleted | Ein Konto wurde gelöscht. Für die DSGVO-Löschung siehe member.erased. |
storm
| Event | Bedeutung |
|---|---|
storm.detected | Viele Monitore sind gleichzeitig ausgefallen — meist gemeinsame Infrastruktur, nicht viele Einzelfehler. |
storm.resolved | Ein erkannter Sturm ist vorüber. |
warning
| Event | Bedeutung |
|---|---|
warning.ssl_expiring | Ein TLS-Zertifikat läuft demnächst ab. |
warning.domain_expiring | Eine Domain-Registrierung läuft demnächst ab. |
warning.degraded_performance | Die Antwortzeiten liegen dauerhaft über dem Schwellwert des Monitors. |
warning.slow_response | Ein einzelner Check hat den Schwellwert für langsame Antworten überschritten. |
warning.application_health | Ein Application-Health-Endpunkt meldet ein Problem. |
warning.sitemap_issues | Die Sitemap ist nicht erreichbar, fehlerhaft oder enthält tote URLs. |
warning.security_headers | Erwartete Security-Header fehlen oder wurden abgeschwächt. |
warning.redirect_chain | Eine Redirect-Kette ist zu lang, läuft im Kreis oder fällt auf HTTP zurück. |
warning.robots_txt | robots.txt hat sich geändert, fehlt oder blockiert jetzt das Crawling. |
warning.dnsbl_listing | Der Host steht auf einer DNS-Blockliste. |
warning.broken_links | Ein Crawl hat Links gefunden, die nicht mehr auflösen. |
warning.mixed_content | Eine HTTPS-Seite lädt Unterressourcen über unverschlüsseltes HTTP. |
warning.server_resources | Ein Agent-Monitor meldet Druck auf CPU, Speicher oder Festplatte. |
warning.cleared | Eine zuvor gemeldete Warnung besteht nicht mehr. |
Mehrfache Zustellung
Die Zustellung ist at-least-once. Ein langsamer Endpunkt, der am Ende doch antwortet, hat den ersten Versuch womöglich schon verarbeitet — dasselbe Event kann also berechtigterweise zweimal ankommen.
Dedupliziere auf X-Webhook-Delivery (im Body delivery_id). Der Wert gehört zu genau einer Zustellung und ist bei jeder Wiederholung derselbe. Es genügt also, die bereits verarbeiteten zu merken und Wiederholungen zu überspringen:
$id = $request->header('X-Webhook-Delivery');
if (! Cache::add("webhook:{$id}", true, now()->addDay())) {
return response()->noContent(); // schon verarbeitet
}Eine Wiederholung benutzt dieselbe ID, ein wirklich neues Event bekommt eine neue. Nimm nicht incident_id plus event als Schlüssel: ein Monitor, der sich erholt und erneut ausfällt, erzeugt zwei echte incident.created-Events, die du beide willst.
Wissenswertes
- Die Reihenfolge ist nicht garantiert. Zwei Events aus derselben Sekunde können in beliebiger Reihenfolge ankommen. Verlass dich auf
timestamp, nicht auf den Eingang. - Antworte schnell. Das Timeout liegt bei 10 Sekunden; nimm die Arbeit in eine Queue und antworte sofort mit 2xx, statt inline zu verarbeiten.
- Ein Secret ist optional, ohne eines ist nichts prüfbar. Dann trägt die Anfrage gar keine Signatur, und wer die URL kennt, kann darauf posten.
Einschränkungen
Jede Zustellung ist ein POST mit Content-Type: application/json. Die Methode lässt sich nicht ändern, eigene Header lassen sich nicht ergänzen — bewusst: die Anfrage authentifiziert sich über ihre HMAC-Signatur, nicht über etwas, das man in einen Header einträgt.
Steck auch kein Token in die URL. Die URL des Kanals wird allen im Team, die den Kanal lesen dürfen, vollständig angezeigt — sie ist kein Ort für ein Geheimnis. Nimm die Signatur von oben.