API & Integrationen

Webhooks einrichten und nutzen

Webhooks ermöglichen es externen Systemen, in Echtzeit über Änderungen in Calcutron benachrichtigt zu werden. Statt die API regelmäßig abzufragen (Polling), sendet Calcutron automatisch HTTP-POST-Requests an Ihre konfigurierte URL, sobald ein relevantes Ereignis eintritt.

Funktionsweise

Wenn in Calcutron ein Ereignis stattfindet (z. B. ein neues Angebot erstellt wird), prüft das System, ob aktive Webhook-Abonnements für diesen Event-Typ existieren. Für jedes passende Abonnement wird ein HTTP-POST-Request mit den Event-Details an die hinterlegte URL gesendet. Die Zustellung erfolgt asynchron über eine Redis Queue, sodass die Benutzeroberfläche nicht blockiert wird.

Jeder Webhook-Request enthält Sicherheits-Header zur Signaturprüfung, die Event-Details im JSON-Body und eine eindeutige Delivery-ID zur Nachverfolgung.

Event-Typen (78+)

Calcutron bietet über 78 Webhook-Event-Typen, gruppiert nach Geschäftsbereichen.

Angebots-Events (Quotation):

| Event | Beschreibung |

|---|---|

| `quotation.created` | Neues Angebot erstellt |

| `quotation.cloned` | Angebot dupliziert |

| `quotation.updated` | Angebotsdaten aktualisiert |

| `quotation.deleted` | Angebot gelöscht |

| `quotation.pdf_generated` | PDF-Export erstellt |

| `quotation.positions_reset` | Alle Positionen zurückgesetzt |

Positions-Events (allgemein):

| Event | Beschreibung |

|---|---|

| `quotation_item.added` | Position hinzugefügt (auch Bulk) |

| `quotation_item.copied` | Position kopiert |

| `quotation_item.moved` | Position verschoben |

| `quotation_item.deleted` | Position gelöscht |

| `quotation_item.quantity_updated` | Menge geändert |

| `quotation_item.position_updated` | Positionsnummer geändert |

Gruppen-Events:

| Event | Beschreibung |

|---|---|

| `quotation_item.group.created` | Neue Gruppe erstellt |

| `quotation_item.group.updated` | Gruppe aktualisiert |

| `quotation_item.group.deleted` | Gruppe gelöscht |

| `quotation_item.group.discount_updated` | Gruppenrabatt geändert |

| `quotation_item.group.etim_class_added` | ETIM-Klasse zugewiesen |

| `quotation_item.group.etim_class_removed` | ETIM-Klasse entfernt |

| `quotation_item.group.manual_sales_price_toggled` | Manueller VK an/aus |

| `quotation_item.group.manual_sales_price_updated` | Manueller VK geändert |

Produkt-Events:

| Event | Beschreibung |

|---|---|

| `quotation_item.product.created` | Neues Produkt erstellt |

| `quotation_item.product.updated` | Produkt aktualisiert |

| `quotation_item.product.deleted` | Produkt gelöscht |

| `quotation_item.product.replaced` | Produkt ersetzt |

| `quotation_item.product.offer_selected` | Lieferantenangebot ausgewählt |

| `quotation_item.product.alternative_swapped` | Alternative getauscht |

Dienstleistungs-Events:

| Event | Beschreibung |

|---|---|

| `quotation_item.service.created` | Neue Dienstleistung erstellt |

| `quotation_item.service.updated` | Dienstleistung aktualisiert |

| `quotation_item.service.deleted` | Dienstleistung gelöscht |

| `quotation_item.service.wage_updated` | Stundensatz geändert |

| `quotation_item.service.time_updated` | Zeitaufwand geändert |

Projekt-Events:

| Event | Beschreibung |

|---|---|

| `project.created` | Neues Projekt erstellt |

| `project.created_from_source` | Projekt aus Quelle erstellt (z. B. GAEB-Import) |

| `project.updated` | Projektdaten aktualisiert |

| `project.deleted` | Projekt gelöscht |

| `project.status_changed` | Projektstatus geändert |

| `project.address_updated` | Projektadresse aktualisiert |

| `project.favourite_toggled` | Favoriten-Status geändert |

Kunden-Events:

| Event | Beschreibung |

|---|---|

| `customer.created` | Neuer Kunde angelegt |

| `customer.updated` | Kundendaten aktualisiert |

