# Webhooks: technische Referenz

<Lead>
Webhooks sind der produktive Echtzeit-Kanal der Plattform: Bei jedem abonnierten Domain-Event sendet sie einen signierten POST an deine Endpoints. Diese Seite ist die technische Referenz; die Schritt-für-Schritt-Einrichtung steht im Help-Center unter [Webhooks einrichten](/help/howto/webhooks).
</Lead>

## Endpoints und Abonnements

- Endpoints werden pro Workspace unter **Einstellungen > Webhooks** verwaltet (Berechtigung erforderlich; standardmäßig Owner und Admins).
- Jeder Endpoint abonniert eine Auswahl von Event-Typen oder per Wildcard `*` alle aktuellen und künftigen Events.
- Pro Endpoint können eigene HTTP-Header hinterlegt werden (etwa ein API-Key deines Zielsystems). Die Signatur-Header der Plattform sind reserviert und nicht überschreibbar.
- Das Signing-Secret (`whsec_` + Base64url) wird beim Anlegen genau einmal angezeigt und ist über **Secret rotieren** ersetzbar; das alte Secret wird dabei sofort ungültig.

## Zustell-Envelope

Jede Zustellung ist ein `POST` mit `Content-Type: application/json`:

<RequestExample method="POST" path="https://deine-domain.de/hooks/reosa" language="JSON">
{`{
  "id": "5f0f9c3a-8f0a-4e6b-b0f3-2f9a1c7d4e21",
  "type": "booking.created",
  "createdAt": "2026-07-21T09:30:00.000Z",
  "data": {
    "appointmentId": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "eventTypeId": "b2c3d4e5-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
    "startsAt": "2026-08-03T09:30:00.000Z"
  }
}`}
</RequestExample>

| Feld | Bedeutung |
|---|---|
| `id` | Eindeutige Zustellungs-ID; identisch über alle Wiederholungs-Versuche. Als Idempotenz-Schlüssel verwenden. |
| `type` | Event-Typ, wortgleich zum Katalog unten. |
| `createdAt` | Entstehungszeitpunkt des Events (ISO 8601, UTC). |
| `data` | Event-spezifische Nutzdaten. Beispiel-Payloads zeigt der Event-Picker in der Verwaltung. |

## Signatur prüfen (Standard-Webhooks)

Drei Header begleiten jede Zustellung:

| Header | Inhalt |
|---|---|
| `webhook-id` | Zustellungs-ID (gleich `id` im Body) |
| `webhook-timestamp` | Unix-Sekunden des Versands |
| `webhook-signature` | `v1,<Base64-HMAC>` |

Die Signatur ist HMAC-SHA256 über die Zeichenkette `{id}.{timestamp}.{body}`. Der Schlüssel ist der Base64url-dekodierte Teil des Secrets nach dem Präfix `whsec_`.

```ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWebhook(
  secret: string, // "whsec_..."
  id: string, // webhook-id header
  timestamp: string, // webhook-timestamp header
  signature: string, // webhook-signature header, "v1,<base64>"
  rawBody: string, // unveraenderter Request-Body als String
): boolean {
  const key = Buffer.from(secret.slice("whsec_".length), "base64url");
  const expected = createHmac("sha256", key)
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest("base64");
  const given = signature.replace(/^v1,/, "");
  const a = Buffer.from(expected);
  const b = Buffer.from(given);
  return a.length === b.length && timingSafeEqual(a, b);
}
```

<Warning title="Rohen Body verwenden">
Signiere-Prüfungen müssen über den unveränderten Request-Body laufen. JSON parsen und erneut serialisieren verändert die Bytes und lässt die Prüfung fehlschlagen. Prüfe außerdem den Zeitstempel gegen ein Toleranzfenster (üblich: 5 Minuten), um Replay-Angriffe zu erschweren.
</Warning>

Fertige Standard-Webhooks-Bibliotheken existieren für alle gängigen Sprachen und übernehmen diese Schritte.

## Zustellung, Wiederholungen, Auto-Pause

- Erwartet wird eine Antwort mit Status **2xx** innerhalb des Zustell-Timeouts. Alles andere gilt als Fehlversuch.
- Wiederholungs-Zeitplan nach Fehlversuchen: **1, 5, 30, 120, 720 Minuten** (maximal 5 Versuche pro Zustellung).
- Nach **15 fehlgeschlagenen Zustellungen in Folge** (ohne zwischenzeitlichen Erfolg) pausiert der Endpoint automatisch und muss nach Behebung manuell reaktiviert werden.
- Das Zustell-Log speichert pro Versuch Status, Dauer und die ersten **2.048 Zeichen** des Antwort-Bodys. Einzelne Zustellungen lassen sich manuell erneut zustellen.
- Test-Events: generisch (`webhook.test`) oder pro Event-Typ mit dessen Beispiel-Payload.

