Webhooks
So kommt ein Ereignis bei dir an
- 1Änderung in BodoWeb, MCP oder API
- 2Outboxgleiche Transaktion
- 3Rechte-Filterdarf das Abo das lesen?
- 4Zustellungsigniert, mit Wiederholung
- 5Dein Endpunktantwortet 2xx in 10 s
GET /v1/contacts/con_4Nf7…mit deinen eigenen Rechten- Änderung in Bodo1Web, MCP oder API
- Outbox2gleiche Transaktion
- Rechte-Filter3darf das Abo das lesen?
- Zustellung4signiert, mit Wiederholung
- Dein Endpunkt5antwortet 2xx in 10 s
- 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
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
// 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 URLIn Bodo verwalten ↗: Zustell-Log, erneut senden, Secret wechseln. Nur https auf Port 443, sonst 422 WEBHOOK_URL_INVALID.
Event-Katalog
| Ereignis | Wann | Braucht Leserecht |
|---|---|---|
contact.created | Kontakt angelegt | contacts:read |
contact.updated | Kontakt geändert | |
contact.deleted | Kontakt gelöscht | |
company.created | Firma angelegt | companies:read |
company.updated | Firma geändert | |
company.deleted | Firma gelöscht | |
task.created | Aufgabe angelegt | tasks:read |
task.updated | Aufgabe geändert | |
task.completed | Aufgabe erledigt | |
task.deleted | Aufgabe gelöscht | |
document.created | Dokument fertig hochgeladen | documents:read |
document.deleted | Dokument gelöscht | |
operation.completed | Langer Lauf beendet (Ergebnis oder Fehler im Vorgang) | eigener Vorgang |
webhook.test | Test-Ereignis auf Knopfdruck | Besitzer 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
// 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
# 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
- Versuch 1sofortmit dem Ereignis
- Versuch 2+30 snach 30 s
- Versuch 3+2 minnach 2,5 min
- Versuch 4+10 minnach 12,5 min
- Versuch 5+1 hnach 1,2 h
- Versuch 6+3 hnach 4,2 h
- Versuch 7+6 hnach 10,2 h
- Versuch 8+12 hnach 22,2 h
- Versuch 9+1 Tagnach 1,9 Tage
- Versuch 10+1 Tagnach 2,9 Tage
- danach aufgegeben
| Lage | Was Bodo tut |
|---|---|
| Gestört | Nach 24 h ohne Erfolg bekommt der Besitzer eine Mail und einen Eintrag in der Glocke. |
| Deaktiviert | Nach 72 h ohne Erfolg oder bei 410 Gone schaltet Bodo den Endpunkt ab. Reaktivieren per Klick, Verpasstes per „Erneut senden“ oder updatedSince. |
| Zustell-Log | Jeder Versuch mit Status, Antwortzeit und Code, 30 Tage lang. Den Body deiner Antwort speichert Bodo nie. |
| Secret wechseln | 24 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.