Wiki / Core Features / Ausgehende Webhooks

Ausgehende Webhooks

Aktualisiert von Maxime_48 · vor 3 Stunden · Aufrufe: 72

Sprachen: English Français Deutsch Español Português (Brasil) Italiano Nederlands Polski Türkçe Русский 日本語 한국어 中文

Ein Webhook sendet die Aktivität deines Teams in dem Moment, in dem sie passiert, an eine URL, die dir gehört. Jede Aktion, die in deinem Audit-Log landet, kann als signierter JSON-POST an deinen eigenen Server, deinen eigenen Bot oder ein internes Tool gepusht werden: kein Polling, kein Export, kein Scraping des Panels.

Eine Regel entscheidet alles auf dieser Seite. Ein Webhook transportiert genau das, was das Aktivitätsprotokoll deines Teams zeigt, und nicht mehr. Kein paralleler Feed mit eigenem Umfang, keine reichhaltigere Nutzlast für Maschinen. Steht es nicht auf dieser Seite, steht es nicht im Body.

Ein aktiver Team-Webhook mit seinen Zustellkategorien und dem Zeitpunkt der letzten Zustellung


📍 Wo du ihn findest

Teameinstellungen → Webhooks, auf der eigenen Seite deines Teams. Bis zu drei Ziele pro Team, jedes mit eigener URL, eigenen Kategorien und eigenem Signaturgeheimnis.

Wer einen einrichten darf: der Inhaber des Teams oder ein Mitglied, dessen Rolle Einen Server aus dem Team entfernen enthält. Das ist dieselbe Regel, die das Aktivitätsprotokoll selbst öffnet: Ein Webhook leitet weiter, was diese Seite anzeigt, also bedeutet die Erlaubnis, sie zu lesen, auch die Erlaubnis, sie weiterzuleiten. Siehe Team-Rollen und Berechtigungen.


🎯 Was gesendet wird und was nie

Du wählst die Kategorien. Nur die angehakten werden zugestellt:

Kategorie Was sie abdeckt
Funktionen Eine Funktion wurde aktiviert, deaktiviert oder ihre Einstellungen gespeichert
Server Ein Server wurde dem Team hinzugefügt oder daraus entfernt
Team Team umbenannt, Rollen bearbeitet, Webhooks geändert
Mitglieder Mitglieder eingeladen, hinzugefügt, entfernt oder ausgetreten
Moderation Sanktionen, Einsprüche, Meldungen, Tickets, gelöschte Historien
Abonnement Änderungen am Abonnement des Teams
Abrechnung Abrechnungsereignisse des Teams
Plattformaktionen für dieses Team Was ein Plattform-Admin an einem deiner Server getan hat

🚫 Drei Dinge werden nie übertragen

  • IP-Adressen. Im Aktivitätsprotokoll wird eine IP nur der Person gezeigt, zu der sie gehört, und niemandem sonst. Ein Webhook hat keinen Leser, gegen den geprüft werden könnte, daher wird das Feld nicht gesendet: Es wird aus der Nutzlast entfernt, nicht geleert.
  • Backoffice-Aktionen, die dich nicht betreffen. Plattformweite Administration trägt kein Team, daher kann nichts aus der Verteilung sie erreichen. Die Kategorie Plattformaktionen für dieses Team umfasst nur das, was ein Admin an deinen Servern getan hat.
  • Zugangsdaten. Jedes Feld, das wie ein Token, ein Schlüssel oder ein Geheimnis aussieht, kommt als •••••••• an, auf beiden Seiten eines Vorher-nachher-Paars. Das geschieht auf dem Weg nach draußen noch einmal, unabhängig von der Maskierung, die die Seite vornimmt.

Anmeldungen werden nicht angeboten. Das Audit-Protokoll hat eine Kategorie auth, und sie fehlt in der obigen Liste mit Absicht: Anmeldeereignisse unbeaufsichtigt und unbegrenzt an einen Drittserver zu pushen ist etwas anderes, als sie auf einer Seite zu zeigen, die jemand erst öffnen musste. Sie könnte später hinzukommen.


📦 Die Zustellung

Jede Zustellung ist ein einzelner POST mit einem JSON-Body:

{
  "version": 1,
  "delivery_id": "0a4b2f0e-8c6e-4a5f-9b3d-2f6c1e0d7a11",
  "event": "server.feature.enabled",
  "category": "feature",
  "team": { "id": 12, "name": "Night Shift" },
  "data": {
    "id": 918273,
    "action": "server.feature.enabled",
    "category": "feature",
    "description": "Enabled Starboard",
    "actor_name": "Ava",
    "actor_role": "moderator",
    "is_admin_action": false,
    "team_id": 12,
    "team_name": "Night Shift",
    "server_id": 4,
    "server_name": "Night Shift HQ",
    "subject_label": "Starboard",
    "changes": { "threshold": { "before": 3, "after": 5 } },
    "metadata": null,
    "created_at": "2026-09-06T09:41:12+00:00"
  }
}

Diese fünfzehn Felder sind der gesamte Inhalt von data: dieselben fünfzehn, die das Aktivitätsprotokoll anzeigt, abzüglich der IP-Adresse. Jedes davon kann null sein: Eine Aktion ohne Server trägt "server_id": null, eine Aktion ohne Unterschiede trägt "changes": null. Lies sie defensiv, statt pro Ereignis eine feste Form anzunehmen.

  • version ist die Version des Umschlags. Sie existiert, damit dein Empfänger darauf verzweigen kann, statt zu raten, wenn die Form wächst.
  • event ist die protokollierte Aktion. Es gibt über hundertdreißig davon, und die Liste wächst mit der Plattform: Vergleiche mit einem Präfix oder mit category, nie mit einer vollständigen Liste von Namen.
  • delivery_id bleibt über alle Wiederholungen desselben Ereignisses hinweg stabil. Sie macht die Deduplizierung möglich, siehe unten.
  • data ist der Audit-Eintrag selbst, dasselbe Objekt, das das Aktivitätsprotokoll anzeigt.

📨 Header

Header Wert
User-Agent YAWBDB-Webhooks/1.0
X-YAWBDB-Event Der Aktionsname, derselbe wie event im Body
X-YAWBDB-Delivery Die Zustell-ID, dieselbe wie delivery_id
X-YAWBDB-Timestamp Unix-Zeit in Sekunden, zu der die Anfrage erstellt wurde
X-YAWBDB-Signature v1= gefolgt vom Hex-Digest

🔐 Die Signatur prüfen

Jeder, der die URL deines Ziels erfährt, kann beliebige Daten per POST dorthin schicken. Die Signatur ist das, womit du unsere Zustellungen von deren unterscheidest.

Der Digest ist HMAC-SHA256 über den Zeitstempel, einen Punkt und dann den rohen Request-Body, mit deinem Signaturgeheimnis als Schlüssel:

signed_string = X-YAWBDB-Timestamp + "." + raw_body
signature     = "v1=" + hex( hmac_sha256(signed_string, secret) )

Drei Details sind tragend, und das Auslassen eines davon hinterlässt eine Lücke:

  • Signiere die rohen Bytes, vor jedem JSON-Parsing. Den Body neu zu serialisieren verändert ihn (Reihenfolge der Schlüssel, maskierte Schrägstriche, Unicode), und der Digest stimmt dann nicht mehr.
  • Der Zeitstempel steckt im signierten String, nicht nur daneben. Das ermöglicht es dir, Wiederholungen abzulehnen: Weise eine Zustellung zurück, deren Zeitstempel mehr als ein paar Minuten alt ist (fünf Minuten sind ein sinnvolles Fenster), und ein Angreifer kann keine Anfrage erneut senden, die er letzte Woche mitgeschnitten hat. Da der Zeitstempel signiert ist, kann er ihn auch nicht ändern.
  • Vergleiche in konstanter Zeit. Ein einfaches == kehrt beim ersten abweichenden Byte zurück, und dieser Zeitunterschied reicht aus, um eine Signatur Byte für Byte zu rekonstruieren. Nutze hash_equals, crypto.timingSafeEqual, hmac.compare_digest, wie auch immer deine Sprache es nennt.

PHP

$raw       = file_get_contents('php://input');
$timestamp = (int) ($_SERVER['HTTP_X_YAWBDB_TIMESTAMP'] ?? 0);
$given     = $_SERVER['HTTP_X_YAWBDB_SIGNATURE'] ?? '';

if (abs(time() - $timestamp) > 300) {
    http_response_code(400);   // too old, or clock is wrong
    exit;
}

$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret);

if (!hash_equals($expected, $given)) {
    http_response_code(401);
    exit;
}

http_response_code(200);       // accepted

Node.js (Express)

import crypto from 'node:crypto';

