Zum Inhalt springen

Webhooks

Webhooks

Bodo meldet Änderungen aktiv an deinen Endpunkt. Ein Ereignis ist dünn: Typ, ID und Zeit. Die Details holst du per API mit deinen eigenen Rechten.

So kommt ein Ereignis bei dir an

Fünf Schritte. Bodo schickt nur die Nachricht „etwas hat sich geändert“; was genau, liest du selbst nach.
  1. Änderung in BodoWeb, MCP oder API
  2. Outboxgleiche Transaktion
  3. Rechte-Filterdarf das Abo das lesen?
  4. Zustellungsigniert, mit Wiederholung
  5. Dein Endpunktantwortet 2xx in 10 s
  6. Details holen:GET /v1/contacts/con_4Nf7…mit deinen eigenen Rechten

Die Outbox schreibt das Ereignis in derselben Transaktion wie die Änderung, es geht also keins verloren. Zugestellt wird mindestens einmal, ohne feste Reihenfolge: Erkenne Doppelte an webhook-id.

Ein Ereignis, wie es ankommt

Thin Event: Typ, ID des Datensatzes und Zeit. Keine Feldwerte, keine Personendaten.
HTTP
POST https://hooks.musterfirma.de/bodo
webhook-id: evt_01JC4M8Q2R6T0V3X5Z7B9D1F3H
webhook-timestamp: 1793697262
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
User-Agent: Bodo-Webhooks/1.0
Content-Type: application/json

{
  "id": "evt_01JC4M8Q2R6T0V3X5Z7B9D1F3H",
  "type": "contact.updated",
  "createdAt": "2026-11-03T09:14:22.000Z",
  "data": {
    "object": "contact",
    "id": "con_4Nf7Kx9aB2cD3eF4gH5jK6mN7p"
  }
}

Dieselben Ereignisse liest du auch über GET /v1/events, etwa zum Nachholen nach einem Ausfall.

Endpunkt anlegen

In Bodo unter Einstellungen › Schnittstellen › Webhooks oder per API. Das Abo gehört dem Prinzipal, der es anlegt; jeder Prinzipal hat höchstens 16 Endpunkte.
TypeScript · SDK
// Portal D10 · Webhooks — create an endpoint for three event types and send the test event.
// Run: BODO_API_KEY=bodo_sa_test_… bun examples/webhook-endpoint.ts
import { Bodo } from "@bodo/api";

const bodo = new Bodo({ apiKey: process.env.BODO_API_KEY, preview: true });

const endpoint = await bodo.webhookEndpoints.create({
  name: "Musterfirma CRM-Abgleich",
  url: "https://hooks.musterfirma.de/bodo",
  eventTypes: ["contact.created", "contact.updated", "task.completed"],
});
// endpoint.secret (whsec_…) is shown exactly once: store it now
console.log(endpoint.id);

await bodo.webhookEndpoints.sendTest(endpoint.id); // sends webhook.test to your URL

In Bodo verwalten: Zustell-Log, erneut senden, Secret wechseln. Nur https auf Port 443, sonst 422 WEBHOOK_URL_INVALID.

Event-Katalog

Du bekommst ein Ereignis nur, wenn der Prinzipal deines Abos den Datensatz zum Zeitpunkt der Zustellung lesen darf.
Event-Katalog
EreignisWannBraucht Leserecht
contact.createdKontakt angelegtcontacts:read
contact.updatedKontakt geändert
contact.deletedKontakt gelöscht
company.createdFirma angelegtcompanies:read
company.updatedFirma geändert
company.deletedFirma gelöscht
task.createdAufgabe angelegttasks:read
task.updatedAufgabe geändert
task.completedAufgabe erledigt
task.deletedAufgabe gelöscht
document.createdDokument fertig hochgeladendocuments:read
document.deletedDokument gelöscht
operation.completedLanger Lauf beendet (Ergebnis oder Fehler im Vorgang)eigener Vorgang
webhook.testTest-Ereignis auf KnopfdruckBesitzer des Abos

Ereignisse aus den gesperrten Familien (Geheimnisse, Identität) gibt es nicht. Neue Ereignistypen kommen additiv, Rechnungen und Zeiten folgen; ignoriere Typen, die du nicht kennst.

Signatur prüfen · TypeScript

