Webhooks — Ereignisse senden und empfangen
TaskMonkey meldet Ereignisse per signiertem POST an n8n, Make oder dein System, und fremde Systeme lösen über eine eigene URL Tools oder Assistants aus. Signatur, Event-Katalog, Retries, Sicherheit.
Webhooks
Webhooks verbinden deinen Workspace in beide Richtungen mit anderen Systemen, ohne dass jemand pollen muss:
- Ausgehend (TaskMonkey → dein System): Sobald etwas passiert — neue Kundennachricht in der Inbox, Tool fertig, Scheduled Task fehlgeschlagen —, schickt TaskMonkey einen signierten
POSTan eine URL deiner Wahl, z. B. einen n8n-Webhook, ein Make-Szenario oder deinen eigenen Server. - Eingehend (dein System → TaskMonkey): Ein Shop, Stripe, ein Formular-Dienst oder n8n ruft eine eigene URL deines Workspaces auf und löst damit ein freigegebenes Tool oder einen Assistant aus.
Beides verwaltest du unter Backend → Verwaltung → Webhooks (/manage/webhooks, Rolle Manager oder Admin).
Webhooks ergänzen die API-Keys: Mit einem Key fragt dein System TaskMonkey („gib mir X“), mit einem Webhook meldet sich TaskMonkey, sobald X passiert — oder ein fremdes System stößt TaskMonkey an, ohne einen Key zu verwalten. Ein Schritt-für-Schritt-Beispiel mit n8n steht im Rezept n8n ↔ TaskMonkey.
Ausgehende Webhooks
Einrichten
- Verwaltung → Webhooks → + Ausgehend
- URL eintragen (nur
https://), Events anhaken, speichern - Das Signatur-Secret (
whsec_…) wird genau einmal angezeigt. Beim Empfänger hinterlegen — damit prüft er, dass ein Aufruf wirklich von TaskMonkey kommt. - Test-Event senden — das Ergebnis erscheint nach wenigen Sekunden im Zustell-Log.
Verlorenes Secret? „Secret rotieren“ erzeugt ein neues, das alte gilt ab sofort nicht mehr.
Was gesendet wird
Jede Zustellung ist ein POST mit JSON-Body im immer gleichen Umschlag:
{
"id": "evt_5f0c2b8e9a7d4c1b8e3f6a2d9c0b1e47",
"type": "inbox.message.received",
"api_version": "2026-09-25",
"created": "2026-09-25T10:15:00Z",
"workspace": "bloomify",
"data": { "…": "je nach Event, siehe Katalog" }
}
| Header | Inhalt |
|---|---|
Content-Type |
application/json |
X-TaskMonkey-Event |
Event-Typ, z. B. inbox.message.received |
X-TaskMonkey-Delivery |
UUID dieser Zustellung — bei Retries gleich, zum Deduplizieren |
X-TaskMonkey-Signature |
t=<unix-zeit>,v1=<hex> (siehe Signatur prüfen) |
X-TaskMonkey-Attempt |
Nummer des Versuchs (1–6) |
X-TaskMonkey-Hop |
Schleifen-Zähler: 1 normal, 2, wenn das Ereignis selbst von einem eingehenden TaskMonkey-Webhook ausgelöst wurde (siehe Schleifen) |
User-Agent |
TaskMonkey-Webhooks/1.0 … |
api_version ändert sich nur, wenn sich das Format eines bestehenden Events ändert. Neue Felder können jederzeit dazukommen — ignoriere, was du nicht kennst.
Antworten, Retries, Deaktivierung
- 2xx innerhalb von 10 Sekunden = zugestellt. Antworte schnell und verarbeite danach (Queue, n8n „Respond: Immediately“).
- Alles andere (Timeout, Verbindungsfehler, 3xx, 4xx, 5xx) ist ein Fehlversuch. TaskMonkey wiederholt nach 1 min, 5 min, 30 min, 2 h und 12 h — insgesamt 6 Versuche, danach steht die Zustellung auf
failed. - Weiterleitungen werden nicht verfolgt. Eine 301/302 zählt als Fehler — trag die endgültige URL ein.
- 410 Gone beendet die Zustellung sofort (kein Retry).
- Nach 5 endgültig fehlgeschlagenen Zustellungen in Folge wird der Endpoint automatisch deaktiviert, die Admin-Adressen des Workspaces bekommen eine Mail. Reaktivieren: Endpoint öffnen, „Aktiv“ anhaken, speichern. Jede erfolgreiche Zustellung setzt den Zähler zurück.
- Erneut senden im Log legt eine neue Zustellung an (neue
X-TaskMonkey-Delivery, gleiche Event-id).
Fairness: Der Worker bedient die Warteschlange reihum pro Workspace und pro Endpoint (je Durchlauf höchstens 5 Zustellungen pro Workspace und 2 pro Endpoint, mit Zeitbudget). Ein Endpoint, der jede Anfrage in den Timeout laufen lässt, oder ein Workspace mit sehr vielen Events verzögert andere also nicht; bei vielen Events verteilt sich die Zustellung über mehrere Durchläufe (alle 2 s).
Reihenfolge und Duplikate: Zustellungen können in anderer Reihenfolge ankommen, als die Ereignisse passiert sind (z. B. weil eine ältere gerade im Retry hängt), und dieselbe Zustellung kann in seltenen Fällen zweimal ankommen. Dedupliziere über X-TaskMonkey-Delivery bzw. — wenn auch manuelle Wiederholungen nur einmal wirken sollen — über die Event-id; ordne über created.
Das Zustell-Log (Verwaltung → Webhooks → Endpoint) zeigt je Zustellung Status, Versuche, Antwort-Code, Dauer, die ersten 1000 Zeichen der Antwort und den Payload. Einträge werden nach 30 Tagen gelöscht.
Signatur prüfen
v1 ist HMAC-SHA256(secret, "<t>.<roher Body>") als Hex. Prüfe immer gegen den rohen Body (nicht gegen ein neu serialisiertes JSON), vergleiche zeitkonstant und lehne Zeitstempel ab, die mehr als 5 Minuten abweichen — so lässt sich ein mitgeschnittener Aufruf nicht später wiederholen.
Node.js (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/taskmonkey', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-TaskMonkey-Signature') || '';
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const t = parseInt(parts.t, 10);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400);
const expected = Buffer.from(
crypto.createHmac('sha256', process.env.TASKMONKEY_WEBHOOK_SECRET)
.update(`${t}.${req.body}`).digest('hex')
);
const given = Buffer.from(parts.v1 || '');
if (given.length !== expected.length || !crypto.timingSafeEqual(expected, given)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
res.sendStatus(204); // erst quittieren …
handle(event); // … dann verarbeiten
});
Python (Flask)
import hmac, hashlib, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["TASKMONKEY_WEBHOOK_SECRET"].encode()
@app.post("/taskmonkey")
def taskmonkey():
raw = request.get_data() # roher Body
parts = dict(p.split("=", 1) for p in request.headers.get("X-TaskMonkey-Signature", "").split(",") if "=" in p)
t = int(parts.get("t", "0"))
if abs(time.time() - t) > 300:
abort(400)
expected = hmac.new(SECRET, f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts.get("v1", "")):
abort(401)
event = request.get_json()
# … verarbeiten
return "", 204
PHP
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_TASKMONKEY_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts);
$t = (int)($parts['t'] ?? 0);
if (abs(time() - $t) > 300) {
http_response_code(400);
exit;
}
$expected = hash_hmac('sha256', $t . '.' . $raw, getenv('TASKMONKEY_WEBHOOK_SECRET'));
if (!hash_equals($expected, (string)($parts['v1'] ?? ''))) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
http_response_code(204);
// … verarbeiten
Event-Katalog
Abonnieren kannst du genau diese Events — sie stehen im Code (src/Service/Webhook/WebhookEvents.php), nicht als Freitext.
| Event | Wann |
|---|---|
tool.completed |
Ein Tool mit 'webhook_event' => true ist fertig (Erfolg oder Fehler) |
inbox.thread.created |
Erste Nachricht eines neuen Inbox-Threads (E-Mail, WhatsApp, Facebook, Instagram) |
inbox.message.received |
Jede eingehende Kundennachricht in der Inbox |
inbox.thread.status_changed |
Thread erledigt, wartet auf Kunde, zurückgestellt oder wieder geöffnet |
scheduled_task.completed |
Scheduled Task erfolgreich |
scheduled_task.failed |
Scheduled Task fehlgeschlagen |
public_tool.submitted |
Aufruf über die Public Tool API (Formular, Landingpage) |
instagram.connected |
Ein Instagram-Profi-Konto wurde verbunden (eigene Verbindung oder Partner-Nutzer mit ref, siehe Instagram) |
instagram.revoked |
Eine Instagram-Verbindung ist weg: in Instagram entfernt, Datenlöschung, abgelaufen oder getrennt (reason) |
custom.* / custom.<name> |
Eigene Events aus Tools (emitWebhookEvent) |
webhook.test |
Nur über „Test-Event senden“, an genau diesen Endpoint |
Beispiele für data:
tool.completed — nur für Tools, die 'webhook_event' => true tragen (genau true). Gilt für Starts aus Chat, API-Key, Scheduled Task und Public Tool API. Nicht für Läufe, die ein eingehender Webhook ausgelöst hat (Schleifen-Schutz), nicht für Tools, die ein anderes Tool intern aufruft, und nicht in bin/cake test_tool.
{
"tool": "createLead",
"success": true,
"error": null,
"entry_type": "api_key",
"execution_id": "0b7c…",
"duration_ms": 412,
"args": { "email": "max@example.com" },
"result": { "success": true, "lead_id": 4711 }
}
args und result gehen mit, weil das Tool das per Flag ausdrücklich will. Felder, deren Name nach Geheimnis aussieht (password, token, secret, api_key, authorization, iban …), werden durch "[entfernt]" ersetzt, Felder mit _-Präfix (_api_key, _client, _webhook) fallen weg.
inbox.thread.created
{
"thread": { "chat_id": "email:kunde@example.com:3f2a…", "channel": "email", "status": "open" },
"first_message": {
"id": "9d1e…",
"channel": "email",
"subject": "Reklamation Bestellung 1001",
"from": { "name": "Max Muster", "address": "kunde@example.com" },
"text": "Hallo, die Pflanze kam beschädigt an …",
"received_at": "2026-09-25T10:14:58+02:00",
"attachments": [{ "filename": "foto.jpg", "mimetype": "image/jpeg", "size": 184223 }]
}
}
inbox.message.received
{
"thread": { "chat_id": "whatsapp:+4917012345678", "channel": "whatsapp", "is_new": false },
"message": {
"id": "a41c…",
"channel": "whatsapp",
"subject": null,
"from": { "name": "Max", "address": "+4917012345678" },
"text": "Wann kommt meine Bestellung?",
"received_at": "2026-09-25T10:15:00+02:00",
"attachments": []
}
}
text ist der Klartext der Nachricht (max. 10 000 Zeichen). Anhänge werden nur beschrieben, nicht mitgeschickt.
inbox.thread.status_changed
{
"chat_id": "email:kunde@example.com:3f2a…",
"channel": "email",
"previous_status": "open",
"status": "resolved",
"source": "user"
}
status/previous_status: open (offen), waiting (wartet auf Kunde), snoozed (zurückgestellt), resolved (erledigt).
source: user (Mitarbeiter in der Inbox), auto_resolve (Spam-/System-Filter), customer_reply (Kunde antwortet auf einen erledigten, wartenden oder zurückgestellten Thread), snooze_ended (Zurückstellen abgelaufen), commslayer_sync, system.
scheduled_task.completed / scheduled_task.failed
{ "task": "daily_report", "tool": "sendDailyReport", "success": false, "error": "Shopify: 503", "duration_ms": 8120 }
public_tool.submitted
{
"tool": "confirmPhotoUpload",
"success": true,
"origin": "https://aktion.bloomify.de",
"ip_prefix": "91.64.12.0",
"args": null,
"result": null
}
args und result (die Formulardaten und die Antwort) sind nur gefüllt, wenn das Tool 'webhook_event' => true trägt — sonst erfährst du nur, dass ein Aufruf stattfand.
custom.<name> — was dein Tool in data mitgibt:
$ctx['tool']('emitWebhookEvent', [
'name' => 'lead.qualified', // → custom.lead.qualified
'data' => ['lead_id' => $id, 'score' => 87],
]);
{ "lead_id": 4711, "score": 87 }
Namen: Kleinbuchstaben, Ziffern, ., _, -, max. 60 Zeichen. emitWebhookEvent ist ein Shared-Tool (in jedem Workspace vorhanden) und kann auch einem Assistant zugewiesen werden. Im Test-Modus sendet es nichts. Eigene Handler senden direkt über $ctx['emitWebhook']('lead.qualified', $data) (gleiche Regeln; Katalog-Events lassen sich so nicht fälschen — der Name wird immer zu custom.<name>).
webhook.test
{ "message": "Test-Event aus TaskMonkey. Wenn du das siehst, stimmt die Verbindung.", "endpoint_id": 3, "triggered_by": "marcus@example.com" }
Was noch nicht als Event kommt
chat.message.created (jede Chat-Nachricht) ist bewusst noch nicht dabei: Backend-, Widget- und Sub-Chats laufen über denselben Speicherpfad, und interne Mitarbeiter-Chats sollen nicht nach außen gehen. Bis dahin: ein eigenes Event per emitWebhookEvent aus dem Assistant. Ebenfalls später: inbox.thread.created für Widget-Chats, Zuweisung/Kategorie-Änderungen.
Eingehende Webhooks
Einrichten
- Ziel freigeben — in der Tool-Definition:
'createLead' => [
'description' => 'Legt einen Lead an.',
'internal' => true,
'inbound_hook' => true, // Ziel eingehender Webhooks
'inbound_hook_write' => true, // darf schreiben — Begründung im Kommentar
'parameters' => [
'type' => 'object',
'properties' => [
'email' => ['type' => 'string', 'pattern' => '^[^@\s]+@[^@\s]+$'],
'name' => ['type' => 'string', 'maxLength' => 100],
],
'required' => ['email'],
],
'handler' => function (array $results, array $args, array $ctx): array {
// $args['_webhook'] = ['hook_id', 'hook_name', 'delivery_id', 'external_id', 'received_at']
// …
},
],
Oder einen Assistant mit 'inbound_hook' => true in seiner Definition.
- Verwaltung → Webhooks → + Eingehend: Name, Auth-Format, Ziel, optional Mapping.
- URL (
https://app.taskmonkey.de/api/v1/hooks/hk_…) und Secret werden einmalig angezeigt — im aufrufenden System eintragen.
Freigabe
| Ziel | Flag | Hinweis |
|---|---|---|
| Tool | 'inbound_hook' => true oder 'external_api' => true |
genau true; Tools mit async: true sind ausgeschlossen |
| Tool, schreibend | zusätzlich 'inbound_hook_write' => true (oder 'external_api_write' => true) |
sonst läuft es im Read-only-Kontext |
| Assistant | 'inbound_hook' => true in der Assistant-Definition |
seine Tools laufen im Read-only-Kontext |
| Assistant, schreibend | zusätzlich 'inbound_hook_write' => true in der Assistant-Definition |
nur, wenn der Assistant aus fremden Daten wirklich schreiben soll |
Die Freigabe wird bei jeder Verarbeitung neu geprüft: nimmst du das Flag weg, laufen auch bestehende Hooks ins Leere. Der Builder setzt inbound_hook/inbound_hook_write nur nach eigener Rückfrage (confirm_exposure), wie bei public_api und external_api.
Auth-Formate
| Format | Header | Prüfung | Replay-Schutz |
|---|---|---|---|
| TaskMonkey-HMAC | X-TaskMonkey-Signature: t=…,v1=… |
wie oben | Zeitstempel-Toleranz (Default 300 s) + Signatur (gleicher Zeitstempel + Body nur einmal) + X-TaskMonkey-Delivery |
| Stripe-Style | Stripe-Signature: t=…,v1=… |
identisch, mit dem Stripe-Endpoint-Secret | Toleranz + Signatur + Event-id aus dem Body |
| Shopify | X-Shopify-Hmac-Sha256 |
base64(HMAC-SHA256(secret, body)) |
Hash des signierten Bodys (derselbe Body wird nur einmal verarbeitet) |
| Bearer | Authorization: Bearer <secret> |
Token-Vergleich | Idempotency-Key / X-Delivery-Id / X-Request-Id, falls gesendet |
Bei Shopify und Stripe gibt der Anbieter das Secret vor — beim Anlegen im Feld „Secret des Anbieters“ einfügen. Sonst erzeugt TaskMonkey eins. Bearer ist am einfachsten, aber ohne Zeitstempel: wer den Token hat, kann beliebig senden — nimm HMAC, wo das aufrufende System es kann.
Beispiel TaskMonkey-HMAC mit curl:
BODY='{"email":"max@example.com","name":"Max"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$TM_HOOK_SECRET" | sed 's/^.* //')
curl -X POST https://app.taskmonkey.de/api/v1/hooks/hk_… \
-H 'Content-Type: application/json' \
-H "X-TaskMonkey-Signature: t=$TS,v1=$SIG" \
-H "X-TaskMonkey-Delivery: $(uuidgen)" \
-d "$BODY"
{"success":true,"accepted":true,"delivery_id":"6f1d0c1e-…"}
Ein ausgehender TaskMonkey-Webhook eines Workspaces kann so direkt den eingehenden eines anderen auslösen — Signatur und Header passen zueinander.
Body → Args
| Einstellung | Ergebnis |
|---|---|
| nichts | Die obersten Felder des JSON-Bodys sind die Args |
Mapping {"email": "/customer/email", "betrag": "/total_price"} |
Jedes Arg per JSON Pointer aus dem Body; fehlende Pfade werden ausgelassen |
Body-Arg payload |
Der ganze Body landet in args.payload (Tool deklariert payload als type: object) |
Danach gilt dasselbe wie bei API-Keys: nur im parameters-Schema deklarierte Args kommen durch, Pflichtfelder und Typen werden geprüft, {{-Marker sind verboten. Das Tool bekommt zusätzlich args['_webhook'] (Hook, Delivery-ID, Zeitpunkt) — serverseitig gesetzt, nicht fälschbar. Formular-Bodys (application/x-www-form-urlencoded) werden wie JSON behandelt.
Bei einem Assistant als Ziel wird ein eigener Chat (hook_<id>_…) angelegt; der Assistant bekommt die (gemappten) Daten als Nachricht und läuft ohne Benutzer. Die Antwort steht im Log des Hooks.
Weil die Daten von außen kommen, sind sie für den Assistant ausdrücklich als nicht vertrauenswürdig markiert und eingezäunt (<untrusted_webhook_data>…</untrusted_webhook_data> mit dem Hinweis, darin enthaltenen Anweisungen nicht zu folgen). Das senkt das Risiko von Prompt-Injection, verhindert es aber nicht sicher — deshalb laufen seine Tools nur lesend, solange der Assistant nicht 'inbound_hook_write' => true trägt. Gib einem schreibenden Hook-Assistant nur die Tools, die er für genau diesen Zweck braucht.
Assistant-Ziele laufen in einer eigenen Warteschlange (Worker-Lane assistant): ein LLM-Lauf, der Minuten dauert, hält Zustellungen und Tool-Hooks nicht auf.
Antworten
| Status | Bedeutung |
|---|---|
| 202 | angenommen: {"accepted":true,"delivery_id":"…"} — das Ziel läuft in wenigen Sekunden im Hintergrund |
| 200 | Duplikat (schon angenommen): {"duplicate":true,"delivery_id":"<ursprüngliche>"} |
| 400 | Body ist kein gültiges JSON |
| 401 | Signatur/Token fehlt, falsch oder Zeitstempel außerhalb der Toleranz |
| 403 | Workspace gesperrt |
| 404 | Hook unbekannt oder deaktiviert (nicht unterscheidbar) |
| 508 | Schleife: X-TaskMonkey-Hop ist 2 oder höher |
| 405 | nicht POST |
| 413 | Body größer als 256 KB |
| 429 | Rate-Limit des Hooks (Default 60/min) — mit Retry-After |
Weil die Antwort vor der Verarbeitung kommt, sieht der Absender Fehler des Tools nicht. Ergebnis, Fehler und Dauer stehen im Log des Hooks (Verwaltung → Webhooks → Hook), dort lässt sich ein Aufruf auch erneut verarbeiten. Einträge werden nach 30 Tagen gelöscht.
Sicherheit
Ausgehend:
- Nur
https://, keine Zugangsdaten in der URL. - Kein Zugriff auf interne Netze (SSRF-Schutz): Der Hostname wird aufgelöst, und jede Adresse muss öffentlich sein. Gesperrt sind u. a.
127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16(Cloud-Metadaten),100.64.0.0/10,0.0.0.0/8, Multicast/reserviert,::1,fc00::/7,fe80::/10sowie IPv4-gemappte Varianten. Geprüft wird beim Speichern und vor jedem Versand; die geprüfte IP wird für die Verbindung festgenagelt, damit ein DNS-Wechsel dazwischen (DNS-Rebinding) nichts bringt. - TaskMonkey selbst ist kein Ziel: URLs auf den App-Host (z. B.
app.taskmonkey.de) werden abgelehnt — ein ausgehender Webhook auf den eigenen/api/v1/hooks/…wäre eine Schleife. - Keine Redirects, Timeout 10 s, Antworten werden nach 64 KB abgeschnitten.
- Payload max. 256 KB. Keine Secrets: das Signatur-Secret steht nie im Payload, Secret-artige Felder werden entfernt.
- Secrets liegen verschlüsselt in der Datenbank.
Eingehend:
- Jeder Aufruf braucht eine gültige Signatur bzw. einen Token — die unratbare URL allein reicht nicht.
- Replay-Schutz über Zeitstempel und Delivery-ID, Rate-Limit pro Hook.
- Der Workspace kommt aus dem Hook: ein Hook erreicht nie Tools oder Assistants eines anderen Workspaces.
- Nur freigegebene Ziele, nur deklarierte Parameter, standardmäßig nur lesend.
- Keine internen Fehlermeldungen nach außen.
Checkliste für jedes Ziel mit inbound_hook:
-
parameterseng (pattern,enum,maxLength) — der Absender kontrolliert den Body -
inbound_hook_writenur, wo wirklich geschrieben wird, mit Begründung im Kommentar - Idempotent: derselbe Datensatz zweimal ergibt kein Duplikat (Absender wiederholen bei Timeouts)
- HMAC statt Bearer, wo möglich
- Rate-Limit passend zum erwarteten Volumen
Schleifen
Ein Kreis „eingehender Hook → Tool → ausgehender Webhook → (n8n) → derselbe Hook“ wird an drei Stellen unterbrochen:
- Läufe aus eingehenden Webhooks melden kein
tool.completed. - Hop-Header: Jede ausgehende Zustellung trägt
X-TaskMonkey-Hop. Normal ist1; stammt das Ereignis aus einem Lauf, den ein eingehender Webhook mitX-TaskMonkey-Hop: 1ausgelöst hat (z. B.emitWebhookEventim Hook-Tool), steht dort2. Der eingehende Endpoint lehnt2und höher mit 508 ab. Reiche den Header in n8n/Make unverändert weiter, wenn ein Workflow ein TaskMonkey-Ereignis an einen TaskMonkey-Hook zurückspielt — dann greift der Schutz auch über Umwege. - Endpoints auf den App-Host selbst sind gesperrt (siehe oben).
Das Rate-Limit pro Hook bremst zusätzlich.
Betrieb
| Was | Wie |
|---|---|
| Zustellung + Verarbeitung | bin/cake run_webhooks — cronjobs.sh startet alle 5 min --loop 290 im Hintergrund (pollt alle 2 s, flock gegen Doppelstart) |
| Assistant-Ziele | bin/cake run_webhooks --lane assistant --loop 290 — eigener Worker mit eigenem Lock, ebenfalls aus cronjobs.sh |
| Fairness | Round-Robin pro Workspace (ausgehend max. 5 je Workspace und 2 je Endpoint pro Batch, eingehend max. 3 je Workspace), Zeitbudget 20 s pro Batch |
| Aufräumen | bin/cake run_webhooks --prune 30, täglich 04:00 aus cronjobs.sh |
| Tool-Läufe | erscheinen im Execution-Log mit Entry-Type webhook und Chat-ID hook_… (tm monitor, tm watch) |
| Log der Worker | tmp/cron-webhooks.log |
Technik
| Was | Wo |
|---|---|
| Event-Katalog | src/Service/Webhook/WebhookEvents.php |
| Emit (nur Queue-Zeile) | src/Service/Webhook/WebhookDispatcher.php |
| Zustellung, Retries, Auto-Deaktivierung | src/Service/Webhook/WebhookDeliveryService.php, WebhookSender.php |
| Signatur | src/Service/Webhook/WebhookSigner.php |
| SSRF-Schutz | src/Service/Security/EgressGuard.php |
| Eingehend | src/Controller/Api/InboundHookController.php, src/Service/Webhook/InboundHookService.php, ArgMapper.php |
| Worker | src/Command/RunWebhooksCommand.php |
| Verwaltung | src/Controller/Manage/WebhooksController.php |
| Eigene Events | config/tenants/_shared/tools/webhooks/emitWebhookEvent.php |
| Tabellen | hook_outbound_endpoints, hook_outbound_deliveries, hook_inbound_endpoints, hook_inbound_deliveries |