| `customer.deleted` | Kunde gelöscht |

Fertigungseinheiten:

| Event | Beschreibung |

|---|---|

| `production_unit.status_changed` | Fertigungsstatus geändert |

Gruppen-Templates und Favoriten:

| Event | Beschreibung |

|---|---|

| `group.marked_as_favourite` | Als Favorit markiert |

| `group.unmarked_as_favourite` | Favorit entfernt |

| `group.marked_as_template` | Als Template markiert |

| `group.unmarked_as_template` | Template entfernt |

| `group.marked_as_production_unit` | Als Fertigungseinheit markiert |

| `group.unmarked_as_production_unit` | Fertigungseinheit entfernt |

Test:

| Event | Beschreibung |

|---|---|

| `test.ping` | Test-Event zur Endpoint-Verifikation |

Webhook-Payload-Format

Jeder Webhook-Request enthält einen JSON-Body mit folgender Grundstruktur:

```json

{

"event": "project.created",

"event_id": "evt_a1b2c3d4e5f6",

"timestamp": "2025-01-15T10:30:00Z",

"data": {

"id": "uuid-des-projekts",

"name": "Neubau Einfamilienhaus",

"status": "active",

"customerId": "uuid-des-kunden",

"createdAt": "2025-01-15T10:30:00Z"

}

}

```

Basis-Felder (immer vorhanden): `event` (Event-Typ-Bezeichnung), `event_id` (eindeutige Event-ID zur Deduplizierung) und `timestamp` (Zeitstempel im ISO-8601-Format).

Filterbare Felder (je nach Event-Typ): `project_id`, `quotation_id`, `customer_id`, `item_type` – diese Felder können beim Erstellen eines Abonnements als Filter verwendet werden, um nur relevante Events zu empfangen.

Das data-Objekt enthält die vollständigen Details der betroffenen Entität. Bei Positions-Events (`quotation_item.*`) kann das `data`-Objekt auch mehrere Items enthalten (z. B. bei Bulk-Operationen wie dem gleichzeitigen Hinzufügen mehrerer Positionen).

Abonnements einrichten

Es gibt drei Methoden, um Webhook-Abonnements zu erstellen:

Methode 1: CalcutronTrigger Node in n8n (empfohlen)

Wenn Sie n8n verwenden, ist der CalcutronTrigger Node die einfachste Methode. Er erstellt und löscht Abonnements automatisch beim Aktivieren bzw. Deaktivieren Ihres Workflows, generiert Secrets clientseitig und prüft HMAC-Signaturen automatisch. Kein manuelles Setup erforderlich – Details dazu im Abschnitt „n8n-Integration" weiter unten.

Methode 2: REST-API

Erstellen Sie ein Abonnement per POST-Request:

```

POST /api/v1/webhooks/subscriptions/

```

```json

{

"url": "https://ihre-domain.de/webhook/calcutron",

"eventType": "project.created",

"secret": "ihr_64_zeichen_hex_secret_hier",

"isActive": true,

"filters": {

"customerId": "uuid-eines-kunden"

}

}

```

Beim Erstellen generiert der Client das Secret selbst (64 Zeichen, hexadezimal) und sendet es mit dem Request. Dieses Muster folgt dem GitHub-Webhook-Modell, bei dem der Client die Kontrolle über das Secret behält.

Methode 3: Django Admin

Navigieren Sie im Admin-Panel zu Gateway → Webhook Subscriptions und erstellen Sie das Abonnement über die Benutzeroberfläche. Diese Methode eignet sich für schnelle Tests und manuelle Konfigurationen.

Filter für Abonnements:

Sie können Abonnements auf bestimmte Entitäten einschränken, sodass nur relevante Events zugestellt werden. Verfügbare Filter sind `project_id`, `quotation_id`, `customer_id`, `item_type` und `catalog_id`. Filter können vor der Erstellung über `POST /api/v1/webhooks/filters/validate/` auf Gültigkeit geprüft werden.

HMAC-SHA256-Signaturprüfung

Jeder Webhook-Request wird mit einer HMAC-SHA256-Signatur versehen. Die Prüfung dieser Signatur ist zwingend erforderlich, um sicherzustellen, dass der Request tatsächlich von Calcutron stammt und nicht manipuliert wurde.

Sicherheits-Header:

Jeder Webhook-Request enthält zwei Sicherheits-Header:

| Header | Beschreibung |