Immer mit dem rohen Body, nie mit geparstem JSON. Erst quittieren, dann verarbeiten.
@bodo/api/webhooks
// Portal D10 · Webhooks — verify every delivery on the raw body, acknowledge first, process after.
// Run: BODO_WEBHOOK_SECRET=whsec_… bun examples/webhook-verify.ts
import { Webhook, WebhookVerificationError } from "@bodo/api/webhooks";

const wh = new Webhook(process.env.BODO_WEBHOOK_SECRET);
const queue: string[] = [];

export async function handleBodoWebhook(request: Request): Promise<Response> {
  const body = await request.text(); // the RAW body, never re-serialized JSON
  try {
    // throws on a wrong signature or a timestamp more than 5 minutes off
    const { event } = await wh.verify(body, request.headers);
    queue.push(event.id); // acknowledge first, process from the queue
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(null, { status: 400 });
    throw error;
  }
}

export default { port: 8080, fetch: handleBodoWebhook };

Signatur prüfen · Python

Gleiche Regel: roher Body, Zeitfenster 5 Minuten.
bodo_api.webhooks
# Verify every delivery on the raw body, acknowledge first, process after.
# Run: BODO_WEBHOOK_SECRET=whsec_… python examples/webhook_verify.py
import os
from collections.abc import Mapping
from http.server import BaseHTTPRequestHandler, HTTPServer
from queue import Queue
from typing import Any

from bodo_api.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ.get("BODO_WEBHOOK_SECRET"))
events: Queue[dict[str, Any]] = Queue()


def handle(body: bytes, headers: Mapping[str, str]) -> int:
    """Status for Bodo: 204 when the delivery verifies, 400 otherwise."""
    try:
        event = wh.verify(body, headers)  # raises on a wrong signature or a timestamp > 5 min off
    except WebhookVerificationError:
        return 400
    events.put(event)  # acknowledge first, process from the queue
    return 204


class Hook(BaseHTTPRequestHandler):
    def do_POST(self) -> None:
        body = self.rfile.read(int(self.headers.get("Content-Length", "0")))  # the RAW body
        self.send_response(handle(body, dict(self.headers.items())))
        self.end_headers()


if __name__ == "__main__":
    HTTPServer(("", 8080), Hook).serve_forever()

Ohne SDK

HMAC-SHA256 über webhook-id.webhook-timestamp.body, Schlüssel ist das Secret ohne whsec_, Base64-dekodiert. Vergleich in konstanter Zeit, Zeitstempel höchstens 5 Minuten alt. Jede Bibliothek nach Standard Webhooks passt.

Wiederholungen

Antwortet dein Endpunkt nicht mit 2xx, versucht Bodo es erneut, mit wachsendem Abstand bis knapp 3 Tage.
  1. Versuch 1sofortmit dem Ereignis
  2. Versuch 2+30 snach 30 s
  3. Versuch 3+2 minnach 2,5 min
  4. Versuch 4+10 minnach 12,5 min
  5. Versuch 5+1 hnach 1,2 h
  6. Versuch 6+3 hnach 4,2 h
  7. Versuch 7+6 hnach 10,2 h
  8. Versuch 8+12 hnach 22,2 h
  9. Versuch 9+1 Tagnach 1,9 Tage
  10. Versuch 10+1 Tagnach 2,9 Tage
  11. danach aufgegeben
Ein Versuch gilt als gescheitert ohne 2xx in 10 s. Jeder Abstand streut um ±10 %.
LageWas Bodo tut
GestörtNach 24 h ohne Erfolg bekommt der Besitzer eine Mail und einen Eintrag in der Glocke.
DeaktiviertNach 72 h ohne Erfolg oder bei 410 Gone schaltet Bodo den Endpunkt ab. Reaktivieren per Klick, Verpasstes per „Erneut senden“ oder updatedSince.
Zustell-LogJeder Versuch mit Status, Antwortzeit und Code, 30 Tage lang. Den Body deiner Antwort speichert Bodo nie.
Secret wechseln24 h lang trägt webhook-signature zwei Signaturen, alt und neu. Kein Ausfall beim Wechsel.

Abos anlegen, auflisten, löschen und das Zustell-Log lesen geht ebenso über MCP und die CLI. Zum Abgleich ohne Webhooks: Pagination & Sync.