Alarm-Webhook
Was Vaicoa an ein Alarmziel sendet. Wie du eines einrichtest, steht unter Alarme dorthin schicken, wo du arbeitest.
Einstellungen eines Ziels
Abschnitt betitelt „Einstellungen eines Ziels“Ziele richtet ein Admin im Portal unter Alarme → Alarmziele ein. Vaicoa Pro stellt an ein Ziel zu, Vaicoa Team an bis zu fünf.
| Feld | Ziel | Hinweise |
|---|---|---|
| Name | Webhook, PagerDuty | bis zu 60 Zeichen, eindeutig im Team. Erscheint auf der Seite, in Alarmen über das Ziel und im Audit-Trail; ist er leer, wird stattdessen die Adresse angezeigt |
| Alarme an diese URL senden | Webhook | nur https://, bis zu 500 Zeichen, ohne Benutzername oder Passwort. Verschlüsselt gespeichert und nie wieder angezeigt: Das Portal zeigt nur Schema und Host, weil bei vielen Empfängern der Pfad die Zugangsdaten sind. Zum Ändern bearbeitest du das Ziel und fügst die vollständige URL neu ein |
| Welche Alarme | Webhook, PagerDuty | Warnungen und kritische Alarme oder Nur kritische Alarme, je Ziel. Push-Mitteilungen sind nicht betroffen |
| Was wir senden | Webhook | Standard-Payload oder Coding-Agent-Bericht (siehe unten) |
| Authentifizierungs-Header (optional), Wert des Headers | Webhook | ein Header, den dein Endpunkt erwartet, zum Beispiel Authorization. Der Wert wird verschlüsselt gespeichert und nie wieder angezeigt. Beim Bearbeiten heißt das Feld Wert des Headers ersetzen: Leer behält es den gespeicherten Wert. Löschst du den Namen, wird der Wert gelöscht. Testereignis senden schickt den Header mit |
| Service-Region | PagerDuty | Vereinigte Staaten oder Europäische Union |
| Routing-Key der Integration | PagerDuty | aus der Events-API-v2-Integration, die du in PagerDuty anlegst. Verschlüsselt gespeichert und nie wieder angezeigt. Vaicoa kann keinen neuen ausstellen, weil PagerDuty das tut. Beim Bearbeiten heißt das Feld Routing-Key ersetzen: Füg einen neuen Key ein oder lass es leer, um den gespeicherten zu behalten |
Verschiedene Ziele können verschiedene Alarme bekommen: zum Beispiel PagerDuty für die Rufbereitschaft, alle Warnungen an einen Technik-Kanal und nur kritische Alarme an einen Agent-Endpunkt. Entfernen stoppt ein Ziel. Legst du es später wieder an, werden frühere Alarme nicht nachgeschickt. Ein Planwechsel löscht nie ein Ziel: Erlaubt der neue Plan weniger, stellen die ältesten weiter zu, und die übrigen bleiben pausiert gespeichert, bis der Plan sie wieder erlaubt oder du ein älteres entfernst.
Der Request
Abschnitt betitelt „Der Request“Ein Webhook-Ziel bekommt einen POST-Request, jedes Mal, wenn ein Alarm
beginnt oder beendet wird, und wenn du Testereignis senden nutzt.
| Header | Inhalt |
|---|---|
Content-Type | application/json |
User-Agent | CloudViewer-Webhook/1 |
X-CloudViewer-Event | der Wert von event, damit du ohne Parsen des Bodys weiterleiten kannst |
X-CloudViewer-Delivery | eine ID, die bei Wiederholungen gleich bleibt; speicher sie, wenn du Idempotenz brauchst. Sie entspricht der id im Body |
X-CloudViewer-Signature | sha256=<hex>, ein HMAC-SHA256 des rohen Bodys |
| dein eigener Header | nur, wenn du am Ziel Authentifizierungs-Header (optional) gesetzt hast |
Dein eigener Header darf keiner sein, den Vaicoa setzt: Content-Type,
Content-Length, User-Agent, Host, Transfer-Encoding oder ein Name,
der mit X-CloudViewer- beginnt.
Der Body ist UTF-8-JSON, kompakt gesendet (ohne Leerzeichen) und mit sortierten Schlüsseln. Das Beispiel unten ist zum Lesen formatiert.
Der Payload
Abschnitt betitelt „Der Payload“Jedes Ereignis und jeder Empfänger bekommt dieselbe JSON-Struktur:
{ "version": 1, "event": "alert.firing", "id": "9f2c…", "sent_at": "2026-08-16T09:12:33Z", "team": { "id": 42, "name": "Ops", "slug": "ops" }, "alert": { "fingerprint": "a1b2c3…", "rule": "disk_space_critical", "severity": "critical", "status": "firing", "summary": "Disk almost full", "description": "Disk 96% full on web-1 (/var)", "started_at": "2026-08-16T09:11:02Z", "resolved_at": null }, "resource": { "id": "srv_1", "project_id": 10, "server_id": 7 }, "links": { "app": "cloudviewer://alert/a1b2c3…?resource=srv_1&project=10&team=42", "portal": "https://account.vaicoa.com/a/ops/alerts/" }}| Feld | Bedeutung |
|---|---|
version | die Schema-Version, 1. Neue Felder ändern sie nicht; sie ändert sich nur bei einer Änderung, an der ein Empfänger scheitern könnte |
event | alert.firing, alert.resolved oder test |
id | die Zustell-ID, dieselbe wie in X-CloudViewer-Delivery |
sent_at | wann dieser Request gesendet wurde, in UTC. Das Feld steht im signierten Body |
team | die numerische ID, der Name und der Slug des Teams |
alert.fingerprint | die Identität des Alarms. Zusammen mit started_at bezeichnet sie eine Alarm-Episode |
alert.rule | der Schlüssel der Regel, zum Beispiel disk_space_critical. Siehe Alarmregeln |
alert.severity | warning oder critical |
alert.status | firing oder resolved |
alert.summary, alert.description | der Text der Regel für diesen Alarm |
alert.started_at | wann der Alarm begonnen hat, in UTC |
alert.resolved_at | null, außer das Ereignis ist alert.resolved |
resource.id | die Ressource, an der der Alarm hängt, oder ein leerer String |
resource.project_id, resource.server_id | das verbundene Projekt und der überwachte Server, oder null |
links.app | derselbe Deep Link, den die Push-Mitteilung verwendet. Auf dem Handy öffnet er den Alarm in der App, wo du ihn stummschalten kannst |
links.portal | die Seite Alarme des Teams im Portal |
Ein Alarm, der beginnt und endet, bevor er zugestellt wurde, wird einmal
gesendet, als alert.resolved, mit beiden Zeitstempeln.
Das Ereignis test hat dieselbe Form: alert.rule ist webhook_test,
alert.severity ist warning, alert.summary ist ein fester Testtext,
resource.id und links.app sind leer, und project_id und
server_id sind null. Seine id beruht auf der Uhrzeit und kollidiert
deshalb nie mit einem echten Alarm.
Zustellung, Timeouts und Wiederholungen
Abschnitt betitelt „Zustellung, Timeouts und Wiederholungen“- Ein Request je Ziel und Ereignis, sobald sich der Alarm ändert: ohne Bündelung und ohne Verzögerung. Jedes Ziel bekommt eine eigene Kopie, ein ausgefallenes Ziel betrifft die anderen also nicht.
- Nur Warnungen und kritische Alarme. Informative Alarme bleiben in der App. Ein Ziel mit Nur kritische Alarme bekommt keine Warnungen. Stummgeschaltete Alarme werden nie gesendet.
- Angenommen heißt: jede
2xx-Antwort. Der Body der Antwort wird bis zu 2 KiB gelesen und verworfen. - Timeout: 5 Sekunden für die Verbindung und die Antwort.
- Weiterleitungen werden nicht verfolgt. Eine
3xx-Antwort zählt als Fehler. - Wiederholung: einmal pro Minute. Eine fehlgeschlagene Zustellung wird in der nächsten Minute wiederholt, mit derselben Zustell-ID, bis sie gelingt oder das Ziel abgeschaltet ist. Eine Änderung, die älter als sechs Stunden ist, wird nicht mehr gesendet. Nach einem Fehler warten die übrigen Ereignisse für dieses Ziel auf die nächste Minute.
- Abgeschaltet nach 20 Fehlversuchen in Folge, also nach etwa zwanzig Minuten. Eine erfolgreiche Zustellung setzt den Zähler zurück. Wieder einschalten auf der Seite Alarme oder das Speichern des Ziels schaltet es wieder ein.
- Höchstens 20 Ereignisse je Team und Minute. Der Rest folgt in der nächsten Minute.
- Nur öffentliche https-Adressen. Eine URL muss mit
https://beginnen und darf keinen Benutzernamen und kein Passwort enthalten. Eine URL, die auf eine private oder interne Adresse zeigt, wird beim Speichern abgelehnt und noch einmal vor jedem Request. - Ein neues Ziel bekommt keine älteren Alarme. Die Zustellung beginnt mit den Alarmen, die sich ändern, nachdem du es angelegt hast.
Die Signatur prüfen
Abschnitt betitelt „Die Signatur prüfen“Das Geheimnis wird einmal angezeigt: beim Anlegen des Ziels und jedes Mal, wenn du Neues Signatur-Geheimnis ausstellen nutzt. Beim Bearbeiten eines Ziels bleibt sein Geheimnis erhalten. Berechne den HMAC über den rohen Request-Body, vor dem Parsen des JSON, und vergleiche in konstanter Zeit:
import hashlib, hmac
def verify(raw_body: bytes, header: str, secret: str) -> bool: expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header)sent_at ist Teil des signierten Bodys. Du kannst also auch Requests
ablehnen, die älter sind als zum Beispiel fünf Minuten.
Der Coding-Agent-Bericht
Abschnitt betitelt „Der Coding-Agent-Bericht“Ein Webhook-Ziel kann statt des Standard-Payloads den
Coding-Agent-Bericht senden: Wähl unter Was wir senden die Option
Coding-Agent-Bericht. Der Request ist dasselbe Objekt mit derselben
Signatur und derselben version, plus ein agent-Objekt:
"agent": { "preamble": "You are being handed a production alert from …", "brief_format": "markdown", "brief": "# Incident brief — disk_space_critical on srv_1\n\n…"}| Feld | Bedeutung |
|---|---|
preamble | die Hinweise für Alarm-Übergaben deines Teams aus Agent-Übergabehinweise im Portal, oder der Standardtext von Vaicoa, wenn das Team keine hat |
brief_format | markdown |
brief | ein kurzer Vorfallsbericht in Markdown: ein Titel, ein Abschnitt Alert (Regel, Schwere, Status, Zeiten, Zusammenfassung, Beschreibung auf 500 Zeichen gekürzt), ein Abschnitt Resource und ein Abschnitt Links. Ein Abschnitt ohne Inhalt wird weggelassen, nicht mit „none“ gefüllt |
Der Bericht entsteht nur aus dem, was das Standard-Objekt enthält (Alarm,
Ressource, Links), und gibt deshalb nichts zusätzlich preis. Er ist kürzer
als der Bericht aus der App: Letzte Änderungen, Metrik-Verläufe und
Kosteninformationen gibt es nur in der App, und der Webhook-Bericht lässt sie
weg. Auch das Ereignis test trägt das agent-Objekt, damit du siehst, was
dein Agent bekommen wird. Für eine Chat-Brücke nimm den Standard-Payload. Mehr
zu Berichten: An einen Coding-Agent übergeben.
Slack, Discord und Teams
Abschnitt betitelt „Slack, Discord und Teams“Der Payload ist generisches JSON. Er hat nicht das Nachrichtenformat von Slack, Discord oder Teams. Richtest du eine Incoming-Webhook-URL eines dieser Dienste direkt bei Vaicoa ein, wird die Nachricht nicht richtig dargestellt, weil jeder Dienst sein eigenes Format erwartet.
Setz eine kleine Brücke dazwischen: einen Cloudflare Worker, eine Lambda-Funktion, einen Schritt in n8n oder Make oder ein paar Zeilen Code in etwas, das bei dir ohnehin läuft. Die Brücke nimmt das JSON-Objekt an, prüft die Signatur und sendet das Format, das dein Chat-Dienst erwartet.
Sendest du an deinen eigenen Endpunkt (ein Incident-Werkzeug, ein Skript, eine Queue), brauchst du keine Brücke. Das Objekt oben ist das vollständige Format.
PagerDuty
Abschnitt betitelt „PagerDuty“Ein PagerDuty-Ziel (Vaicoa Team) sendet Ereignisse der PagerDuty Events API v2 an die Region, die du ausgewählt hast:
| Region | Endpunkt |
|---|---|
| Vereinigte Staaten | https://events.pagerduty.com/v2/enqueue |
| Europäische Union | https://events.eu.pagerduty.com/v2/enqueue |
Der Routing-Key steht im Body, deshalb gibt es keine Signatur und keinen
X-CloudViewer-*-Header. Timeout, Wiederholung und Abschalten folgen
denselben Regeln wie beim Webhook. Jede 2xx-Antwort gilt als angenommen.
| Vaicoa | PagerDuty-Ereignis |
|---|---|
alert.firing | event_action: "trigger" |
alert.resolved | event_action: "resolve", nur mit routing_key, event_action und dedup_key |
| eine Alarm-Episode | ein dedup_key: cv- und 32 Hex-Zeichen, ein Hash aus Team, Ziel, Fingerprint des Alarms und seiner Startzeit. Trigger und Resolve tragen denselben Key, deshalb schließt der Resolve den Incident, den der Trigger eröffnet hat |
alert.summary | payload.summary, auf 1.024 Zeichen gekürzt (der Schlüssel der Regel, wenn es keine Zusammenfassung gibt) |
alert.severity | payload.severity: critical oder warning |
resource.id | payload.source; server <id>, wenn es keine Ressource gibt, der Slug des Teams, wenn es beides nicht gibt |
alert.started_at | payload.timestamp, der Beginn des Alarms selbst, nicht der Sendezeitpunkt |
links.app | links: [{"href": …, "text": …}], ein Link, der den Alarm in der App öffnet |
links.portal | client_url, mit dem Namen des Dienstes in client |
| Regel, Fingerprint, IDs, Beschreibung | payload.custom_details: rule, fingerprint, project_id, server_id, resource_id, description |
Ein Alarm, der elfmal flattert, erzeugt elf Incidents. Zum Zusammenfassen nutzt du die Suppression-Regeln von PagerDuty. Zwei PagerDuty-Ziele eröffnen getrennte Incidents.
Testereignis senden schickt einen trigger mit einer festen
Zusammenfassung, die sagt, dass kein Alarm aktiv ist, der Schwere warning und
einem Dedup-Key, der mit cv-test- beginnt, und danach einen resolve dafür.
Es kann keinen echten Incident schließen.