|---|---|

| `X-Calcutron-Signature` | HMAC-SHA256-Signatur im Format `sha256=<hex_digest>` |

| `X-Calcutron-Timestamp` | Unix-Timestamp des Sendezeitpunkts |

Signatur-Algorithmus:

Die Signatur wird berechnet über den String `"{timestamp}.{json_payload}"`, wobei der JSON-Payload mit sortierten Keys und ohne Leerzeichen serialisiert wird (`separators=(',', ':')`). Das verwendete Secret ist der bei der Abonnement-Erstellung festgelegte Wert.

```

signatur_string = "{timestamp}.{json_payload}"

signatur = HMAC-SHA256(secret, signatur_string)

```

Prüfung in Python (Flask/Django):

```python

import hmac

import hashlib

import json

import time

def verify_webhook(request_body, signature_header, timestamp_header, secret):

1. Timestamp-Prüfung (max. 5 Minuten)

current_time = int(time.time())

timestamp = int(timestamp_header)

if abs(current_time - timestamp) > 300:

raise ValueError("Timestamp too old - possible replay attack")

2. Signatur berechnen

payload_str = json.dumps(

json.loads(request_body),

separators=(',', ':'),

sort_keys=True

)

message = f"{timestamp}.{payload_str}"

expected = hmac.new(

secret.encode('utf-8'),

message.encode('utf-8'),

hashlib.sha256

).hexdigest()

3. Constant-Time-Vergleich

received = signature_header.replace("sha256=", "")

if not hmac.compare_digest(expected, received):

raise ValueError("Invalid signature")

return True

```

Prüfung in Node.js:

```javascript

const crypto = require('crypto');

function verifyWebhook(body, signatureHeader, timestampHeader, secret) {

// 1. Timestamp-Prüfung (max. 5 Minuten)

const currentTime = Math.floor(Date.now() / 1000);

const timestamp = parseInt(timestampHeader);

if (Math.abs(currentTime - timestamp) > 300) {

throw new Error('Timestamp too old - possible replay attack');

}

// 2. Signatur berechnen

const payload = JSON.stringify(

JSON.parse(body),

Object.keys(JSON.parse(body)).sort()

).replace(/\s/g, '');

const message = `${timestamp}.${payload}`;

const expected = crypto

.createHmac('sha256', secret)

.update(message)

.digest('hex');

// 3. Constant-Time-Vergleich

const received = signatureHeader.replace('sha256=', '');

if (!crypto.timingSafeEqual(

Buffer.from(expected), Buffer.from(received)

)) {

throw new Error('Invalid signature');

}

return true;

}

```

HTTP-Headers

Request-Headers (von Calcutron gesendet):

| Header | Beschreibung |

|---|---|

| `Content-Type` | `application/json` |

| `User-Agent` | `Calcutron-Webhooks/1.0` |

| `X-Webhook-Event` | Name des Event-Typs (z. B. `project.created`) |

| `X-Webhook-Delivery-ID` | Eindeutige ID dieser Zustellung |

| `X-Calcutron-Signature` | HMAC-SHA256-Signatur |

| `X-Calcutron-Timestamp` | Unix-Timestamp des Sendezeitpunkts |

Erwartete Response:

Ihr Endpoint sollte mit einem 2xx-Statuscode innerhalb von 10 Sekunden antworten. Der Response-Body wird ignoriert – nur der Statuscode zählt.

Zustellung und Retry-Logik

Webhook-Zustellungen erfolgen asynchron über eine Redis Queue (RQ). Falls eine Zustellung fehlschlägt, versucht Calcutron automatisch eine erneute Zustellung mit exponentiellem Backoff.

Retry-Strategie (6 Versuche insgesamt):

| Versuch | Wartezeit | Kumuliert |

|---|---|---|

| 1 (Erstversuch) | sofort | — |

| 2 | 1 Minute | 1 min |

| 3 | 5 Minuten | 6 min |

| 4 | 15 Minuten | 21 min |

| 5 | 1 Stunde | 1h 21min |

| 6 | 4 Stunden | 5h 21min |

| (letzter Versuch) | 24 Stunden | 29h 21min |

Zustellungsstatus:

| Status | Bedeutung |

|---|---|

| `PENDING` | Zustellung geplant, noch nicht ausgeführt |

| `SUCCESS` | Endpoint hat mit 2xx geantwortet |