## Event-Katalog

Wortgleiche Typen, wie sie im `type`-Feld ankommen. Die Verwaltung zeigt zu jedem Event Beschreibung und Beispiel-Payload.

| Gruppe | Typ | Auslöser |
|---|---|---|
| Objekte | `property.created` | Neues Immobilien-Objekt angelegt |
| Objekte | `property.updated` | Objekt geändert |
| Objekte | `property.sync.completed` | Objekt-Import aus onOffice durchgelaufen |
| Kontakte | `contact.sync.completed` | Adressbuch-Import aus onOffice durchgelaufen |
| Tools | `tool.installed` | Tool im Workspace installiert |
| Tools | `tool.uninstalled` | Tool deinstalliert |
| Tools | `tool.run.completed` | Tool-Durchlauf erfolgreich beendet |
| Tools | `tool.run.failed` | Tool-Durchlauf mit Fehler abgebrochen |
| Zugangsdaten-Tresor | `credential.created` | Zugangsdaten-Eintrag erstellt |
| Zugangsdaten-Tresor | `credential.updated` | Eintrag geändert |
| Zugangsdaten-Tresor | `credential.deleted` | Eintrag entfernt |
| Zugangsdaten-Tresor | `credential.shared` | Zugang intern geteilt |
| Zugangsdaten-Tresor | `credential.external_shared` | Zugang per externem Link geteilt |
| Zugangsdaten-Tresor | `credential.revealed` | Geheimnis im Klartext angezeigt |
| Zugangsdaten-Tresor | `credential.imported` | Zugangsdaten importiert |
| Zugangsdaten-Tresor | `credential.template_provisioned` | Zugangsdaten-Vorlage eingerichtet |
| DB-Leads | `db-lead.created` | Neuer Lead erfasst |
| DB-Leads | `db-lead.moved` | Lead hat die Pipeline-Stufe gewechselt |
| DB-Leads | `db-lead.qualified` | Lead als qualifiziert markiert |
| DB-Leads | `db-lead.scan.completed` | Datenbank-Scan durchgelaufen |
| DB-Leads | `db-lead.sync.completed` | Lead-Import aus onOffice durchgelaufen |
| Homestaging | `homestaging.generated` | Homestaging-Bild generiert |
| Website | `website_lead.received` | Anfrage über ein Website-Formular eingegangen |
| Website | `website_page.published` | Website-Seite live geschaltet |
| Website | `website_page.rolled_back` | Seite auf frühere Version zurückgesetzt |
| Website | `website_domain.verified` | Website-Domain verifiziert |
| Website | `website_onboarding.completed` | Website-Ersteinrichtung abgeschlossen |
| Termin-Booking | `booking.created` | Termin gebucht (Buchungsseite oder Team) |
| Termin-Booking | `booking.updated` | Status oder Details eines Termins geändert |
| Termin-Booking | `booking.cancelled` | Termin abgesagt (Gast oder Team) |
| Termin-Booking | `booking.rescheduled` | Termin auf neuen Zeitpunkt verschoben |
| Workspace und Abrechnung | `tenant.plan_changed` | Abrechnungsplan gewechselt |
| Workspace und Abrechnung | `tenant.credit_pack_purchased` | Credit-Paket gekauft |
| Workspace und Abrechnung | `tenant.recurring_pack_changed` | Wiederkehrendes Credit-Paket angepasst |
| Workspace und Abrechnung | `tenant.extended_usage_changed` | Limit für erweiterte Nutzung angepasst |
| Workspace und Abrechnung | `tenant.domain_verified` | Custom-Domain des Workspace verifiziert |
| Workspace und Abrechnung | `tenant.seat_subscribed` | Zusätzlicher Team-Sitzplatz gebucht |

<Info title="Wildcard und neue Events">
Mit dem Abonnement `*` empfängt ein Endpoint automatisch auch künftig hinzukommende Event-Typen. Baue deine Verarbeitung deshalb tolerant: Unbekannte `type`-Werte quittierst du mit 2xx und ignorierst sie.
</Info>

## Betriebs-Empfehlungen

<Checklist>
  <ChecklistItem>Signatur immer prüfen und `webhook-id` als Idempotenz-Schlüssel verwenden.</ChecklistItem>
  <ChecklistItem>Schnell mit 2xx antworten und die eigentliche Verarbeitung asynchron erledigen (Queue), damit der Zustell-Timeout nicht reißt.</ChecklistItem>
  <ChecklistItem>Reihenfolge nicht voraussetzen: bei Wiederholungen können Events außer der Reihe ankommen; `createdAt` nutzen.</ChecklistItem>
  <ChecklistItem>Endpoint-Pausierung überwachen (Zustell-Log) und nach Behebung reaktivieren.</ChecklistItem>
</Checklist>
