Skip to content

Webhooks

Webhooks

Bodo reports changes to your endpoint. An event is thin: type, id and time. You fetch the details through the API with your own rights.

How an event reaches you

Five steps. Bodo only sends the message “something changed”; you read what changed yourself.
  1. Change in Bodoweb, MCP or API
  2. Outboxsame transaction
  3. Permission filtermay the subscription read it?
  4. Deliverysigned, with retries
  5. Your endpointanswers 2xx within 10 s
  6. 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

Thin event: type, record id and time. No field values, no personal data.
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"
  }
}

You read the same events through GET /v1/events, for example to catch up after an outage.

Create an endpoint

In Bodo under Settings › Interfaces › Webhooks, or through the API. The subscription belongs to the principal that creates it; each principal has at most 16 endpoints.
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

Manage in Bodo: delivery log, resend, rotate the secret. Only https on port 443, otherwise 422 WEBHOOK_URL_INVALID.

Event catalog

You only get an event when the principal of your subscription may read the record at delivery time.
Event catalog
EventWhenNeeds read access
contact.createdContact createdcontacts:read
contact.updatedContact updated
contact.deletedContact deleted
company.createdCompany createdcompanies:read
company.updatedCompany updated
company.deletedCompany deleted
task.createdTask createdtasks:read
task.updatedTask updated
task.completedTask completed
task.deletedTask deleted
document.createdDocument uploaded and checkeddocuments:read
document.deletedDocument deleted
operation.completedLong run finished (result or error in the operation)own operation
webhook.testTest event on demandowner 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

Always on the raw body, never on parsed JSON. Acknowledge first, process after.
@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 };

Verify the signature · Python

Same rule: raw body, a window of 5 minutes.
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()

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

When your endpoint does not answer with a 2xx, Bodo tries again with growing gaps for almost 3 days.
  1. Attempt 1at oncewith the event
  2. Attempt 2+30 safter 30 s
  3. Attempt 3+2 minafter 2.5 min
  4. Attempt 4+10 minafter 12.5 min
  5. Attempt 5+1 hafter 1.2 h
  6. Attempt 6+3 hafter 4.2 h
  7. Attempt 7+6 hafter 10.2 h
  8. Attempt 8+12 hafter 22.2 h
  9. Attempt 9+1 dayafter 1.9 days
  10. Attempt 10+1 dayafter 2.9 days
  11. then given up
An attempt fails without a 2xx within 10 s. Every gap varies by ±10 %.
SituationWhat Bodo does
FailingAfter 24 h without success the owner gets an email and an entry in the bell.
DisabledAfter 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 logEvery attempt with status, response time and code, for 30 days. Bodo never stores the body of your answer.
Rotate the secretFor 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.