Webhooks
How an event reaches you
- 1Change in Bodoweb, MCP or API
- 2Outboxsame transaction
- 3Permission filtermay the subscription read it?
- 4Deliverysigned, with retries
- 5Your endpointanswers 2xx within 10 s
GET /v1/contacts/con_4Nf7…with your own rights- Change in Bodo1web, MCP or API
- Outbox2same transaction
- Permission filter3may the subscription read it?
- Delivery4signed, with retries
- Your endpoint5answers 2xx within 10 s
- Fetch details:
GET /v1/contacts/con_4Nf7…with your own rights
The outbox writes the event in the same transaction as the change, so none gets lost. Delivery is at least once, without a fixed order: spot duplicates by webhook-id.
An event as it arrives
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"
}
}You read the same events through GET /v1/events, for example to catch up after an outage.
Create an endpoint
// 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 URLManage in Bodo ↗: delivery log, resend, rotate the secret. Only https on port 443, otherwise 422 WEBHOOK_URL_INVALID.
Event catalog
| Event | When | Needs read access |
|---|---|---|
contact.created | Contact created | contacts:read |
contact.updated | Contact updated | |
contact.deleted | Contact deleted | |
company.created | Company created | companies:read |
company.updated | Company updated | |
company.deleted | Company deleted | |
task.created | Task created | tasks:read |
task.updated | Task updated | |
task.completed | Task completed | |
task.deleted | Task deleted | |
document.created | Document uploaded and checked | documents:read |
document.deleted | Document deleted | |
operation.completed | Long run finished (result or error in the operation) | own operation |
webhook.test | Test event on demand | owner of the subscription |
Events of the blocked families (secrets, identity) do not exist. New event types are additive, invoices and time entries follow; ignore types you do not know.
Verify the signature · 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 };Verify the signature · 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()Without an SDK
HMAC-SHA256 over webhook-id.webhook-timestamp.body; the key is the secret without whsec_, Base64-decoded. Compare in constant time, timestamps at most 5 minutes old. Any Standard Webhooks library fits.
Retries
- Attempt 1at oncewith the event
- Attempt 2+30 safter 30 s
- Attempt 3+2 minafter 2.5 min
- Attempt 4+10 minafter 12.5 min
- Attempt 5+1 hafter 1.2 h
- Attempt 6+3 hafter 4.2 h
- Attempt 7+6 hafter 10.2 h
- Attempt 8+12 hafter 22.2 h
- Attempt 9+1 dayafter 1.9 days
- Attempt 10+1 dayafter 2.9 days
- then given up
| Situation | What Bodo does |
|---|---|
| Failing | After 24 h without success the owner gets an email and an entry in the bell. |
| Disabled | After 72 h without success or on 410 Gone Bodo switches the endpoint off. Re-enable with one click, catch up with “Resend” or updatedSince. |
| Delivery log | Every attempt with status, response time and code, for 30 days. Bodo never stores the body of your answer. |
| Rotate the secret | For 24 h webhook-signature carries two signatures, old and new. No outage during the change. |
Creating, listing and deleting subscriptions and reading the delivery log work through MCP and the CLI as well. Syncing without webhooks: Pagination & sync.