| `RETRYING` | Zustellung fehlgeschlagen, nächster Versuch geplant |

| `FAILED` | Alle Versuche ausgeschöpft, Zustellung endgültig fehlgeschlagen |

Fehlergründe: Eine Zustellung gilt als fehlgeschlagen bei einem Non-2xx-Statuscode, Timeout (>10 Sekunden), Connection Error (Endpoint nicht erreichbar), DNS-Auflösungsfehler oder TLS/SSL-Zertifikatproblemen.

Webhook-Management über die API

Die vollständige Verwaltung von Abonnements und Zustellungen erfolgt über REST-Endpunkte.

Abonnements:

| Methode | Endpunkt | Beschreibung |

|---|---|---|

| GET | `/api/v1/webhooks/subscriptions/` | Alle Abonnements auflisten (Filter: eventType, isActive, page, pageSize) |

| POST | `/api/v1/webhooks/subscriptions/` | Neues Abonnement erstellen (Secret wird nur hier zurückgegeben) |

| PATCH | `/api/v1/webhooks/subscriptions/{id}/` | Abonnement aktualisieren (eventType ist nicht änderbar) |

| DELETE | `/api/v1/webhooks/subscriptions/{id}/` | Abonnement löschen |

Test und Zustellungen:

| Methode | Endpunkt | Beschreibung |

|---|---|---|

| POST | `/api/v1/webhooks/test/` | Sendet ein `test.ping`-Event an den angegebenen Endpoint |

| GET | `/api/v1/webhooks/deliveries/` | Zustellungshistorie abrufen (Filter: subscriptionId, status, eventType) |

Die Zustellungshistorie enthält Details wie `responseStatusCode`, `responseTimeMs`, `errorMessage` und den Zustellungsstatus – ideal für Debugging und Monitoring.

n8n-Integration

Calcutron bietet eine native Integration mit n8n über einen eigenen CalcutronTrigger Node.

CalcutronTrigger Node (empfohlen):

Der CalcutronTrigger Node automatisiert die gesamte Webhook-Verwaltung. Beim Aktivieren eines Workflows erstellt er automatisch ein Abonnement bei Calcutron mit einem clientseitig generierten Secret (64 Zeichen, hexadezimal). Beim Deaktivieren wird das Abonnement automatisch wieder gelöscht. Eingehende Requests werden automatisch per HMAC-SHA256 verifiziert. Die Subscription-Metadaten werden im Workflow Static Data gespeichert.

Sie müssen lediglich den gewünschten Event-Typ und Ihre Calcutron-API-Credentials im Node konfigurieren – alles Weitere übernimmt der Node automatisch.

Manuelle Integration (HTTP Request + Webhook Nodes):

Falls Sie den CalcutronTrigger Node nicht verwenden möchten, können Sie Webhooks auch manuell mit den Standard-n8n-Nodes einrichten. Dafür benötigen Sie einen Generic Webhook Node zum Empfang der Requests, einen Code Node für die HMAC-Signaturprüfung und HTTP Request Nodes für die API-Aufrufe.

Für die HMAC-Prüfung in n8n-Code-Nodes muss die Umgebungsvariable `NODE_FUNCTION_ALLOW_BUILTIN=crypto` gesetzt sein, damit das Node.js crypto-Modul verfügbar ist.

Calcutron stellt vorgefertigte n8n-Workflow-Templates bereit: `tools/n8n-webhook-receiver.json` (Webhook-Empfang mit HMAC-Prüfung) und `tools/n8n-api-fetch-projects.json` (API-Abfrage mit Authentifizierung).

Typische Anwendungsfälle

Webhooks eignen sich ideal für die Automatisierung von Geschäftsprozessen rund um Angebote, Projekte und Kunden.

ERP-Synchronisierung: Bei `quotation.pdf_generated` automatisch Angebotsdaten an Ihr ERP-System übertragen. Bei `project.status_changed` den Projektstatus bidirektional synchronisieren.

Workflow-Automatisierung: Bei `project.created` automatisch Aufgaben in Ihrem Projektmanagement-Tool erstellen. Bei `customer.created` den neuen Kunden in Ihrem CRM-System anlegen.

Benachrichtigungen: Bei `quotation.created` eine Slack-Nachricht an das Vertriebsteam senden. Bei `production_unit.status_changed` das Fertigungsteam benachrichtigen.

