Webhooks
Empfange Echtzeit-Benachrichtigungen, wenn Teams erstellt oder gelöscht werden, Monitore sich ändern, Vorfälle in verwalteten Teams auftreten oder Plattform-E-Mails unterdrückt werden.
Einrichtung
Option A: Komfort-API (empfohlen)
Erstell ein Webhook-Abonnement in einem einzigen Aufruf:
curl -X POST https://app.hoppla.dev/api/v1/reseller/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Ops Integration",
"url": "https://your-server.com/hoppla-webhook",
"secret": "your-hmac-secret",
"events": [
"monitor.down",
"monitor.up",
"incident.created",
"incident.resolved",
"ssl.expiring",
"team.provisioned"
],
"tenant_filter": null
}'Damit werden Benachrichtigungskanal und -regel automatisch erstellt. Verwalten kannst du sie über GET/PATCH/DELETE /reseller/webhooks/{id}. Zustellprotokolle rufst du mit GET /reseller/webhooks/{id}/deliveries ab.
Option B: Manuelle Einrichtung (fortgeschritten)
Für die volle Kontrolle über die Kanaleinstellungen (Ruhezeiten, Rate-Limits, Batch-Fenster) nutzt du die Standard-Endpoints für Kanal + Regel:
1. Benachrichtigungskanal erstellen
curl -X POST https://app.hoppla.dev/api/v1/notification-channels \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Provisioning Webhooks",
"type": "webhook",
"config": {
"url": "https://your-server.com/hoppla-webhook",
"secret": "your-hmac-secret"
},
"is_enabled": true
}'2. Benachrichtigungsregel erstellen
curl -X POST https://app.hoppla.dev/api/v1/notification-rules \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"channel_id": "channel-uuid-...",
"event_types": [
"team.provisioned",
"team.deleted",
"incident.created",
"incident.resolved",
"monitor.down",
"ssl.expiring"
],
"tenant_filter": {
"tenant_ids": ["uuid1", "uuid2"],
"labels": ["production-*"]
}
}'Mandanten-Filterung
Lege fest, welche verwalteten Mandanten Events für jeden Webhook auslösen:
| Filter | Beschreibung |
|---|---|
null / weggelassen | Alle verwalteten Mandanten (Standard) |
tenant_ids | Explizite Liste von Tenant-UUIDs |
labels | Mustervergleich auf Labels verwalteter Teams (unterstützt nachgestelltes *-Wildcard) |
Filter werden mit ODER verknüpft: Ein Event passt, wenn es zu irgendeiner tenant_id ODER zu irgendeinem Label-Muster passt.
Vollständige Event-Referenz
Team-Verwaltungs-Events
| Event | Auslöser | Wichtige Payload-Felder |
|---|---|---|
team.provisioned | Team über API oder Portal-Registrierung erstellt | managed_team_id, tenant_id, tenant_name, label |
team.deleted | Team soft-gelöscht | managed_team_id, tenant_id, tenant_name |
team.paused | Monitoring für ein Team pausiert | managed_team_id, tenant_id, reason |
team.unpaused | Monitoring wieder aufgenommen | managed_team_id, tenant_id |
team.trial_ending | Testphase endet in 3 Tagen oder 1 Tag | managed_team_id, tenant_id, plan, trial_ends_at, days_remaining |
team.trial_expired | Testphase abgelaufen, Plan auf Free zurückgesetzt | managed_team_id, tenant_id, previous_plan |
tenant.quota_warning | Ein verwaltetes Team erreicht ≥80 % eines Plan-Limits (z. B. Monitore) | tenant_id, feature, current_count, limit, usage_pct |
tenant.quota_exceeded | Eine Erstellung wurde blockiert, weil ein verwaltetes Team ein Plan-Limit erreicht hat | tenant_id, feature, current_count, limit, usage_pct |
Quota-Events werden pro Team + Feature entprellt (ein
tenant.quota_warningpro Tag, eintenant.quota_exceededpro 6 Stunden), damit ein Batch-Import deinen Webhook nicht überflutet.
Monitor-Events
| Event | Auslöser |
|---|---|
monitor.created | Monitor in einem verwalteten Team erstellt |
monitor.updated | Monitor in einem verwalteten Team aktualisiert |
monitor.deleted | Monitor in einem verwalteten Team gelöscht |
monitor.down | Monitor ausgefallen (Convenience-Alias für incident.created) |
monitor.up | Monitor wiederhergestellt (Convenience-Alias für incident.resolved) |
monitor.enabled | Monitor aktiviert (war pausiert/deaktiviert) |
monitor.disabled | Monitor deaktiviert/pausiert |
monitor.flapping | Monitor flattert (wechselt schnell zwischen erreichbar/ausgefallen) |
monitor.flapping_resolved | Monitor flattert nicht mehr und hat sich stabilisiert |
ssl.expiring | SSL-Zertifikat läuft innerhalb des Schwellwerts ab (Alias für warning.ssl_expiring) |
Mitglieder-Events
| Event | Auslöser |
|---|---|
member.added | Benutzer zu einem verwalteten Team hinzugefügt |
member.removed | Benutzer aus einem verwalteten Team entfernt |
member.erased | Personenbezogene Daten eines Mitglieds nach DSGVO Art. 17 gelöscht |
member.erasedist nichtmember.removed. Entfernen gibt einen Platz frei: die Person existiert weiter und kann erneut eingeladen werden. Löschen vernichtet ihre Identität plattformweit — Login, Passkeys, Geräte-Tokens und jedes Team, dem sie angehörte, auch Teams, die du nicht verwaltest. Wenn dein CRMmember.removedals „Platz frei, Kontakt behalten" behandelt, ist das hier falsch: der Kontaktdatensatz ist genau das, was weg muss.Das Event wird ausgelöst, egal ob die Person sich selbst in ihren Kontoeinstellungen gelöscht hat oder du es in ihrem Auftrag über
POST /reseller/managed-teams/{managedTeam}/members/{userId}/erasegetan hast.initiated_byim Payload istselfoderreseller.Das Event enthält die E-Mail-Adresse bewusst — sie ist der einzige Schlüssel, über den du deine eigenen Datensätze zuordnen kannst, und dies ist die letzte Nachricht, die sie transportieren kann.
Monitoring-Events (Weitergeleitet)
Alle Monitoring-Events deiner verwalteten Teams werden automatisch an deine Benachrichtigungskanäle weitergeleitet. Du hast immer volle Sichtbarkeit.
| Kategorie | Event-Typen |
|---|---|
| Vorfälle | incident.created, incident.resolved |
| Warnungen | warning.ssl_expiring, warning.domain_expiring, warning.degraded_performance, warning.application_health, warning.sitemap_issues, warning.security_headers, warning.redirect_chain, warning.robots_txt, warning.dnsbl_listing, warning.broken_links, warning.mixed_content, warning.server_resources, warning.cleared |
| DNS | dns.records_changed, dns.nameserver_changed |
| Zertifikate | certificate.renewed |
| Storms | storm.detected, storm.resolved |
dns.nameserver_changedist ein Convenience-Alias, der zusätzlich zudns.records_changedausgelöst wird, sobald eine bestätigte DNS-Änderung auch den Satz der autoritativen Nameserver betrifft (ein Indikator für ein NS-Hijacking). Er trägt denselben Payload wiedns.records_changed. Wie alle weitergeleiteten DNS-Events wird er unabhängig vomnotify_dns_changes-Schalter des verwalteten Kunden zugestellt — schaltet ein Kunde seine eigenen DNS-Alarme stumm, bleibst du als Reseller dennoch über eine Nameserver-Änderung in deinem Bestand informiert.
E-Mail-Unterdrückungs-Events
Wenn du Plattform-E-Mails unterdrückst, sendet Hoppla email.*-Webhooks mit dem vollständigen E-Mail-Payload — damit du die E-Mails selbst versenden kannst.
| Event | Unterdrückte E-Mail |
|---|---|
email.password_reset | Wird nie ausgelöst — Passwort-Reset-Mails können nicht unterdrückt werden. |
email.email_change_verify | Wird nie ausgelöst — die Verifizierung einer Adressänderung kann nicht unterdrückt werden. |
email.email_changed_notify | Wird nie ausgelöst — die Nachricht an die alte Adresse kann nicht unterdrückt werden. |
email.invitation | Unterdrückte Team-Einladung. |
email.member_removed | Unterdrückte Nachricht über das Entfernen aus einem Team. |
email.trial_ending | Unterdrückte Erinnerung an das Ende der Testphase. |
email.trial_expired | Unterdrückte Nachricht über die abgelaufene Testphase. |
email.incident_created | Unterdrückter Alarm zu einem eröffneten Vorfall. |
email.incident_resolved | Unterdrückte Entwarnung zu einem behobenen Vorfall. |
email.warning | Unterdrückte Hinweis-Mail zu einer Warnung. |
email.dns_changed | Unterdrückte DNS-Änderungs-Mail. Kein Live-Feed für DNS-Änderungen — dafür dns.records_changed / dns.nameserver_changed abonnieren. |
email.certificate_renewed | Unterdrückte Nachricht über ein erneuertes Zertifikat. |
email.digest | Unterdrückte Sammel-Benachrichtigung. |
email.monthly_report | Unterdrückter monatlicher Monitoring-Bericht. |
email.status_page_subscription | Unterdrücktes Status-Seiten-Update an einen Abonnenten. |
email.security_mfa_enabled | Unterdrückte Nachricht, dass die Zwei-Faktor-Authentifizierung aktiviert wurde. |
email.security_mfa_disabled | Unterdrückte Nachricht, dass die Zwei-Faktor-Authentifizierung deaktiviert wurde. |
email.security_recovery_codes_regenerated | Unterdrückte Nachricht, dass die Wiederherstellungscodes ersetzt wurden und die alten ungültig sind. |
email.security_mfa_reset | Unterdrückte Nachricht, dass die Zwei-Faktor-Authentifizierung administrativ zurückgesetzt wurde. |
email.security_passkey_added | Unterdrückte Nachricht, dass ein Passkey zum Konto hinzugefügt wurde. |
email.security_passkey_removed | Unterdrückte Nachricht, dass ein Passkey vom Konto entfernt wurde. |
email.security_password_changed | Unterdrückte Nachricht, dass das Kontopasswort geändert wurde. |
email.security_email_changed | Unterdrückte Nachricht, dass die E-Mail-Adresse des Kontos geändert wurde. |
email.security_api_token_created | Unterdrückte Nachricht, dass ein API-Token ausgestellt wurde. |
email.security_api_token_revoked | Unterdrückte Nachricht, dass ein API-Token widerrufen wurde. |
email.security_session_revoked | Unterdrückte Nachricht, dass eine angemeldete Sitzung aus der Ferne beendet wurde. |
email.security_login_new_device | Unterdrückte Nachricht über eine Anmeldung von einem bisher unbekannten Gerät. |
email.security_account_blocked | Unterdrückte Nachricht, dass das Konto für Anmeldungen gesperrt wurde. |
email.security_account_unblocked | Unterdrückte Nachricht, dass eine Sperre des Kontos aufgehoben wurde. |
email.security_role_changed | Unterdrückte Nachricht, dass sich die Rolle des Kontos in einem Team geändert hat. |
email.security_auth_policy_changed | Unterdrückte Nachricht, dass sich die Anmelderichtlinie des Teams geändert hat. |
email.security_account_deleted | Unterdrückte Nachricht, dass das Konto gelöscht wurde. |
Unterdrückung braucht einen Empfänger
Eine E-Mail wird nur unterdrückt, wenn du einen aktiven Webhook-Kanal mit einer Regel auf genau dieses email.*-Event hast. Fehlt sie, geht die Plattform-E-Mail ganz normal an den Benutzer, statt ersatzlos zu verschwinden. Erst abonnieren, dann unterdrücken.
Die Auth-Klasse wird nie unterdrückt
email.password_reset, email.email_change_verify und email.email_changed_notify stehen nur der Vollständigkeit halber hier und werden nie ausgelöst. Ihre Template-Daten sind die Zugangsdaten selbst — ein funktionierender Reset- oder Verifizierungslink. Ihn an einen Reseller zuzustellen wäre eine Kontoübernahme, von der der Benutzer nie erfährt. Nutze Impersonation, wenn du auf einem verwalteten Konto handeln musst; dieser Weg wird als Impersonation protokolliert.
Payload-Beispiele
Team erstellt
{
"event": "team.provisioned",
"timestamp": "2026-01-15T10:30:00.000000Z",
"managed_team_id": "mt-uuid-...",
"tenant_id": "new-tenant-uuid-...",
"tenant_name": "Acme Corp",
"label": "Enterprise-Kunde #42"
}Testphase endet
{
"event": "team.trial_ending",
"timestamp": "2026-01-28T00:00:00.000000Z",
"managed_team_id": "mt-uuid-...",
"tenant_id": "tenant-uuid-...",
"tenant_name": "Acme Corp",
"label": "Enterprise-Kunde #42",
"plan": "pro",
"trial_ends_at": "2026-01-31T00:00:00.000000Z",
"days_remaining": 3
}Weitergeleiteter Vorfall
Weitergeleitete Monitoring-Events enthalten ein _managed_tenant-Objekt zur Identifikation des Teams:
{
"event": "incident.created",
"timestamp": "2026-01-15T14:00:00.000000Z",
"incident_id": "inc-uuid-...",
"monitor_id": "monitor-uuid-...",
"monitor_name": "Acme Webseite",
"monitor_url": "https://acmecorp.com",
"title": "HTTP 503 Service Unavailable",
"status": "investigating",
"severity": "major",
"started_at": "2026-01-15T14:00:00.000000Z",
"resolved_at": null,
"_managed_tenant": {
"managed_tenant_id": "tenant-uuid-...",
"name": "Acme Corp",
"label": "Enterprise-Kunde #42"
}
}Weitergeleitete DNS-Änderung
Sowohl dns.records_changed als auch dns.nameserver_changed tragen denselben zonen-bewussten Payload. nameserver_change ist null (und nameservers_changed ist false) bei einer reinen Record-Änderung; ändert sich der Satz der autoritativen Nameserver, beschreibt es die Delegation vorher/nachher:
{
"event": "dns.nameserver_changed",
"timestamp": "2026-01-15T16:00:00.000000Z",
"monitor_id": "monitor-uuid-...",
"monitor_name": "Acme Webseite",
"monitor_url": "https://acmecorp.com",
"check_type": "dns",
"zone": "acmecorp.com",
"nameservers": ["ns1.evil-host.com", "ns2.evil-host.com"],
"changes": [
{ "type": "NS", "old": ["ns1.acme-dns.com", "ns2.acme-dns.com"], "new": ["ns1.evil-host.com", "ns2.evil-host.com"] }
],
"nameserver_change": {
"old": ["ns1.acme-dns.com", "ns2.acme-dns.com"],
"new": ["ns1.evil-host.com", "ns2.evil-host.com"]
},
"nameservers_changed": true,
"_managed_tenant": {
"managed_tenant_id": "tenant-uuid-...",
"name": "Acme Corp",
"label": "Enterprise-Kunde #42"
}
}E-Mail-Unterdrückungs-Webhook
Wenn eine Plattform-E-Mail unterdrückt wird, enthält der Webhook alles, was du brauchst:
{
"event": "email.security_password_changed",
"timestamp": "2026-01-15T15:00:00.000000Z",
"email_type": "security_password_changed",
"recipient": {
"email": "[email protected]",
"name": "John Doe",
"language": "de",
"user_id": "user-uuid-..."
},
"template_data": {
"user_name": "John Doe",
"user_email": "[email protected]",
"brand_name": "Acme Monitoring",
"occurred_at": "2026-01-15T14:59:58+00:00",
"ip_address": "203.0.113.10"
},
"_managed_tenant": {
"managed_tenant_id": "tenant-uuid-...",
"name": "Acme Corp",
"label": "Enterprise-Kunde #42"
}
}Dieses Beispiel zeigte früher email.password_reset mit einem funktionierenden reset_url. Dieses Event wird nicht ausgelöst und dieser Payload nie gesendet — siehe den Hinweis zur Auth-Klasse oben.
Wichtige Verhaltensweisen
- Gleiche Event-Typ-Namen — Reseller abonnieren
incident.created, nichtreseller.incident.created. Das_managed_tenant-Feld unterscheidet weitergeleitete Events. - Monitor-Stummschaltung wird umgangen — Wenn ein verwaltetes Team Benachrichtigungen für einen Monitor stummschaltet, erhältst du das weitergeleitete Event trotzdem.
- Gecachte Abfragen — Zuordnungen verwalteter Teams werden 5 Minuten gecacht und automatisch invalidiert bei Änderungen.
E-Mail-Unterdrückung
Reseller können bestimmte Plattform-E-Mails für alle verwalteten Teams unterdrücken. Statt die E-Mail zu senden, feuert Hoppla einen email.*-Webhook mit Empfänger und Template-Daten, damit du sie mit deinen eigenen Templates und deinem Branding selbst verschickst.
Unterdrückung ist eine Umleitung, kein Ausschalter. Sie greift nur, wenn du das passende Event abonniert hast, und die Auth-Klasse wird überhaupt nie unterdrückt — siehe die beiden Hinweise oben.
Unterdrückung konfigurieren
# Aktuelle Konfiguration abrufen
curl https://app.hoppla.dev/api/v1/reseller/email-suppression \
-H "Authorization: Bearer $TOKEN"
# Unterdrückte E-Mail-Typen aktualisieren
curl -X PUT https://app.hoppla.dev/api/v1/reseller/email-suppression \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"suppressed_types": [
"invitation",
"member_removed",
"incident_created",
"incident_resolved",
"warning"
]
}'So funktioniert es
- Konfiguriere über die API oder das Dashboard, welche E-Mail-Typen unterdrückt werden sollen
- Wenn Hoppla normalerweise eine dieser E-Mails an einen Benutzer eines verwalteten Teams senden würde, wird die Unterdrückungskonfiguration geprüft
- Wenn der E-Mail-Typ unterdrückt ist, feuert Hoppla stattdessen einen
email.*-Webhook - Dein System empfängt den Webhook mit dem vollständigen E-Mail-Payload und kann die E-Mail selbst versenden
- Nicht-E-Mail-Benachrichtigungskanäle (Slack, Telegram usw.) sind nicht betroffen — sie funktionieren weiterhin normal
Wichtige Hinweise
- Die Unterdrückung gilt für alle verwalteten Teams (kontoweite Einstellung)
- Nur der E-Mail-Kanal wird unterdrückt — andere Benachrichtigungskanäle funktionieren normal
- Der Payload enthält die Template-Daten der ersetzten Mail. Passwort-Reset- oder Verifizierungslinks sind nie dabei, weil diese Mails nicht unterdrückt werden können
- Ein Abonnement ist Pflicht. Einen Typ zu unterdrücken, den du nicht abonniert hast, hat keine Wirkung: die Plattform sendet die E-Mail normal. Das ist Absicht — die Alternative wäre eine Nachricht, die niemanden erreicht
- Ist dein Endpunkt nicht erreichbar, bleibt die E-Mail trotzdem unterdrückt (kein Fallback auf Plattform-E-Mail). Behandle Zustellfehler auf
email.*also als für den Benutzer sichtbar
Zustellung
- Events werden asynchron im Hintergrund zugestellt
- Fehlgeschlagene Zustellungen werden 3-mal mit Backoff wiederholt (10s, 60s, 300s)
- HTTP-Zustellungs-Timeout: 10 Sekunden -- dein Endpoint muss innerhalb von 10s antworten, sonst gilt die Zustellung als fehlgeschlagen und wird wiederholt
- Der Zustellungsstatus wird protokolliert und ist über die Benachrichtigungsprotokoll-API einsehbar
Unterstützte Kanäle
Reseller-Events können über jeden Benachrichtigungskanal-Typ zugestellt werden:
| Kanal | Beschreibung |
|---|---|
| Webhook | HTTP POST mit JSON-Payload und optionaler HMAC-Signatur |
| Event-Details an konfigurierte Empfänger | |
| Slack | Nachricht via Incoming Webhook |
| Discord | Nachricht via Discord-Webhook |
| Microsoft Teams | Nachricht via Teams Incoming Webhook |
| PagerDuty | Event-Weiterleitung über PagerDuty Events API |
| Telegram | Nachricht über Telegram Bot API |
Webhook-Signaturen prüfen
Wenn du ein secret bei deinem Webhook-Kanal konfiguriert hast, enthält jede Anfrage einen X-Webhook-Signature-Header mit einer HMAC-SHA256-Signatur. (Der frühere Header X-Hoppla-Signature ist seit dem 28. September 2026 entfernt; er trug denselben Wert.)
Prüfe gegen die rohen Bytes des Request-Bodys -- nicht gegen ein neu serialisiertes Objekt. Die Signatur wird über genau die Bytes berechnet, die Hoppla sendet. Hoppla erzeugt diesen Body, indem die obersten Schlüssel alphabetisch sortiert und das JSON ohne maskierte Schrägstriche serialisiert wird. Wenn du das JSON parst und vor der Prüfung neu kodierst, unterscheiden sich die Bytes (Schlüsselreihenfolge, Schrägstrich-Maskierung, Leerzeichen) und die Prüfung schlägt fehl. Lies immer den rohen Request-Body und gib genau diese Bytes an deine HMAC-Funktion weiter.
import hmac
import hashlib
# payload_body MUSS der rohe Request-Body (Bytes) sein, vor jeglichem JSON-Parsing
def verify_signature(payload_body: bytes, secret: str, signature_header: str) -> bool:
expected = hmac.new(
secret.encode(),
payload_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)// Roh-Body lesen, z. B. file_get_contents('php://input') -- kein dekodiertes Array neu kodieren
$expected = hash_hmac('sha256', $rawRequestBody, $secret);
$valid = hash_equals($expected, $signatureHeader);