API - Webhooks Admin
📕 Für Administratoren und Entwickler.
Webhooks ermöglichen es, Echtzeit-Benachrichtigungen über Aenderungen in spiritflow an externe Systeme zu senden. Wenn ein Ereignis eintritt (z.B. eine Aufgabe wird erstellt oder ein Kunde wird aktualisiert), sendet spiritflow einen HTTP POST-Request an die konfigurierten Webhook-Endpunkte.
Wichtig: Webhooks werden sowohl bei Aenderungen über die API als auch bei Aenderungen über die spiritflow-Oberflaeche ausgeloest.
Event-Typen
Aufgaben-Events
| Event | Beschreibung |
|---|---|
task.created | Neue Aufgabe wurde erstellt |
task.updated | Aufgaben-Felder wurden geändert |
task.status_changed | Aufgaben-Status wurde geändert |
task.assigned | Bearbeiter einer Aufgabe wurde geändert |
task.deleted | Aufgabe wurde gelöscht/archiviert |
Hinweis: Bei Aufgaben kann ein einzelner Vorgang mehrere Events ausloesen (Dual-Delivery). Zum Beispiel loest eine Status-Aenderung sowohl task.updated als auch task.status_changed aus.
Projekt-Events
| Event | Beschreibung |
|---|---|
project.created | Neues Projekt wurde erstellt |
project.updated | Projekt-Felder wurden geändert |
project.archived | Projekt wurde archiviert/gelöscht |
Kunden-Events
| Event | Beschreibung |
|---|---|
customer.created | Neuer Kunde wurde erstellt |
customer.updated | Kunden-Felder wurden geändert |
customer.archived | Kunde wurde archiviert/gelöscht |
CTI-Events (Telefonie)
| Event | Beschreibung |
|---|---|
cti.incoming_call | Eingehender Anruf wurde gemeldet |
cti.dial_requested | Anruf wurde über Click-to-Dial angefordert |
Hinweis: CTI-Events werden ausgeloest, wenn die CTI-Integration aktiv ist. cti.dial_requested wird gesendet, wenn ein Benutzer in der spiritflow-Oberflaeche auf eine Telefonnummer klickt. Die CTI-Middleware empfaengt diesen Webhook und initiiert den Anruf über die Telefonanlage.
Webhook-Payload-Format
Jeder Webhook-Request wird als HTTP POST mit einem JSON-Body gesendet. Alle Payloads folgen demselben Schema:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"event": "task.updated",
"created_at": "2025-03-01T10:30:00Z",
"source": "api",
"actor": {
"id": 42,
"type": "api_key",
"name": "Mein API Key"
},
"data": {
"id": 1,
"title": "Webseite erstellen",
"status": "IN_PROGRESS",
"..."
},
"changes": {
"status": {
"from": "PLANNED",
"to": "IN_PROGRESS"
},
"assigneeId": {
"from": null,
"to": 5
}
}
}
Payload-Felder
| Feld | Typ | Beschreibung |
|---|---|---|
id | string | Eindeutige Event-ID (UUID) |
event | string | Event-Typ (z.B. task.created, project.updated) |
created_at | string | ISO 8601 Zeitstempel in UTC |
source | string | Quelle des Events: "api" (API-Aenderung) oder "ui" (Oberflaeche) |
actor.id | number | ID des Ausloeser-Benutzers |
actor.type | string | Typ des Ausloeser: "api_key" oder "user" |
actor.name | string | Name des API-Keys oder des Benutzers |
data | object | Vollstaendiges Objekt nach der Aenderung (gleiche Struktur wie die jeweilige API-Response) |
changes | object | Geaenderte Felder mit from/to-Werten. Leer bei *.created-Events. |
data-Objekt je nach Event-Typ
| Event-Praefix | data-Objekt | Referenz |
|---|---|---|
task.* | PublicTaskDto | Siehe Aufgaben-Endpunkte |
project.* | PublicProjectDto | Siehe Projekt-Endpunkte |
customer.* | PublicCustomerDto | Siehe Kunden-Endpunkte |
HTTP-Header
Jeder Webhook-Request enthält folgende Header:
| Header | Beschreibung | Beispiel |
|---|---|---|
Content-Type | Immer application/json | application/json |
User-Agent | Absender-Kennung | spiritflow-Webhooks/1.0 |
X-spiritflow-Event | Event-Typ | task.updated |
X-spiritflow-Delivery | Eindeutige Delivery-ID (UUID) | a1b2c3d4-... |
X-spiritflow-Signature | HMAC-SHA256 Signatur | sha256=abc123... |
Signatur-Verifizierung
Jeder Webhook-Request wird mit einer HMAC-SHA256-Signatur versehen, um die Authentizitaet und Integritaet der Nachricht sicherzustellen.
Algorithmus:
- Den vollständigen Request-Body als UTF-8-String nehmen
- HMAC-SHA256 mit dem Webhook-Secret als Schlüssel berechnen
- Das Ergebnis als Hex-String formatieren
- Mit dem Wert aus dem
X-spiritflow-Signature-Header vergleichen (nach Entfernen dessha256=-Praefixes)
Beispiel (Node.js):
const crypto = require('crypto');
function verifySignature(payload, secret, signatureHeader) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
const receivedSignature = signatureHeader.replace('sha256=', '');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}
Beispiel (Python):
import hmac
import hashlib
def verify_signature(payload: bytes, secret: str, signature_header: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
received = signature_header.replace('sha256=', '')
return hmac.compare_digest(expected, received)
Sicherheitshinweis: Das Webhook-Secret wird nur einmalig bei der Erstellung des Webhooks angezeigt. Verwenden Sie stets einen timing-safe Vergleich, um Timing-Angriffe zu vermeiden.
Retry-Verhalten
Wenn ein Webhook-Endpunkt nicht mit einem 2xx-Statuscode antwortet, versucht spiritflow die Zustellung automatisch erneut.
| Versuch | Verzoegerung |
|---|---|
| 1 | Sofort |
| 2 | 1 Minute |
| 3 | 5 Minuten |
| 4 | 30 Minuten |
| 5 | 2 Stunden |
Nach 5 erfolglosen Versuchen wird die Zustellung als fehlgeschlagen markiert.
Automatische Deaktivierung: Wenn ein Webhook-Endpunkt 15 aufeinanderfolgende Fehler erreicht (über alle Events hinweg), wird er automatisch deaktiviert. Er kann manuell über die API oder die Oberflaeche wieder aktiviert werden.
Timeout: Webhook-Endpunkte müssen innerhalb von 10 Sekunden antworten, andernfalls gilt die Zustellung als fehlgeschlagen.
Webhooks verwalten (Public API)
Webhooks können vollständig über die Public API verwaltet werden. Erforderliche Scopes: webhooks:read und/oder webhooks:write.
Basis-URL: /api/v1/webhooks
Hinweis: Jeder API-Key sieht nur die Webhooks, die mit ihm erstellt wurden.
Limit: Pro Mandant sind maximal 10 Webhooks zulässig. Bei Überschreitung antwortet der Server mit 409 Conflict.
Verfuegbare Event-Typen abrufen
GET /api/v1/webhooks/event-types
Scope: webhooks:read
Antwort:
{
"data": [
"task.created",
"task.updated",
"task.status_changed",
"task.assigned",
"task.deleted",
"project.created",
"project.updated",
"project.archived",
"customer.created",
"customer.updated",
"customer.archived",
"cti.incoming_call",
"cti.dial_requested"
]
}
Webhooks auflisten
GET /api/v1/webhooks
Scope: webhooks:read
Antwort:
{
"data": [
{
"id": 1,
"name": "Mein Webhook",
"url": "https://example.com/webhook",
"eventTypes": ["task.created", "task.updated", "project.created"],
"description": "Benachrichtigung bei neuen Aufgaben und Projekten",
"isActive": true,
"consecutiveFailures": 0,
"lastSuccessAt": "2025-03-01T10:00:00Z",
"lastFailureAt": null,
"lastFailureReason": null,
"createdAt": "2025-02-15T08:00:00Z",
"updatedAt": "2025-03-01T10:00:00Z"
}
]
}
Einzelnen Webhook abrufen
GET /api/v1/webhooks/{id}
Scope: webhooks:read
Antwort: Einzelnes Webhook-Objekt im data-Feld (gleiche Struktur wie in der Liste).
Webhook erstellen
POST /api/v1/webhooks
Scope: webhooks:write
Request-Body:
{
"name": "Mein Webhook",
"url": "https://example.com/webhook",
"eventTypes": ["task.created", "task.updated", "project.created"],
"description": "Benachrichtigung bei neuen Aufgaben"
}
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
name | string | Ja | Name des Webhooks (max. 100 Zeichen) |
url | string | Ja | Ziel-URL (max. 2048 Zeichen, muss mit http:// oder https:// beginnen) |
eventTypes | string[] | Ja | Mindestens ein Event-Typ |
description | string | Nein | Beschreibung (max. 500 Zeichen) |
Antwort (201 Created):
{
"data": {
"id": 1,
"name": "Mein Webhook",
"url": "https://example.com/webhook",
"eventTypes": ["task.created", "task.updated", "project.created"],
"description": "Benachrichtigung bei neuen Aufgaben",
"isActive": true,
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"createdAt": "2025-03-01T10:00:00Z",
"updatedAt": "2025-03-01T10:00:00Z"
}
}
Wichtig: Das secret wird nur einmalig bei der Erstellung angezeigt. Speichern Sie es sicher ab — es wird für die Signatur-Verifizierung benötigt und kann später nicht mehr abgerufen werden.
Webhook aktualisieren
PUT /api/v1/webhooks/{id}
Scope: webhooks:write
Request-Body:
{
"name": "Aktualisierter Name",
"url": "https://example.com/new-webhook-url",
"eventTypes": ["task.created", "project.created", "customer.created"],
"description": "Aktualisierte Beschreibung"
}
Alle Felder sind optional. Nur angegebene Felder werden aktualisiert.
Antwort (200 OK): Aktualisiertes Webhook-Objekt im data-Feld.
Webhook löschen
DELETE /api/v1/webhooks/{id}
Scope: webhooks:write
Antwort: 204 No Content
Webhooks verwalten (Interne API)
Zusaetzlich zur Public API stehen über die interne API weitere Verwaltungsfunktionen zur Verfuegung (Testen, Aktivieren/Deaktivieren, Zustellungshistorie). Zugriff erfordert die Rolle TENANT_ADMIN und Session-basierte Authentifizierung (JWT-Cookie).
Basis-URL: /api/webhooks
Webhook aktivieren
PATCH /api/webhooks/{id}/enable
Antwort (200 OK): Aktualisiertes Webhook-Objekt mit isActive: true.
Webhook deaktivieren
PATCH /api/webhooks/{id}/disable
Antwort (200 OK): Aktualisiertes Webhook-Objekt mit isActive: false.
Webhook testen
Sendet ein Test-Ping-Event an den Webhook-Endpunkt.
POST /api/webhooks/{id}/test
Antwort:
{
"success": true,
"httpStatus": 200,
"durationMs": 150,
"responseBody": "OK",
"errorMessage": null
}
| Feld | Typ | Beschreibung |
|---|---|---|
success | boolean | Ob der Endpunkt mit 2xx geantwortet hat |
httpStatus | number? | HTTP-Statuscode der Antwort (null bei Verbindungsfehler) |
durationMs | number? | Antwortzeit in Millisekunden |
responseBody | string? | Antwort-Body (max. 500 Zeichen) |
errorMessage | string? | Fehlermeldung bei Verbindungsfehler |
Der Test-Payload hat das Event ping:
{
"id": "a1b2c3d4-...",
"event": "ping",
"created_at": "2025-03-01T10:30:00Z",
"source": "test",
"data": {
"message": "This is a test webhook delivery from spiritflow."
}
}
Zustellungs-Historie abrufen
GET /api/webhooks/{id}/deliveries?page=0&size=20
Query-Parameter:
| Parameter | Typ | Standard | Beschreibung |
|---|---|---|---|
page | int | 0 | Seite (0-basiert) |
size | int | 20 | Einträge pro Seite (max. 100) |
Antwort:
{
"content": [
{
"id": 1,
"eventId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"eventType": "task.created",
"status": "DELIVERED",
"httpStatus": 200,
"errorMessage": null,
"attempt": 1,
"requestDurationMs": 150,
"createdAt": "2025-03-01T10:30:00Z",
"deliveredAt": "2025-03-01T10:30:01Z",
"nextRetryAt": null
}
],
"totalElements": 42,
"totalPages": 3,
"page": 0,
"size": 20
}
Delivery-Status-Werte:
| Status | Beschreibung |
|---|---|
PENDING | Zustellung steht aus |
DELIVERED | Erfolgreich zugestellt |
FAILED | Zustellung endgültig fehlgeschlagen |
RETRYING | Erneuter Zustellversuch geplant |