Archivierung: Bei `quotation.pdf_generated` das PDF automatisch in Ihrem Dokumentenmanagementsystem ablegen. Bei `project.deleted` eine Archivierung in externen Systemen anstoßen.

Qualitätssicherung: Bei `quotation_item.product.replaced` prüfen, ob das Ersatzprodukt den Projektanforderungen entspricht. Bei `quotation_item.group.discount_updated` ungewöhnlich hohe Rabatte zur Freigabe markieren.

Best Practices

Schnell antworten: Ihr Endpoint sollte sofort mit HTTP 200 antworten und die eigentliche Verarbeitung asynchron durchführen. Verwenden Sie eine Message Queue (z. B. Redis, RabbitMQ) für die nachgelagerte Verarbeitung.

Idempotenz sicherstellen: Verwenden Sie die `event_id` aus dem Payload zur Deduplizierung. Jedes Event hat eine eindeutige ID – speichern Sie bereits verarbeitete IDs, um doppelte Verarbeitung bei Retries zu vermeiden.

HTTPS verwenden: Webhook-URLs müssen immer HTTPS verwenden. HTTP-Endpoints werden nicht unterstützt und sind ein Sicherheitsrisiko, da Payloads und Signaturen im Klartext übertragen würden.

Secrets regelmäßig rotieren: Erstellen Sie periodisch neue Abonnements mit neuen Secrets und löschen Sie die alten. Dies begrenzt den Schaden bei einem möglichen Secret-Leak.

Monitoring einrichten: Überwachen Sie die Zustellungshistorie über `GET /api/v1/webhooks/deliveries/` und richten Sie Alerts für wiederholt fehlgeschlagene Zustellungen ein.

Troubleshooting

„Signature verification failed":

Prüfen Sie, ob das korrekte Secret verwendet wird. Stellen Sie sicher, dass der JSON-Payload mit sortierten Keys und ohne Leerzeichen serialisiert wird (`sort_keys=True`, `separators=(',', ':')`). Achten Sie auf UTF-8-Encoding in allen Schritten der Signaturberechnung.

„Timestamp too old":

Die Timestamp-Prüfung hat ein Standard-Fenster von 5 Minuten. Stellen Sie sicher, dass die Systemuhr Ihres Servers korrekt synchronisiert ist (NTP). Falls nötig, erhöhen Sie das `max_age_seconds`-Fenster.

„Connection error":

Prüfen Sie, ob Ihr Endpoint öffentlich erreichbar ist und ob Firewall-Regeln den eingehenden Traffic zulassen. Stellen Sie sicher, dass ein gültiges HTTPS-Zertifikat installiert ist.

Zustellungen prüfen:

Nutzen Sie `GET /api/v1/webhooks/deliveries/`, um die Zustellungshistorie einzusehen. Die Response enthält `errorMessage`, `responseStatusCode` und `responseTimeMs` für jede Zustellung – ideal zur Fehlerdiagnose.

Webhook Permissions

Webhook Permissions werden separat von den API Permissions pro API-Client konfiguriert. Jede Permission-Gruppe kontrolliert, welche Event-Typen abonniert werden können:

| Permission | Events |

|---|---|

| `quotation.*` | Alle Angebots- und Positions-Events |

| `project.*` | Alle Projekt-Events |

| `customer.*` | Alle Kunden-Events |

| `production_unit.*` | Fertigungseinheiten-Events |

| `group.*` | Gruppen-Template- und Favoriten-Events |

| `test.ping` | Test-Event |

Ein API-Client kann nur Events abonnieren, für die er die entsprechende Webhook Permission besitzt.

Hinweis
Wichtig
Secret sicher aufbewahren

Das Webhook-Secret wird nur bei der Erstellung übertragen und ist danach nicht mehr über die API abrufbar. Speichern Sie es sofort sicher ab – z. B. in einer Umgebungsvariablen oder einem Secret-Manager. Ohne das Secret können Sie eingehende Webhooks nicht verifizieren.

Beispiel
Warnung
Tipp
Replay-Attack-Schutz

Die Timestamp-Prüfung (maximal 5 Minuten Alter) verhindert Replay-Angriffe, bei denen ein abgefangener Request erneut gesendet wird. Verwenden Sie immer einen Constant-Time-Vergleich (hmac.compare\_digest in Python, crypto.timingSafeEqual in Node.js), um Timing-Angriffe zu verhindern.