# Webhooks und Events SDK

<Lead>
Jede fachlich relevante Aktion emittiert ein **Domain-Event**. Tenants abonnieren diese Events pro Webhook-Endpoint; die Plattform übernimmt Zustellung, Signatur und Retries. Dein Tool-Code kennt genau einen Einstieg: `emitEventBestEffort`.
</Lead>

## Event emittieren

```typescript
import { emitEventBestEffort } from "@/lib/webhooks";

await emitEventBestEffort(ctx, "homestaging.generated", {
  generationId: generation.id,
  propertyId,
});
```

Was der Aufruf macht:

- Er schreibt in **einer Transaktion** das Event in den append-only Event-Stream und je eine Outbox-Zeile pro passendem aktiven Endpoint des Tenants.
- Die eigentliche HTTP-Zustellung läuft **nach** der Antwort (deferred Kick plus Cron-Worker); deine Server-Action wartet nie auf fremde Endpoints.
- **Fail-soft:** Ein Emit-Fehler wird geloggt, nie geworfen. Ein Webhook-Problem darf die Nutzer-Aktion nicht scheitern lassen.

Payload-Regeln: nur Daten des eigenen Tenants, IDs statt Inhalte, **keine Geheimnisse und keine PII-Payloads**. Der Payload ist für den Tenant im Zustell-Inspektor sichtbar und geht wortgleich an dessen Endpoints.

## Katalog-Pflicht: neuer Event-Typ, neuer Eintrag

Der **einzige** Ort, der die abonnierbaren Event-Typen kennt, ist `WEBHOOK_EVENT_CATALOG` in `@reosa/config`. Wer einen `emitEventBestEffort`-Aufruf mit neuem Typ hinzufügt, **muss** denselben Typ dort registrieren:

```typescript
// packages/config/src/webhook-events.ts · in der passenden Domain-Gruppe
{
  type: "homestaging.generated",          // WORTGLEICH zum Emit-Aufruf
  label: "Homestaging erstellt",          // deutsches Label für den Picker
  description: "Ein Homestaging-Bild wurde generiert.",
  examplePayload: { generationId: "..." }, // optional: speist den Per-Typ-Test-Event
}
```

- **Wortgleich heißt wortgleich:** Der `type`-String im Katalog muss exakt dem Emit entsprechen. Ein unregistrierter Typ ist zwar zustellbar, aber im UI-Picker unter **Einstellungen > Webhooks** nicht auswählbar.
- Die Konvention für Typen ist `<domäne>.<verb>` bzw. `<domäne>.<entity>.<verb>` in Vergangenheitsform (`property.created`, `db-lead.scan.completed`).
- **Kein hardcodiertes Event-Array in Komponenten.** Anzeige-Labels kommen überall aus `webhookEventLabel(type)`.
- Der Test `packages/config/tests/webhook-events.test.ts` bewacht Eindeutigkeit und Format; er bricht, wenn zwei Einträge kollidieren oder ein Typ-String aus der Reihe tanzt.

## Zustellung im Kurzüberblick

Die Zustellung läuft über das Outbox-Pattern, **at-least-once**:

- Signierte POSTs nach Standard-Webhooks: Header `webhook-id`, `webhook-timestamp`, `webhook-signature` (HMAC-SHA256 mit dem Endpoint-Secret `whsec_...`, das genau einmal angezeigt wird).
- Fehlversuche wiederholen mit Backoff **1m → 5m → 30m → 2h → 12h**, danach gilt die Zustellung als `dead`.
- Ein Endpoint, der 15-mal in Folge ohne Zwischenerfolg fehlschlägt, wird automatisch pausiert; der Owner wird benachrichtigt.
- Endpoint-URLs sind tenant-kontrolliert und werden SSRF-geprüft; nur `https://` ist erlaubt.
- Verwaltung, Zustell-Log, Test-Events, Secret-Rotation und Neu-Zustellung liegen für Tenants unter **Einstellungen > Webhooks**.

Das komplette Wire-Format (Body-Aufbau, Signatur-Verifikation, Empfänger-Sicht) dokumentiert die [Webhook-Referenz](/api/webhooks) in der API-Sektion.

## Events vs. Audit vs. Notifications

Drei Systeme, drei Zwecke, oft alle drei in derselben Aktion:

| System | Zweck | Empfänger |
|---|---|---|
| [Events/Webhooks](/dev/sdk/webhooks-und-events) | externe Systeme reagieren lassen | Endpoints des Tenants |
| [Audit](/dev/sdk/audit) | unveränderlicher Nachweis "wer hat was getan" | Tenant-Admins |
| [Notifications](/dev/sdk/notifications) | Menschen in der App informieren | einzelne Nutzer:innen/Rollen |

## Weiterführend

Zurück zum [Modul-Guide](/dev/module-guide); der Runs-Harness emittiert `tool.run.completed` und `tool.run.failed` bereits für dich, eigene Events brauchst du nur für fachliche Ereignisse deines Tools.