// The raw body is required - express.json() alone destroys it.
app.post('/yawbdb', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = Number(req.get('X-YAWBDB-Timestamp'));
  const given     = req.get('X-YAWBDB-Signature') ?? '';

  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return res.sendStatus(400);

  const expected = 'v1=' + crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${req.body}`)
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(given);

  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);

  res.sendStatus(200);
});

Das Präfix v1= benennt das Verfahren. Sollte es sich je ändern, kann ein Empfänger, der das Präfix prüft, ein unbekanntes gezielt ablehnen, statt es stillschweigend falsch zu lesen.

🔑 Das Geheimnis

64 hexadezimale Zeichen, für dich erzeugt, genau einmal angezeigt, im Panel, direkt nachdem du das Ziel erstellt oder auf Neues Geheimnis geklickt hast. Danach liest es nichts mehr zurück: nicht die Seite, nicht die API, nicht ein Export. Verlierst du es, erzeugst du ein neues.

Das Rotieren wirkt sofort für alles, was ab diesem Moment gesendet wird. Bereits eingereihte Zustellungen gehen weiter mit dem alten Geheimnis signiert hinaus, bis die Warteschlange abgearbeitet ist. Rechne also mit einer kurzen Überlappung und aktualisiere zuerst deinen Empfänger.


🔁 Zustellung, Wiederholungen und Deduplizierung

  • Nur ein 2xx gilt als empfangen. Alles andere (3xx, 4xx, 5xx) ist ein Fehler. Antworte schnell und erledige die Arbeit danach.
  • Weiterleitungen werden nicht verfolgt. Ein 302 ist ein Fehler, kein Zwischenschritt. Gib uns die endgültige URL.
  • Timeouts sind kurz: drei Sekunden zum Verbinden, fünf Sekunden zum Antworten. Das ist eine Benachrichtigung, kein Remote Procedure Call: erst bestätigen, später verarbeiten.
  • Vier Versuche, im Abstand von 30 Sekunden, 2 Minuten und dann 10 Minuten: insgesamt etwa zwölf Minuten. Das deckt ein Deployment ab, keinen Ausfall.
  • Wiederholungen verwenden dieselbe delivery_id. Ein Empfänger, der jeden POST als neu behandelt, wird doppelt zählen, an dem Tag, an dem unser erster Versuch bei dir erfolgreich ist, bei uns aber in ein Timeout läuft. Speichere die ID und ignoriere eine, die du schon verarbeitet hast.
  • Die Reihenfolge ist nicht garantiert. Zustellungen werden unabhängig eingereiht; eine wiederholte kommt nach Ereignissen an, die später waren. Nutze data.created_at, wenn dir die Reihenfolge wichtig ist.

⛔ Automatische Abschaltung

Nach 20 aufeinanderfolgenden Fehlern wird das Ziel abgeschaltet, und das Panel zeigt das auf der Karte an. Ein Ziel, das aufgehört hat zu antworten, hat meist endgültig aufgehört, und niemand sagt uns Bescheid: Ewig zu wiederholen würde pro Aktion eine Anfrage und eine Verlaufszeile kosten, unbegrenzt.

Zum Neustart: Behebe deinen Empfänger, klicke dann beim Webhook auf Bearbeiten und setze den Haken bei Ereignisse an dieses Ziel senden erneut. Das Wiedereinschalten setzt den Zähler zurück, sodass ein reparierter Endpunkt nicht nur einen Fehler davon entfernt ist, wieder zu verstummen.


🧪 Einen Test senden

Test senden schickt sofort eine echte, signierte Anfrage an dein Ziel und zeigt dir den Statuscode und die Round-Trip-Zeit. Es ist keine Simulation: Sie läuft durch denselben Code wie jede echte Zustellung, mit denselben Headern, sodass ein Ziel, das den Test besteht, nicht danach den Produktivverkehr wegen eines Headers ablehnen kann, den der Test nie gesendet hat.

Der Test-Body trägt "event": "panel.webhook.test" und "category": "test", damit dein Empfänger ihn erkennen kann.

Rechne mit zwei Zustellungen, nicht einer. Das Drücken der Schaltfläche ist selbst eine protokollierte Team-Aktion. Dein Ziel erhält also die Testzustellung und, falls es die Kategorie Team abonniert hat, einen Moment später eine zweite, gewöhnliche Zustellung für team.webhook.tested. Das ist korrekt und keine Schleife.

Ein Test zählt nie zur Obergrenze von 20 Fehlern und setzt sie nie zurück. Das Debuggen eines Ziels kann es nicht abschalten.


📜 Letzte Versuche

Letzte Versuche zeigt die letzten 20 Versuche für dieses Ziel: wann, welches Ereignis, welche Versuchsnummer, den Statuscode oder den Fehler und wie lange es dauerte. Fehler tragen einen kurzen Auszug dessen, was dein Server geantwortet hat, und das ist meist der schnellste Weg herauszufinden, dass ein Reverse-Proxy und nicht dein Code derjenige ist, der Nein sagt.

Versuche werden 30 Tage lang aufbewahrt und nachts bereinigt. Das ist Debugging-Abfall, keine Aufzeichnung: Das Audit-Protokoll selbst hat eine eigene, deutlich längere Aufbewahrung.


🛡️ Wohin wir nicht senden

Die URL eines Ziels muss auf das öffentliche Internet zeigen. Eine URL, die zu einer Loopback-Adresse, einem privaten Bereich, einer Link-Local-Adresse oder einer Cloud-Metadaten-Adresse aufgelöst wird, wird abgelehnt: beim Speichern und noch einmal vor jedem einzelnen Senden.

Die zweite Prüfung ist die, die auf Dauer zählt. Ein Name, der am Tag der Eingabe auf etwas Öffentliches zeigte, kann einen Monat später auf 127.0.0.1 zeigen, und ein Webhook feuert, solange er existiert. Eine Ablehnung zählt als Fehler für das Ziel und wird nicht wiederholt: Denselben Resolver noch dreimal dasselbe zu fragen würde nichts ändern.

Bevorzuge ein https-Ziel. Die Signatur beweist, wer den Body gesendet hat, sie verbirgt ihn nicht: Über einfaches http reist deine Aktivität im Klartext.


💬 Direkt in einen Discord- oder Slack-Kanal

Füge eine Discord-Webhook-URL (https://discord.com/api/webhooks/…) als Ziel ein, und das Panel sendet statt des JSON-Umschlags eine Discord-Nachricht: ein Embed pro Eintrag, mit der Beschreibung als Titel, mit Ereignis, Kategorie, Akteur, Team, Server, Betreff und, falls vorhanden, den Änderungen als kurzem Diff. Der Zeitstempel ist der des Eintrags, und die Fußzeile trägt die Zustell-ID.

Das Embed wird aus demselben oben beschriebenen Umschlag erzeugt und erbt daher seine zwei Regeln: keine IP-Adresse, Zugangsdaten maskiert. Erwähnungen sind auf Seiten von Discord abgeschaltet, sodass ein Kanal oder eine Rolle namens @everyone in einem Eintrag als Text gepostet wird und niemanden pingt. Die X-YAWBDB-*-Header und die Signatur werden weiterhin gesendet; Discord ignoriert sie einfach.

Discord antwortet auf eine akzeptierte Nachricht mit 204 No Content, und genau das zeigt eine grüne Zeile unter Letzte Versuche dafür an.

Dasselbe gilt für eine Slack-Incoming-Webhook-URL (https://hooks.slack.com/services/…): Das Panel postet pro Eintrag eine Slack-Nachricht: die Beschreibung fett, dann je eine Zeile pro Fakt, die Änderungen in einem Codeblock und darunter Zeitstempel und Zustell-ID. Sie wird aus demselben Umschlag erzeugt, daher gelten dieselben zwei Regeln, und die drei Zeichen, die Slack für seine eigenen Befehle reserviert (<, >, &), werden maskiert, sodass ein von einem Mitglied getippter Name nie zu @channel oder @here werden kann. Slack antwortet auf eine akzeptierte Nachricht mit 200 ok. Die URLs von Slack für Workflows und Trigger sind keine Nachrichten: Sie behalten den JSON-Umschlag, den ein Workflow mit seinen eigenen Variablen zerlegen kann.


💡 Was Leute damit bauen

  • Die Aktivität deines Teams in einen eigenen Discord- oder Slack-Kanal posten: Eine Discord- oder Slack-Webhook-URL funktioniert unverändert, oder setze einen eigenen Empfänger dazwischen, um zu filtern und umzuformatieren.
  • Das Protokoll in einen eigenen Log-Speicher spiegeln und über das Aufbewahrungsfenster des Panels hinaus behalten.
  • Jemanden alarmieren, wenn außerhalb der Dienstzeiten eine Moderations-Aktion eingeht.
  • Einen Build, eine Synchronisierung oder ein Backup auslösen, wenn sich eine Einstellung einer Funktion ändert.

🔗 Siehe auch