spiritflow Public API v1
Überblick
Die spiritflow Public API ermöglicht die programmatische Integration von Aufgaben, Kunden, Projekten und Rechnungen in externe Systeme. Die API ist REST-basiert und gibt JSON-Antworten zurück.
Basis-URL:
https://app.spiritflow.team/api/v1
Datumsformat: Alle Zeitstempel werden im ISO 8601-Format in UTC zurückgegeben (Beispiel: 2025-03-01T10:30:00Z). Datumsfelder ohne Uhrzeit (z.B. dueDate bei Projekten) werden als YYYY-MM-DD dargestellt.
Mandantenisolation: Jeder API-Key ist an genau einen Mandanten gebunden. Anfragen werden automatisch auf die Daten des jeweiligen Mandanten beschränkt.
Authentifizierung
Die Public API authentifiziert alle Anfragen über einen API-Key, der als HTTP-Header übermittelt wird.
Header-Format
X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
API-Key erstellen
API-Keys werden in der spiritflow-Anwendung unter Einstellungen > Integrationen > API-Keys erstellt. Nur Tenant-Administratoren können API-Keys erstellen und verwalten.
Pro Mandant sind maximal 10 API-Keys zulässig.
Sicherheitshinweis: Der vollständige API-Key wird nur einmalig bei der Erstellung angezeigt. Bewahre ihn sicher auf. Es ist nicht möglich, den Schlüssel später erneut einzusehen.
Fehler bei Authentifizierung
| Situation | HTTP-Status | Fehlercode |
|---|---|---|
| API-Key fehlt oder leer | 401 | UNAUTHORIZED |
| API-Key ungültig oder widerrufen | 401 | INVALID_API_KEY |
| API-Key abgelaufen | 401 | API_KEY_EXPIRED |
| IP-Adresse nicht erlaubt | 403 | IP_NOT_ALLOWED |
| Scope fehlt | 403 | ACCESS_DENIED |
Response-Format
Alle Antworten folgen einem einheitlichen Hüllformat.
Erfolg - Einzelobjekt
{
"data": {
"id": 42,
"title": "Webseite für Firma XYZ erstellen",
"status": "IN_PROGRESS"
},
"meta": {
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2025-03-01T10:30:00Z"
}
}
Erfolg - Liste mit Pagination
{
"data": [
{
"id": 42,
"title": "Webseite für Firma XYZ erstellen",
"status": "IN_PROGRESS"
}
],
"pagination": {
"page": 0,
"size": 20,
"totalElements": 57,
"totalPages": 3
},
"meta": {
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2025-03-01T10:30:00Z"
}
}
Fehler
{
"error": {
"code": "NOT_FOUND",
"message": "Task with ID 999 not found for this tenant.",
"status": 404,
"details": {
"id": 999
}
},
"meta": {
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2025-03-01T10:30:00Z"
}
}
Pagination
Alle Listen-Endpunkte unterstützen Pagination über Query-Parameter:
| Parameter | Typ | Standard | Beschreibung |
|---|---|---|---|
page | Integer | 0 | Seitennummer (0-basiert) |
size | Integer | 20 | Einträge pro Seite (max. 100) |
sort | String | abhängig vom Endpunkt | Sortierfeld und Richtung, Format: feld,richtung |
Beispiele für sort:
createdAt,desc- Neueste zuersttitle,asc- Alphabetisch aufsteigenddueDate,asc- Frühestes Fälligkeitsdatum zuerst
Scopes
Jeder API-Key trägt eine oder mehrere Berechtigungen (Scopes). Die verfügbaren Scopes sind:
| Scope | Beschreibung | Erlaubte Operationen |
|---|---|---|
tasks:read | Aufgaben lesen | GET-Anfragen auf /api/v1/tasks |
tasks:write | Aufgaben schreiben | POST, PUT, PATCH, DELETE auf /api/v1/tasks |
customers:read | Kunden lesen | GET-Anfragen auf /api/v1/customers |
customers:write | Kunden schreiben | POST, PUT, DELETE auf /api/v1/customers |
projects:read | Projekte lesen | GET-Anfragen auf /api/v1/projects |
projects:write | Projekte schreiben | POST, PUT, DELETE auf /api/v1/projects |
invoices:read | Rechnungen lesen | GET-Anfragen auf /api/v1/invoices (Read-Only) |
interactions:read | Interaktionen lesen | GET-Anfragen auf /api/v1/customers/{id}/interactions |
interactions:write | Interaktionen schreiben | POST auf /api/v1/customers/{id}/interactions |
cti:read | CTI-Daten lesen | GET-Anfragen auf /api/v1/cti/lookup und /api/v1/cti/device-mappings |
cti:write | CTI-Events senden | POST auf /api/v1/cti/events/incoming-call |
users:read | Benutzer lesen | GET-Anfragen auf /api/v1/users |
users:write | Benutzer anlegen | POST auf /api/v1/users, POST auf /api/v1/users/{id}/avatar |
time_entries:read | Zeiteinträge lesen | GET-Anfragen auf /api/v1/time-entries |
time_entries:write | Zeiteinträge schreiben | POST, PUT, DELETE auf /api/v1/time-entries |
vacation:read | Urlaubsanträge lesen | GET-Anfragen auf /api/v1/vacation-requests |
vacation:write | Urlaubsanträge erstellen | POST auf /api/v1/vacation-requests |
sick_leave:read | Krankmeldungen lesen | GET-Anfragen auf /api/v1/sick-leaves |
sick_leave:write | Krankmeldungen erstellen | POST auf /api/v1/sick-leaves |
tags:read | Tags lesen | GET-Anfragen auf /api/v1/tags |
tags:write | Tags erstellen | POST auf /api/v1/tags |
webhooks:read | Webhooks lesen | GET-Anfragen auf /api/v1/webhooks |
webhooks:write | Webhooks verwalten | POST, PUT, DELETE auf /api/v1/webhooks |
receipts:read | Belege lesen | GET-Anfragen auf /api/v1/receipts |
receipts:write | Belege erstellen und bearbeiten | POST, PATCH, DELETE auf /api/v1/receipts |
notifications:write | Benachrichtigungen senden | POST auf /api/v1/notifications, /bulk, /broadcast |
tenant:read | Mandantendaten lesen | GET-Anfragen auf /api/v1/tenant |
tenant:write | Mandantendaten ändern | PUT-Anfragen auf /api/v1/tenant |
DSGVO-Hinweis: Die CTI-Scopes (
cti:read,cti:write) ermöglichen den Zugriff auf Kundendaten (Telefonnummern, Namen) durch externe Systeme. Stell sicher, dass die CTI-Middleware in deinem Auftragsverarbeitungsvertrag (AVV) abgedeckt ist.
DSGVO-Hinweis: Krankmeldungen (
sick_leave:read,sick_leave:write) sind Gesundheitsdaten und unterliegen besonderem Schutz. Dieser Scope ist bewusst vom Urlaubs-Scope getrennt.
Hinweis: tasks:write schließt NICHT automatisch tasks:read ein. Für vollständigen Zugriff brauchst du beide Scopes. Gleiches gilt für Kunden und Projekte.
Sonderregel: Der Endpunkt GET /api/v1/projects/{id}/tasks erfordert sowohl projects:read als auch tasks:read.
API-Key Introspection
GET /api/v1/auth/introspect
Gibt Metadaten über den aktuell verwendeten API-Key zurück. Erfordert keinen speziellen Scope — jeder gültige API-Key kann sich selbst abfragen.
Antwort-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
apiKeyId | number | ID des API-Keys |
apiKeyName | string | Name des API-Keys (z.B. “AI-Worker”) |
tenantId | number | ID des zugehörigen Mandanten |
createdByUserId | number | ID des Erstellers |
scopes | string[] | Liste der zugewiesenen Scopes |
expiresAt | string? | Ablaufzeitpunkt (ISO 8601), null = kein Ablauf |
excludedProjectIds | number[] | IDs der ausgeschlossenen Projekte |
cURL-Beispiel:
curl "https://app.spiritflow.team/api/v1/auth/introspect" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Antwort:
{
"data": {
"apiKeyId": 3,
"apiKeyName": "AI-Worker",
"tenantId": 1,
"createdByUserId": 2,
"scopes": ["tasks:read", "tasks:write", "users:read"],
"expiresAt": null,
"excludedProjectIds": [7, 12]
},
"meta": {
"requestId": "...",
"timestamp": "2025-03-01T10:30:00Z"
}
}
API-Keys verwalten
Diese Endpunkte sind nur für Tenant-Administratoren zugänglich und erfordern eine normale JWT-Authentifizierung (kein API-Key).
Basis-Pfad: /api/api-keys
API-Keys auflisten
GET /api/api-keys
Antwort:
[
{
"id": 1,
"name": "Produktionssystem",
"keyPrefix": "sf_live_abc1",
"scopes": ["tasks:read", "tasks:write", "projects:read"],
"isActive": true,
"expiresAt": "2026-01-01T00:00:00Z",
"lastUsedAt": "2025-03-01T09:15:00Z",
"lastUsedIp": "203.0.113.42",
"allowedIps": "203.0.113.0/24",
"createdAt": "2025-01-15T08:00:00Z"
}
]
Einzelnen API-Key abrufen
GET /api/api-keys/{id}
Antwort: Einzelnes ApiKeyDto-Objekt (wie oben)
API-Key erstellen
POST /api/api-keys
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
name | String | Ja | Bezeichnung des Keys (max. 100 Zeichen) |
scopes | String[] | Ja | Mindestens ein gültiger Scope |
expiresAt | Instant | Nein | Ablaufdatum (ISO 8601 UTC), null = kein Ablauf |
allowedIps | String | Nein | IP-Whitelist (z.B. "203.0.113.0/24,198.51.100.5") |
Beispiel-Request:
{
"name": "ERP-Integration Produktiv",
"scopes": ["tasks:read", "tasks:write", "customers:read"],
"expiresAt": "2026-12-31T23:59:59Z",
"allowedIps": "203.0.113.0/24"
}
Antwort (201 Created):
{
"id": 3,
"name": "ERP-Integration Produktiv",
"apiKey": "sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"keyPrefix": "sf_live_xxxx",
"scopes": ["tasks:read", "tasks:write", "customers:read"],
"expiresAt": "2026-12-31T23:59:59Z",
"createdAt": "2025-03-01T10:30:00Z"
}
Wichtig: Das Feld
apiKeyim Response enthält den vollständigen Schlüssel im Klartext. Dieser wird nur einmalig bei der Erstellung zurückgegeben.
cURL-Beispiel:
curl -X POST https://app.spiritflow.team/api/api-keys \
-H "Authorization: Bearer <jwt-token>" \
-H "Content-Type: application/json" \
-d '{
"name": "ERP-Integration Produktiv",
"scopes": ["tasks:read", "tasks:write", "customers:read"],
"expiresAt": "2026-12-31T23:59:59Z"
}'
API-Key aktualisieren
PUT /api/api-keys/{id}
Request-Body (alle Felder optional):
| Feld | Typ | Beschreibung |
|---|---|---|
name | String | Neue Bezeichnung (max. 100 Zeichen) |
scopes | String[] | Neue Scope-Liste (ersetzt bestehende) |
allowedIps | String | Neue IP-Whitelist |
Antwort (200 OK): Aktualisiertes ApiKeyDto-Objekt
API-Key widerrufen
DELETE /api/api-keys/{id}
Antwort: 204 No Content
Ein widerrufener Key kann nicht reaktiviert werden. Erstelle bei Bedarf einen neuen Key.
Aufgaben (Tasks)
Basis-Pfad: /api/v1/tasks
Aufgaben-Status-Werte
| Status | Bedeutung |
|---|---|
BACKLOG | Backlog (kein Assignee erforderlich) |
DRAFT | Entwurf (kein Assignee erforderlich) |
PLANNED | Geplant |
IN_PROGRESS | In Bearbeitung |
READY_FOR_REVIEW | Bereit zur Prüfung (erfordert einen Prüfer/Reviewer) |
IN_REVIEW | In Prüfung |
COMPLETED | Abgeschlossen |
BLOCKED | Blockiert |
REJECTED | Abgelehnt |
ON_HOLD | Pausiert |
Hinweis: Alle Status außer
BACKLOGundDRAFTerfordern einen zugewiesenen Benutzer (assigneeId). Wird ein Task ohne Assignee auf einen anderen Status gesetzt, gibt die API einen400 VALIDATION_ERRORzurück.
Aufgaben-Prioritäts-Werte
| Priorität | Bedeutung |
|---|---|
LOW | Niedrig |
MEDIUM | Mittel |
HIGH | Hoch |
CRITICAL | Kritisch |
Aufgaben auflisten
GET /api/v1/tasks
Scope: tasks:read
Query-Parameter:
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
status | String | Nein | Filtern nach Status (z.B. IN_PROGRESS) |
priority | String | Nein | Filtern nach Priorität (z.B. HIGH) |
assigneeId | Long | Nein | Filtern nach zugewiesenem Benutzer (ID) |
projectId | Long | Nein | Filtern nach Projekt (ID) |
archived | Boolean | Nein | Archivierte einschließen (Standard: false) |
search | String | Nein | Volltextsuche in Titel und Beschreibung (case-insensitive) |
searchNotes | Boolean | Nein | Wenn true, wird die Suche auf Kommentare/Notizen erweitert (Standard: false) |
createdAfter | Instant | Nein | Nur Aufgaben, die nach diesem Zeitpunkt erstellt wurden (ISO 8601 UTC) |
createdBefore | Instant | Nein | Nur Aufgaben, die vor diesem Zeitpunkt erstellt wurden (ISO 8601 UTC) |
updatedAfter | Instant | Nein | Nur Aufgaben, die nach diesem Zeitpunkt aktualisiert wurden (ISO 8601 UTC) |
updatedBefore | Instant | Nein | Nur Aufgaben, die vor diesem Zeitpunkt aktualisiert wurden (ISO 8601 UTC) |
dueDateAfter | Instant | Nein | Nur Aufgaben mit Fälligkeitsdatum nach diesem Zeitpunkt (ISO 8601 UTC) |
dueDateBefore | Instant | Nein | Nur Aufgaben mit Fälligkeitsdatum vor diesem Zeitpunkt (ISO 8601 UTC) |
plannedStartAfter | Instant | Nein | Nur Aufgaben mit geplantem Start nach diesem Zeitpunkt (ISO 8601 UTC) |
plannedStartBefore | Instant | Nein | Nur Aufgaben mit geplantem Start vor diesem Zeitpunkt (ISO 8601 UTC) |
page | Integer | Nein | Seite (Standard: 0) |
size | Integer | Nein | Einträge pro Seite, max. 100 (Standard: 20) |
sort | String | Nein | Sortierung (Standard: createdAt,desc) |
Antwort (200 OK):
{
"data": [
{
"id": 42,
"ticketNumber": "PROJ-0042",
"title": "Webseite für ACME GmbH erstellen",
"description": "Komplette Neuentwicklung der Unternehmenswebseite.",
"status": "IN_PROGRESS",
"priority": "HIGH",
"progress": 45,
"assigneeId": 7,
"reviewerId": 3,
"ownerId": 2,
"projectId": 5,
"projectName": "Website-Relaunch ACME",
"dueDate": "2025-04-30T22:00:00Z",
"plannedStartDate": "2025-03-01T00:00:00Z",
"completedAt": null,
"timeBudgetMinutes": 4800,
"timeSpentMinutes": 1320,
"archived": false,
"tags": [
{ "id": 1, "name": "Frontend", "color": "#3F51B5" },
{ "id": 2, "name": "Prio", "color": "#F44336" }
],
"createdAt": "2025-01-15T08:30:00Z",
"updatedAt": "2025-02-28T14:15:00Z"
}
],
"pagination": {
"page": 0,
"size": 20,
"totalElements": 57,
"totalPages": 3
},
"meta": {
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2025-03-01T10:30:00Z"
}
}
cURL-Beispiel:
curl -X GET "https://app.spiritflow.team/api/v1/tasks?status=IN_PROGRESS&priority=HIGH&page=0&size=20" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Aufgabe abrufen
GET /api/v1/tasks/{id}
Scope: tasks:read
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID der Aufgabe |
Antwort (200 OK): Vollständiges PublicTaskDto-Objekt (gleiche Struktur wie in der Liste)
| HTTP-Status | Code | Beschreibung |
|---|---|---|
| 404 | NOT_FOUND | Aufgabe nicht gefunden oder gehört nicht zu diesem Mandanten |
cURL-Beispiel:
curl -X GET "https://app.spiritflow.team/api/v1/tasks/42" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Aufgabe nach Ticketnummer abrufen
GET /api/v1/tasks/by-ticket-number/{ticketNumber}
Scope: tasks:read
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
ticketNumber | String | Ticketnummer im Format PREFIX-NUMMER, z.B. PROJ-0042 oder SPIR-0276 |
Antwort: Identisch mit GET /api/v1/tasks/{id}.
Fehler-Codes:
| HTTP-Status | Code | Beschreibung |
|---|---|---|
| 404 | NOT_FOUND | Aufgabe mit dieser Ticketnummer nicht gefunden |
cURL-Beispiel:
curl -X GET "https://app.spiritflow.team/api/v1/tasks/by-ticket-number/PROJ-0042" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Aufgabe erstellen
POST /api/v1/tasks
Scope: tasks:write
Request-Body:
| Feld | Typ | Pflicht | Standard | Beschreibung |
|---|---|---|---|---|
title | String | Ja | - | Titel der Aufgabe (max. 500 Zeichen) |
description | String | Nein | null | Beschreibung |
status | String | Nein | BACKLOG | Status (siehe Status-Werte). Achtung: Alle Status außer BACKLOG und DRAFT erfordern eine assigneeId. |
priority | String | Nein | MEDIUM | Priorität (siehe Prioritäts-Werte) |
assigneeId | Long | Nein | null | ID des zugewiesenen Benutzers |
reviewerId | Long | Nein | null | ID des Prüfers |
projectId | Long | Nein | null | ID des zugehörigen Projekts |
dueDate | Instant | Nein | null | Fälligkeitsdatum (ISO 8601 UTC) |
plannedStartDate | Instant | Nein | null | Geplantes Startdatum (ISO 8601 UTC) |
timeBudgetMinutes | Integer | Nein | null | Zeitbudget in Minuten |
tagIds | Long[] | Nein | [] | Liste der Tag-IDs |
Beispiel-Request:
{
"title": "Angebot für Mustermann GmbH erstellen",
"description": "Detailliertes Angebot für die Neugestaltung der Firmenwebseite inkl. CMS.",
"status": "PLANNED",
"priority": "HIGH",
"assigneeId": 7,
"reviewerId": 3,
"projectId": 5,
"dueDate": "2025-04-15T22:00:00Z",
"plannedStartDate": "2025-03-10T00:00:00Z",
"timeBudgetMinutes": 480,
"tagIds": [1, 4]
}
Antwort (201 Created): Vollständiges PublicTaskDto-Objekt
| HTTP-Status | Code | Beschreibung |
|---|---|---|
| 400 | VALIDATION_ERROR | Pflichtfeld fehlt oder ungültig |
| 404 | NOT_FOUND | Referenziertes Projekt, Benutzer oder Tag nicht gefunden |
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/tasks" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "Angebot für Mustermann GmbH erstellen",
"priority": "HIGH",
"assigneeId": 7,
"dueDate": "2025-04-15T22:00:00Z"
}'
Aufgabe aktualisieren
PUT /api/v1/tasks/{id}
Scope: tasks:write
Request-Body (alle Felder optional):
| Feld | Typ | Beschreibung |
|---|---|---|
title | String | Neuer Titel (max. 500 Zeichen) |
description | String | Neue Beschreibung (null = löschen) |
status | String | Neuer Status |
priority | String | Neue Priorität |
assigneeId | Long | Neue Zuweisung (null = entfernen) |
reviewerId | Long | Neuer Prüfer (null = entfernen) |
projectId | Long | Neues Projekt (null = aus Projekt entfernen) |
dueDate | Instant | Neues Fälligkeitsdatum (null = entfernen) |
plannedStartDate | Instant | Neues geplantes Startdatum (null = entfernen) |
timeBudgetMinutes | Integer | Neues Zeitbudget in Minuten |
progress | Integer | Fortschritt in Prozent (0-100) |
tagIds | Long[] | Tag-IDs (ersetzt alle bisherigen Tags) |
Beispiel-Request:
{
"status": "IN_PROGRESS",
"progress": 25,
"assigneeId": 8
}
Antwort (200 OK): Aktualisiertes PublicTaskDto-Objekt
cURL-Beispiel:
curl -X PUT "https://app.spiritflow.team/api/v1/tasks/42" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"status": "IN_PROGRESS",
"progress": 25
}'
Aufgabe löschen (archivieren)
DELETE /api/v1/tasks/{id}
Scope: tasks:write
Antwort: 204 No Content
Hinweis: Aufgaben werden nicht endgültig gelöscht, sondern archiviert. Archivierte Aufgaben können mit
archived=trueabgerufen werden.
cURL-Beispiel:
curl -X DELETE "https://app.spiritflow.team/api/v1/tasks/42" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Aufgaben-Status aktualisieren
PATCH /api/v1/tasks/{id}/status
Scope: tasks:write
Request-Body:
{
"status": "COMPLETED"
}
Antwort (200 OK): Aktualisiertes PublicTaskDto-Objekt
cURL-Beispiel:
curl -X PATCH "https://app.spiritflow.team/api/v1/tasks/42/status" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"status": "COMPLETED"}'
Aufgaben-Bearbeiter aktualisieren
PATCH /api/v1/tasks/{id}/assignee
Scope: tasks:write
Beispiel-Request (Bearbeiter setzen):
{
"assigneeId": 9
}
Beispiel-Request (Zuweisung aufheben):
{
"assigneeId": null
}
Antwort (200 OK): Aktualisiertes PublicTaskDto-Objekt
cURL-Beispiel:
curl -X PATCH "https://app.spiritflow.team/api/v1/tasks/42/assignee" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"assigneeId": 9}'
Kommentare einer Aufgabe lesen
GET /api/v1/tasks/{id}/notes
Scope: tasks:read
Gibt alle Kommentare einer Aufgabe zurück, sortiert nach Erstellungsdatum (neueste zuerst).
Antwort-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
id | number | Eindeutige ID des Kommentars |
content | string | Inhalt des Kommentars |
noteType | string | Typ: COMMENT, FIELD_REPORT oder SYSTEM |
authorId | number | ID des Autors |
authorName | string | Anzeigename des Autors |
createdAt | string | Erstellungszeitpunkt (ISO 8601, UTC) |
updatedAt | string | Letzte Aktualisierung (ISO 8601, UTC) |
cURL-Beispiel:
curl "https://app.spiritflow.team/api/v1/tasks/42/notes" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Kommentar erstellen
POST /api/v1/tasks/{id}/notes
Scope: tasks:write
Erstellt einen neuen Kommentar zu einer Aufgabe. Der Autor ist der Inhaber des API-Keys.
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
content | string | Ja | Inhalt des Kommentars |
noteType | string | Nein | Typ des Kommentars (Standard: COMMENT). Erlaubt: COMMENT, FIELD_REPORT |
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/tasks/42/notes" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"content": "Review abgeschlossen, sieht gut aus!"}'
Anhänge (Attachments)
Basis-Pfad: /api/v1/tasks/{taskId}/attachments
Anhänge einer Aufgabe auflisten
GET /api/v1/tasks/{taskId}/attachments
Scope: tasks:read
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
taskId | Long | ID der Aufgabe |
Antwort-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
id | number | Eindeutige ID des Anhangs |
fileName | string | Angezeigter Dateiname |
fileSize | number | Dateigröße in Bytes |
contentType | string | MIME-Typ der Datei |
attachedBy | string? | Anzeigename des Uploaders |
createdAt | string | Zeitpunkt des Hochladens (ISO 8601, UTC) |
cURL-Beispiel:
curl "https://app.spiritflow.team/api/v1/tasks/42/attachments" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Anhang herunterladen
GET /api/v1/tasks/{taskId}/attachments/{attachmentId}/download
Scope: tasks:read
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
taskId | Long | ID der Aufgabe |
attachmentId | Long | ID des Anhangs |
Antwort (200 OK): Datei als Binärdaten
Response-Header:
| Header | Beispielwert |
|---|---|
Content-Type | application/pdf (je nach Dateityp) |
Content-Disposition | attachment; filename="rechnung.pdf" |
cURL-Beispiel:
curl "https://app.spiritflow.team/api/v1/tasks/42/attachments/7/download" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-o "heruntergeladene-datei.pdf"
Anhang hochladen
POST /api/v1/tasks/{taskId}/attachments
Scope: tasks:write
Content-Type: multipart/form-data
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
taskId | Long | ID der Aufgabe |
Formular-Felder:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
file | File | Ja | Die hochzuladende Datei |
description | String | Nein | Optionale Beschreibung der Datei |
Besonderes Verhalten:
- Dateien werden vor dem Speichern auf Viren geprüft (ClamAV)
- Hash-basierte Deduplizierung: Wird dieselbe Datei erneut hochgeladen, wird der bestehende S3-Speicher wiederverwendet
- Die maximale Dateigröße und das Speicherkontingent richten sich nach dem Lizenzplan des Mandanten
Antwort (201 Created):
{
"data": {
"id": 7,
"fileName": "angebot.pdf",
"fileSize": 204800,
"contentType": "application/pdf",
"attachedBy": "Max Mustermann",
"createdAt": "2025-03-01T10:30:00Z"
},
"meta": {
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2025-03-01T10:30:00Z"
}
}
Fehler-Codes:
| HTTP-Status | Code | Beschreibung |
|---|---|---|
| 400 | BAD_REQUEST | Datei zu groß oder Speicherkontingent erschöpft |
| 404 | NOT_FOUND | Aufgabe nicht gefunden |
| 422 | VIRUS_DETECTED | Datei wurde als infiziert erkannt |
| 503 | VIRUS_SCAN_FAILED | Virenscanner nicht verfügbar |
| 503 | VIRUS_SCAN_TIMEOUT | Virenscanner hat nicht rechtzeitig geantwortet |
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/tasks/42/attachments" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-F "file=@/pfad/zur/datei.pdf" \
-F "description=Angebot für Kunde"
Benutzer (Users)
Basis-Pfad: /api/v1/users
Benutzer auflisten
GET /api/v1/users
Scope: users:read
Gibt alle aktiven Benutzer des Mandanten zurück.
Antwort-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
id | number | Eindeutige ID des Benutzers |
displayName | string | Anzeigename |
email | string | E-Mail-Adresse |
roles | string[] | Rollen des Benutzers |
cURL-Beispiel:
curl "https://app.spiritflow.team/api/v1/users" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Antwort:
{
"data": [
{
"id": 2,
"displayName": "Max Mustermann",
"email": "max.mustermann@firma.de",
"roles": ["TENANT_ADMIN"]
},
{
"id": 5,
"displayName": "Lisa Schmidt",
"email": "lisa.schmidt@firma.de",
"roles": ["USER"]
}
],
"pagination": {
"page": 0,
"size": 2,
"totalElements": 2,
"totalPages": 1
},
"meta": {
"requestId": "...",
"timestamp": "2025-03-01T10:30:00Z"
}
}
Einzelnen Benutzer abrufen
GET /api/v1/users/{id}
Scope: users:read
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Benutzers |
Antwort (200 OK): Identisch mit den Einträgen der Benutzerliste.
Benutzer anlegen
POST /api/v1/users
Scope: users:write
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
username | string | Ja | Benutzername (eindeutig innerhalb des Mandanten) |
email | string | Ja | E-Mail-Adresse (systemweit eindeutig) |
firstName | string | Ja | Vorname |
lastName | string | Ja | Nachname |
password | string | Ja | Passwort (wird serverseitig gehasht) |
roles | string[] | Nein | Rollen, Standard: ["USER"]. Erlaubt: USER, TEAM_LEADER, PROJECT_MANAGER, EXTERNAL_MEMBER, FIELD_USER |
employmentStartDate | string | Nein | Beschäftigungsbeginn (YYYY-MM-DD) |
weeklyWorkingHours | number | Nein | Wochenstunden (überschreibt Mandanten-Standard) |
workingDays | string[] | Nein | Arbeitstage, z.B. ["MONDAY","TUESDAY","WEDNESDAY","THURSDAY","FRIDAY"] |
Antwort (201 Created): Gleiche Felder wie bei der Benutzerliste.
Fehler-Codes:
| HTTP-Status | Code | Beschreibung |
|---|---|---|
| 402 | LICENSE_SEAT_LIMIT_REACHED | Lizenz-Sitzplatzlimit erreicht |
| 409 | CONFLICT | E-Mail oder Benutzername bereits vergeben |
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/users" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"username": "j.müller",
"email": "j.müller@firma.de",
"firstName": "Julia",
"lastName": "Müller",
"password": "sicheresPasswort123!",
"roles": ["USER"]
}'
Profilbild hochladen
POST /api/v1/users/{id}/avatar
Scope: users:write
Request: Multipart Form-Data mit dem Feld file (JPEG, PNG, GIF oder WebP).
Antwort (200 OK):
{
"data": {
"url": "/api/attachments/profile-photos/abc123.jpg"
}
}
Benutzer aktualisieren
PATCH /api/v1/users/{id}
Aktualisiert einen bestehenden Benutzer (PATCH-Semantik). Nur angegebene Felder werden geändert. null-Werte bedeuten “nicht ändern”.
Scope: users:write
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Benutzers |
Request-Body (alle Felder optional):
| Feld | Typ | Beschreibung |
|---|---|---|
firstName | String | Vorname (max. 100 Zeichen) |
lastName | String | Nachname (max. 100 Zeichen) |
displayName | String | Anzeigename (max. 201 Zeichen). Wird in firstName und lastName aufgeteilt, wenn diese nicht explizit angegeben sind. |
username | String | Benutzername (eindeutig innerhalb des Mandanten, max. 100 Zeichen) |
roles | String[] | Rollen. Erlaubt: USER, TEAM_LEADER, PROJECT_MANAGER, EXTERNAL_MEMBER, FIELD_USER. Nicht erlaubt: TENANT_ADMIN, SUPER_ADMIN. |
employmentStartDate | String | Eintrittsdatum (YYYY-MM-DD) |
employmentEndDate | String | Austrittsdatum (YYYY-MM-DD). null = noch beschäftigt |
weeklyWorkingHours | number | Individuelle Wochenarbeitsstunden |
workingDays | String[] | Arbeitstage, z.B. ["MONDAY","TUESDAY","WEDNESDAY","THURSDAY","FRIDAY"] |
defaultHourlyRate | number | Standard-Stundensatz in Euro |
annualVacationDays | number | Individuelle Urlaubstage pro Jahr |
jobTitle | String | Berufsbezeichnung (max. 100 Zeichen) |
mobilePhone | String | Mobiltelefonnummer (max. 20 Zeichen) |
enabled | boolean | Benutzerkonto deaktivieren. Nur false erlaubt (sperren). Reaktivierung archivierter Benutzer ist über die Public API nicht möglich. |
Antwort (200 OK): Aktualisiertes Benutzerobjekt (gleiche Struktur wie bei der Benutzerliste)
Fehler-Codes:
| HTTP-Status | Code | Beschreibung |
|---|---|---|
| 400 | VALIDATION_ERROR | Ungültige Feldwerte oder verbotene Rolle |
| 404 | NOT_FOUND | Benutzer nicht gefunden |
| 409 | CONFLICT | Benutzername bereits vergeben |
cURL-Beispiel:
curl -X PATCH "https://app.spiritflow.team/api/v1/users/7" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"jobTitle": "Senior-Entwickler",
"weeklyWorkingHours": 38.5,
"roles": ["USER", "TEAM_LEADER"]
}'
Benutzer deaktivieren
DELETE /api/v1/users/{id}
Deaktiviert einen Benutzer (archiviert ihn). Der Tenant-Admin kann nicht gelöscht werden.
Scope: users:write
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Benutzers |
Antwort: 204 No Content
Fehler-Codes:
| HTTP-Status | Code | Beschreibung |
|---|---|---|
| 403 | ACCESS_DENIED | Versuch den Tenant-Admin zu löschen |
| 404 | NOT_FOUND | Benutzer nicht gefunden oder gehört nicht zu diesem Mandanten |
cURL-Beispiel:
curl -X DELETE "https://app.spiritflow.team/api/v1/users/7" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Kunden (Customers)
Basis-Pfad: /api/v1/customers
Kunden-Status-Werte
| Status | Bedeutung |
|---|---|
ACTIVE | Aktiver Kunde |
LEAD | Lead / Interessent |
PROSPECT | Potenzieller Kunde |
LOST | Verlorener Kunde |
INACTIVE | Inaktiv |
SUSPENDED | Gesperrt |
ARCHIVED | Archiviert |
Kunden-Typen
| Typ | Bedeutung |
|---|---|
COMPANY | Unternehmen |
INDIVIDUAL | Privatperson |
Kunden auflisten
GET /api/v1/customers
Scope: customers:read
Query-Parameter:
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
status | String | Nein | Filtern nach Status (z.B. ACTIVE) |
customerType | String | Nein | Filtern nach Typ (COMPANY oder INDIVIDUAL) |
search | String | Nein | Volltextsuche in Name, E-Mail, Kundennummer |
page | Integer | Nein | Seite (Standard: 0) |
size | Integer | Nein | Einträge pro Seite, max. 100 (Standard: 20) |
sort | String | Nein | Sortierung (Standard: name,asc) |
Antwort (200 OK):
{
"data": [
{
"id": 12,
"name": "ACME GmbH",
"customerNumber": "KD-00042",
"email": "kontakt@acme-gmbh.de",
"phone": "+49 40 123456",
"mobile": null,
"street": "Musterstrasse 1",
"postalCode": "20095",
"city": "Hamburg",
"country": "Deutschland",
"website": "https://www.acme-gmbh.de",
"description": "Langjähriger Kunde seit 2018.",
"taxNumber": "48/123/45678",
"vatId": "DE123456789",
"status": "ACTIVE",
"industry": "Software",
"customerType": "COMPANY",
"customerSince": "2018-06-01T00:00:00Z",
"createdAt": "2018-06-01T09:00:00Z",
"updatedAt": "2025-01-15T11:30:00Z"
}
],
"pagination": { "page": 0, "size": 20, "totalElements": 34, "totalPages": 2 },
"meta": { "requestId": "...", "timestamp": "2025-03-01T10:30:00Z" }
}
cURL-Beispiel:
curl -X GET "https://app.spiritflow.team/api/v1/customers?status=ACTIVE&sort=name,asc" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Kunden abrufen
GET /api/v1/customers/{id}
Scope: customers:read
Antwort (200 OK): Einzelnes PublicCustomerDto-Objekt (gleiche Struktur wie in der Liste)
Kunden erstellen
POST /api/v1/customers
Scope: customers:write
Request-Body:
| Feld | Typ | Pflicht | Standard | Beschreibung |
|---|---|---|---|---|
name | String | Ja | - | Name des Kunden (max. 255 Zeichen) |
customerNumber | String | Nein | null | Kundennummer (max. 50 Zeichen) |
email | String | Nein | null | E-Mail-Adresse (max. 255 Zeichen) |
phone | String | Nein | null | Telefonnummer (max. 50 Zeichen) |
mobile | String | Nein | null | Mobilnummer (max. 50 Zeichen) |
street | String | Nein | null | Strasse und Hausnummer |
postalCode | String | Nein | null | Postleitzahl (max. 10 Zeichen) |
city | String | Nein | null | Stadt (max. 100 Zeichen) |
country | String | Nein | null | Land (max. 100 Zeichen) |
website | String | Nein | null | Webseite (max. 100 Zeichen) |
description | String | Nein | null | Notizen / Beschreibung |
taxNumber | String | Nein | null | Steuernummer (max. 50 Zeichen) |
vatId | String | Nein | null | USt-IdNr. (max. 50 Zeichen) |
status | String | Nein | ACTIVE | Status (siehe Status-Werte) |
industry | String | Nein | null | Branche (max. 100 Zeichen) |
customerType | String | Nein | COMPANY | Kundentyp |
Beispiel-Request:
{
"name": "Mustermann GmbH",
"customerNumber": "KD-00099",
"email": "info@mustermann-gmbh.de",
"phone": "+49 30 987654",
"street": "Berliner Allee 42",
"postalCode": "10115",
"city": "Berlin",
"country": "Deutschland",
"vatId": "DE987654321",
"status": "LEAD",
"industry": "Handwerk",
"customerType": "COMPANY"
}
Antwort (201 Created): Vollständiges PublicCustomerDto-Objekt
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/customers" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Mustermann GmbH",
"email": "info@mustermann-gmbh.de",
"status": "LEAD",
"customerType": "COMPANY"
}'
Kunden aktualisieren
PUT /api/v1/customers/{id}
Scope: customers:write
Gleiche Felder wie beim Erstellen, alle optional. Nur übermittelte Felder werden aktualisiert.
Beispiel-Request:
{
"status": "ACTIVE",
"email": "neuemail@mustermann-gmbh.de",
"phone": "+49 30 112233"
}
Antwort (200 OK): Aktualisiertes PublicCustomerDto-Objekt
Kunden löschen (archivieren)
DELETE /api/v1/customers/{id}
Scope: customers:write
Antwort: 204 No Content
Kunden-Logo hochladen
POST /api/v1/customers/{id}/logo
Content-Type: multipart/form-data
Lädt ein Logo für den Kunden hoch. Das Bild wird auf max. 512px verkleinert. Unterstützte Formate: JPEG, PNG, WebP.
Scope: customers:write
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Kunden |
Form-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
file | multipart | Bilddatei (max. 5 MB) |
Antwort (200 OK):
{
"data": {
"logoUrl": "/api/attachments/customer-logos/50_1234567890.png"
}
}
Die Logo-URL ist öffentlich ohne Authentifizierung abrufbar.
Kunden-Logo entfernen
DELETE /api/v1/customers/{id}/logo
Entfernt das Logo eines Kunden.
Scope: customers:write
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Kunden |
Antwort: 204 No Content
Kontaktpersonen auflisten
Kontaktpersonen sind Ansprechpartner eines Kunden (z.B. Geschäftsführer, Projektleiter, Buchhaltung).
GET /api/v1/customers/{customerId}/contacts
Scope: customers:read
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
customerId | Long | ID des Kunden |
Antwort-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
id | number | Eindeutige ID der Kontaktperson |
customerId | number | ID des zugehörigen Kunden |
salutation | string? | Anrede |
firstName | string | Vorname |
lastName | string | Nachname |
position | string? | Position / Funktion |
department | string? | Abteilung |
email | string? | E-Mail-Adresse |
phone | string? | Telefonnummer |
mobile | string? | Mobilnummer |
isPrimary | boolean | Ist Hauptansprechpartner |
notes | string? | Notizen |
preferredLanguage | string? | Bevorzugte Sprache (ISO-Code) |
createdAt | string | Erstellungszeitpunkt (ISO 8601) |
updatedAt | string | Letzter Änderungszeitpunkt (ISO 8601) |
Einzelne Kontaktperson abrufen
GET /api/v1/customers/{customerId}/contacts/{contactId}
Scope: customers:read
Kontaktperson erstellen
POST /api/v1/customers/{customerId}/contacts
Scope: customers:write
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
firstName | string | Ja | Vorname (max. 100 Zeichen) |
lastName | string | Ja | Nachname (max. 100 Zeichen) |
salutation | string | Nein | Anrede (max. 20 Zeichen) |
position | string | Nein | Position (max. 200 Zeichen) |
department | string | Nein | Abteilung (max. 100 Zeichen) |
email | string | Nein | E-Mail-Adresse |
phone | string | Nein | Telefonnummer (max. 50 Zeichen) |
mobile | string | Nein | Mobilnummer (max. 50 Zeichen) |
isPrimary | boolean | Nein | Hauptansprechpartner (Standard: false) |
notes | string | Nein | Notizen |
preferredLanguage | string | Nein | Sprachcode (Standard: de) |
Antwort (201 Created): Gleiche Felder wie bei der Kontaktliste.
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/customers/15/contacts" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Andrea",
"lastName": "Bauer",
"position": "Geschäftsführerin",
"email": "a.bauer@acme.de",
"phone": "+49 221 12345678",
"isPrimary": true
}'
Projekte (Projects)
Basis-Pfad: /api/v1/projects
Projekt-Status-Werte
| Status | Bedeutung |
|---|---|
PLANNED | Geplant |
ACTIVE | Aktiv |
COMPLETED | Abgeschlossen |
ARCHIVED | Archiviert |
Projekt-Prioritäts-Werte
| Priorität | Bedeutung |
|---|---|
LOW | Niedrig |
NORMAL | Normal |
HIGH | Hoch |
CRITICAL | Kritisch |
Projekte auflisten
GET /api/v1/projects
Scope: projects:read
Query-Parameter:
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
status | String | Nein | Filtern nach Status (z.B. ACTIVE) |
search | String | Nein | Volltextsuche in Titel und Beschreibung |
page | Integer | Nein | Seite (Standard: 0) |
size | Integer | Nein | Einträge pro Seite, max. 100 (Standard: 20) |
sort | String | Nein | Sortierung (Standard: createdAt,desc) |
Antwort (200 OK):
{
"data": [
{
"id": 5,
"title": "Website-Relaunch ACME",
"description": "Komplette Neugestaltung der Unternehmenswebseite für ACME GmbH.",
"numberPrefix": "ACME",
"color": "#3F51B5",
"status": "ACTIVE",
"priority": "HIGH",
"startDate": "2025-01-01",
"endDate": "2025-06-30",
"progress": 32,
"ownerId": 2,
"assigneeId": 7,
"teamMemberIds": [7, 8, 9],
"customerName": "ACME GmbH",
"createdAt": "2024-12-15T09:00:00Z",
"updatedAt": "2025-02-28T16:45:00Z"
}
],
"pagination": { "page": 0, "size": 20, "totalElements": 12, "totalPages": 1 },
"meta": { "requestId": "...", "timestamp": "2025-03-01T10:30:00Z" }
}
cURL-Beispiel:
curl -X GET "https://app.spiritflow.team/api/v1/projects?status=ACTIVE" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Projekt abrufen
GET /api/v1/projects/{id}
Scope: projects:read
Antwort (200 OK): Einzelnes PublicProjectDto-Objekt
Projekt erstellen
POST /api/v1/projects
Scope: projects:write
Request-Body:
| Feld | Typ | Pflicht | Standard | Beschreibung |
|---|---|---|---|---|
title | String | Ja | - | Projekttitel (max. 255 Zeichen) |
numberPrefix | String | Ja | - | Ticket-Präfix (Grossbuchstaben/Ziffern, 1-10 Zeichen) |
description | String | Nein | null | Projektbeschreibung |
color | String | Nein | #3F51B5 | Farbe als Hex-Code (#RRGGBB) |
status | String | Nein | PLANNED | Status |
priority | String | Nein | NORMAL | Priorität |
startDate | LocalDate | Nein | null | Startdatum (YYYY-MM-DD) |
endDate | LocalDate | Nein | null | Enddatum (YYYY-MM-DD) |
assigneeId | Long | Nein | null | ID des Projektleiters |
teamMemberIds | Long[] | Nein | [] | IDs der Projektmitglieder |
customerId | Long | Nein | null | ID des zugeordneten Kunden |
Beispiel-Request:
{
"title": "Online-Shop Rollout Baeckerei Schneider",
"numberPrefix": "BSNDR",
"description": "Implementierung eines WooCommerce-basierten Online-Shops.",
"color": "#FF9800",
"status": "PLANNED",
"priority": "HIGH",
"startDate": "2025-04-01",
"endDate": "2025-08-31",
"assigneeId": 7,
"teamMemberIds": [7, 8, 10],
"customerId": 12
}
Antwort (201 Created): Vollständiges PublicProjectDto-Objekt
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/projects" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "Online-Shop Rollout Baeckerei Schneider",
"numberPrefix": "BSNDR",
"color": "#FF9800",
"startDate": "2025-04-01",
"endDate": "2025-08-31"
}'
Projekt aktualisieren
PUT /api/v1/projects/{id}
Scope: projects:write
Request-Body (alle Felder optional):
| Feld | Typ | Beschreibung |
|---|---|---|
title | String | Neuer Titel (max. 255 Zeichen) |
description | String | Neue Beschreibung |
color | String | Neue Farbe (#RRGGBB) |
status | String | Neuer Status |
priority | String | Neue Priorität |
startDate | LocalDate | Neues Startdatum |
endDate | LocalDate | Neues Enddatum |
assigneeId | Long | Neuer Projektleiter |
teamMemberIds | Long[] | Neue Teammitglieder (ersetzt alle bisherigen) |
customerId | Long | Neuer Kunde |
Beispiel-Request:
{
"status": "ACTIVE",
"endDate": "2025-09-30",
"teamMemberIds": [7, 8, 10, 11]
}
Antwort (200 OK): Aktualisiertes PublicProjectDto-Objekt
Projekt löschen (archivieren)
DELETE /api/v1/projects/{id}
Scope: projects:write
Antwort: 204 No Content
Projekt-Logo hochladen
POST /api/v1/projects/{id}/logo
Content-Type: multipart/form-data
Lädt ein Logo für das Projekt hoch. Das Bild wird auf max. 512px verkleinert. Unterstützte Formate: JPEG, PNG, WebP.
Scope: projects:write
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Projekts |
Form-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
file | multipart | Bilddatei (max. 5 MB) |
Antwort (200 OK):
{
"data": {
"logoUrl": "/api/attachments/project-logos/13_1234567890.png"
}
}
Die Logo-URL ist öffentlich ohne Authentifizierung abrufbar.
Projekt-Logo entfernen
DELETE /api/v1/projects/{id}/logo
Entfernt das Logo eines Projekts.
Scope: projects:write
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Projekts |
Antwort: 204 No Content
Aufgaben eines Projekts auflisten
GET /api/v1/projects/{id}/tasks
Scope: projects:read und tasks:read (beide Scopes erforderlich)
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
page | Integer | Nein | Seite (Standard: 0) |
size | Integer | Nein | Einträge pro Seite, max. 100 (Standard: 20) |
sort | String | Nein | Sortierung (Standard: createdAt,desc) |
Antwort (200 OK): Liste von PublicTaskDto-Objekten
cURL-Beispiel:
curl -X GET "https://app.spiritflow.team/api/v1/projects/5/tasks?page=0&size=50" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Rechnungen (Invoices)
Basis-Pfad: /api/v1/invoices
Hinweis: Rechnungen sind über die Public API nur lesbar (Read-Only). Das Erstellen, Bearbeiten und Löschen von Rechnungen ist ausschließlich über die spiritflow-Anwendung möglich.
Rechnungs-Status-Werte
| Status | Bedeutung |
|---|---|
DRAFT | Entwurf |
SENT | Versendet |
PAID | Bezahlt |
OVERDUE | Überfällig |
CANCELLED | Storniert |
Rechnungen auflisten
GET /api/v1/invoices
Scope: invoices:read
Query-Parameter:
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
status | String | Nein | Filtern nach Status (z.B. PAID) |
customerId | Long | Nein | Filtern nach Kunden-ID |
search | String | Nein | Suche in Rechnungsnummer und Titel |
page | Integer | Nein | Seite (Standard: 0) |
size | Integer | Nein | Einträge pro Seite, max. 100 (Standard: 20) |
sort | String | Nein | Sortierung (Standard: invoiceDate,desc) |
Antwort (200 OK):
{
"data": [
{
"id": 87,
"invoiceNumber": "RE-2025-0042",
"title": "Website-Relaunch ACME - Phase 1",
"description": "Abrechnung der Konzeptionsphase.",
"customerName": "ACME GmbH",
"projectName": "Website-Relaunch ACME",
"netAmount": 3200.00,
"taxRate": 19.00,
"taxAmount": 608.00,
"grossAmount": 3808.00,
"currency": "EUR",
"invoiceDate": "2025-02-01",
"dueDate": "2025-03-01",
"paidDate": "2025-02-20",
"status": "PAID",
"isOverdue": false,
"isCancellationInvoice": false,
"lineItems": [
{
"description": "Konzeption und Wireframes",
"quantity": 16.00,
"unit": "Std",
"unitPrice": 120.00,
"netAmount": 1920.00
},
{
"description": "Design-Entwuerfe (3 Varianten)",
"quantity": 1.00,
"unit": "Pauschal",
"unitPrice": 1280.00,
"netAmount": 1280.00
}
],
"createdAt": "2025-01-31T16:00:00Z",
"updatedAt": "2025-02-20T10:00:00Z"
}
],
"pagination": { "page": 0, "size": 20, "totalElements": 28, "totalPages": 2 },
"meta": { "requestId": "...", "timestamp": "2025-03-01T10:30:00Z" }
}
cURL-Beispiel:
curl -X GET "https://app.spiritflow.team/api/v1/invoices?status=PAID&sort=invoiceDate,desc" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Rechnung abrufen
GET /api/v1/invoices/{id}
Scope: invoices:read
Antwort (200 OK): Einzelnes PublicInvoiceDto-Objekt
Hinweis: Bei Rechnungsentwürfen (Status
DRAFT) istinvoiceNumbernull.
Rechnungs-PDF herunterladen
GET /api/v1/invoices/{id}/pdf
Scope: invoices:read
Antwort (200 OK): PDF-Datei als Binärdaten
Response-Header:
| Header | Beispielwert |
|---|---|
Content-Type | application/pdf |
Content-Disposition | attachment; filename="RE-2025-0042.pdf" |
cURL-Beispiel:
curl -X GET "https://app.spiritflow.team/api/v1/invoices/87/pdf" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-o "rechnung-RE-2025-0042.pdf"
Belege (Receipts)
Basis-Pfad: /api/v1/receipts
Belege repräsentieren eingehende Rechnungen, Kassenbelege und andere Ausgabendokumente, die durch einen Genehmigungs-Workflow laufen.
Beleg-Status-Werte
| Status | Bedeutung |
|---|---|
UPLOADED | Hochgeladen, noch nicht eingereicht |
SUBMITTED | Eingereicht, wartet auf Prüfung |
IN_REVIEW | In Prüfung |
APPROVED | Genehmigt |
SETTLED | Abgerechnet / erledigt |
WITHDRAWN | Zurückgezogen (durch Einreicher) |
REJECTED | Abgelehnt (durch Prüfenden) |
VOIDED | Entwertet |
Zahlungsstatus-Werte
| Status | Bedeutung |
|---|---|
OPEN | Offen / noch nicht bezahlt |
PAID | Bezahlt |
OVERDUE | Überfällig |
Beleg-Typen
| Typ | Bedeutung |
|---|---|
INVOICE | Eingangsrechnung (Standard) |
RECEIPT | Kassenbeleg |
CREDIT_NOTE | Gutschrift |
TRAVEL_EXPENSE | Reisekostenabrechnung |
OTHER | Sonstiger Beleg |
Belege auflisten
GET /api/v1/receipts
Scope: receipts:read
Query-Parameter:
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
status | String | Nein | Filtert nach Belegstatus (z.B. APPROVED) |
paymentStatus | String | Nein | Filtert nach Zahlungsstatus (OPEN, PAID, OVERDUE) |
categoryId | Long | Nein | Filtert nach Kategorie-ID |
receiptDateAfter | String | Nein | Belegdatum ab (YYYY-MM-DD) |
receiptDateBefore | String | Nein | Belegdatum bis (YYYY-MM-DD) |
supplierName | String | Nein | Textsuche im Lieferantennamen |
active | Boolean | Nein | true = nur aktive Belege (UPLOADED..SETTLED), false = nur inaktive (WITHDRAWN/REJECTED/VOIDED), nicht gesetzt = alle |
page | Integer | Nein | Seitennummer (Standard: 0) |
size | Integer | Nein | Einträge pro Seite, max. 100 (Standard: 20) |
sort | String | Nein | Sortierung (Standard: createdAt,desc). Erlaubte Felder: createdAt, updatedAt, receiptDate, dueDate, grossAmount, status, receiptNumber, supplierName |
Antwort (200 OK):
{
"data": [
{
"id": 42,
"receiptNumber": "BLG-2025-0001",
"externalInvoiceNumber": "RE-2025-00123",
"type": "INVOICE",
"status": "APPROVED",
"isActive": true,
"supplierName": "Bürobedarfs GmbH",
"receiptDate": "2025-03-01",
"dueDate": "2025-03-31",
"netAmount": 84.03,
"vatRate": 19.00,
"vatAmount": 15.97,
"grossAmount": 100.00,
"currency": "EUR",
"paymentStatus": "OPEN",
"paymentMethod": null,
"paymentDate": null,
"categoryId": 3,
"categoryName": "Bürobedarf",
"projectId": null,
"projectName": null,
"expenseReportId": null,
"expenseReportNumber": null,
"submittedById": 5,
"submittedByName": "Lisa Schmidt",
"notes": null,
"attachmentCount": 1,
"createdAt": "2025-03-01T09:00:00Z",
"updatedAt": "2025-03-05T14:30:00Z"
}
],
"pagination": {
"page": 0,
"size": 20,
"totalElements": 1,
"totalPages": 1
}
}
Einzelnen Beleg abrufen
GET /api/v1/receipts/{id}
Scope: receipts:read
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Belegs |
Antwort (200 OK): Einzelnes Belegobjekt (gleiche Struktur wie in der Liste)
Beleg erstellen
POST /api/v1/receipts
Erstellt einen neuen Beleg. Neue Belege erhalten automatisch eine Belegnummer und den Status SUBMITTED.
Scope: receipts:write
Request-Body:
| Feld | Typ | Pflicht | Standard | Beschreibung |
|---|---|---|---|---|
receiptDate | String | Ja | - | Belegdatum (YYYY-MM-DD) |
grossAmount | number | Ja | - | Bruttobetrag (>= 0) |
externalInvoiceNumber | String | Nein | null | Externe Rechnungsnummer (max. 255 Zeichen) |
type | String | Nein | INVOICE | Belegtyp (siehe Beleg-Typen) |
supplierName | String | Nein | null | Lieferantenname (max. 255 Zeichen) |
dueDate | String | Nein | null | Fälligkeitsdatum (YYYY-MM-DD) |
netAmount | number | Nein | null | Nettobetrag |
vatRate | number | Nein | null | Mehrwertsteuersatz in Prozent |
vatAmount | number | Nein | null | Mehrwertsteuerbetrag |
currency | String | Nein | EUR | Währung (3-stelliger ISO-Code) |
paymentStatus | String | Nein | null | Zahlungsstatus |
paymentMethod | String | Nein | null | Zahlungsart |
paymentDate | String | Nein | null | Zahlungsdatum (YYYY-MM-DD) |
categoryId | Long | Nein | null | ID der Kategorie |
categoryName | String | Nein | null | Kategoriename (alternativ zu categoryId) |
projectId | Long | Nein | null | ID des zugehörigen Projekts |
notes | String | Nein | null | Notizen (max. 500 Zeichen) |
Antwort (201 Created): Erstelltes Belegobjekt
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/receipts" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"receiptDate": "2025-03-01",
"grossAmount": 100.00,
"supplierName": "Bürobedarfs GmbH",
"externalInvoiceNumber": "RE-2025-00123",
"vatRate": 19.0,
"currency": "EUR"
}'
Beleg aktualisieren
PATCH /api/v1/receipts/{id}
Aktualisiert einen vorhandenen Beleg (PATCH-Semantik). Nur angegebene Felder werden geändert.
Scope: receipts:write
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Belegs |
Request-Body: Gleiche Felder wie beim Erstellen, alle optional.
Antwort (200 OK): Aktualisiertes Belegobjekt
Beleg-Status ändern
PATCH /api/v1/receipts/{id}/status
Ändert den Workflow-Status eines Belegs für reguläre Vorwärts-Übergänge.
Erlaubte Übergänge:
UPLOADED→SUBMITTEDSUBMITTED→IN_REVIEWIN_REVIEW→APPROVEDAPPROVED→SETTLED
Für Zurückziehen, Ablehnen und Entwerten die dedizierten Endpoints verwenden.
Scope: receipts:write
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Belegs |
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
status | String | Ja | Zielstatus |
Antwort (200 OK): Aktualisiertes Belegobjekt
Beleg zurückziehen
POST /api/v1/receipts/{id}/withdraw
Zieht einen eingereichten Beleg zurück (SUBMITTED → WITHDRAWN). Nur möglich aus Status SUBMITTED. Eine Begründung ist Pflicht.
Scope: receipts:write
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Belegs |
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
reason | String | Ja | Begründung für das Zurückziehen |
Antwort (200 OK): Aktualisiertes Belegobjekt
Beleg ablehnen
POST /api/v1/receipts/{id}/reject
Lehnt einen Beleg in Prüfung ab (IN_REVIEW → REJECTED). Nur möglich aus Status IN_REVIEW. Eine Begründung ist Pflicht.
Scope: receipts:write
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Belegs |
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
reason | String | Ja | Begründung für die Ablehnung |
Antwort (200 OK): Aktualisiertes Belegobjekt
Beleg entwerten
POST /api/v1/receipts/{id}/void
Entwertet einen genehmigten oder abgelehnten Beleg (APPROVED/REJECTED → VOIDED). Eine Begründung ist Pflicht.
Scope: receipts:write
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Belegs |
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
reason | String | Ja | Begründung für das Entwerten |
replacedByReceiptId | Long | Nein | ID eines Ersatz-Belegs |
Antwort (200 OK): Aktualisiertes Belegobjekt
Audit-Trail abrufen
GET /api/v1/receipts/{id}/audit-trail
Gibt den vollständigen Statusänderungs-Verlauf eines Belegs chronologisch zurück. Jeder Eintrag dokumentiert eine Statusänderung mit Begründung, Akteur und Zeitstempel (GoBD-konform).
Scope: receipts:read
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Belegs |
Antwort-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
fromStatus | String? | Ausgangsstatus (null beim ersten Eintrag) |
toStatus | String | Zielstatus |
changedByName | String | Name des Akteurs |
changedAt | String | Zeitpunkt der Änderung (ISO 8601, UTC) |
reason | String? | Begründung (bei Ablehnen, Zurückziehen, Entwerten) |
source | String | Quelle der Änderung (z.B. PUBLIC_API, UI) |
Antwort (200 OK):
{
"data": [
{
"fromStatus": null,
"toStatus": "SUBMITTED",
"changedByName": "API-Key: Buchhaltungs-Integration",
"changedAt": "2025-03-01T09:00:00Z",
"reason": null,
"source": "PUBLIC_API"
},
{
"fromStatus": "SUBMITTED",
"toStatus": "IN_REVIEW",
"changedByName": "Lisa Schmidt",
"changedAt": "2025-03-03T10:15:00Z",
"reason": null,
"source": "UI"
}
]
}
Anhänge eines Belegs auflisten
GET /api/v1/receipts/{id}/attachments
Scope: receipts:read
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Belegs |
Antwort-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
id | number | ID des Anhangs |
displayFilename | String | Dateiname |
contentType | String? | MIME-Typ |
sizeBytes | number | Dateigröße in Bytes |
isPrimary | boolean | Ist dieser Anhang der Hauptbeleg |
attachedByName | String | Name des Hochladers |
attachedAt | String | Hochladezeitpunkt (ISO 8601, UTC) |
Anhang herunterladen
GET /api/v1/receipts/{id}/attachments/{attachmentId}/download
Lädt eine Anhang-Datei herunter.
Scope: receipts:read
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Belegs |
attachmentId | Long | ID des Anhangs |
Antwort: Binäre Datei mit entsprechendem Content-Type-Header
Anhang hochladen
POST /api/v1/receipts/{id}/attachments
Content-Type: multipart/form-data
Lädt eine Datei als Anhang für einen Beleg hoch. Die Datei wird auf Viren gescannt und in MinIO gespeichert.
Scope: receipts:write
Pfad-Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
id | Long | ID des Belegs |
Form-Parameter:
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
file | multipart | Ja | Hochzuladende Datei |
isPrimary | boolean | Nein | Ob dieser Anhang der Hauptbeleg ist (Standard: false) |
Antwort (201 Created): Erstelltes Anhang-Objekt (gleiche Felder wie bei der Anhang-Liste)
Belegkategorien auflisten
GET /api/v1/receipt-categories
Gibt alle Belegkategorien des Mandanten zurück, sortiert nach sort_order.
Scope: receipts:read
Antwort-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
id | number | ID der Kategorie |
name | String | Name der Kategorie |
datevAccountNumber | String? | DATEV-Kontonummer für Steuerexport |
color | String? | Farbe der Kategorie (#RRGGBB) |
sortOrder | number | Sortierposition |
Beleg-Statistiken abrufen
GET /api/v1/receipts/stats
Gibt aggregierte Statistiken über alle Belege des Mandanten zurück.
Scope: receipts:read
Antwort (200 OK):
{
"data": {
"totalCount": 128,
"openCount": 15,
"openAmount": 4250.75,
"byStatus": {
"SUBMITTED": 5,
"IN_REVIEW": 3,
"APPROVED": 7,
"SETTLED": 113
},
"byPaymentStatus": {
"OPEN": 15,
"PAID": 113,
"OVERDUE": 0
}
}
}
Belege exportieren
GET /api/v1/receipts/export
Exportiert Belege als CSV oder JSON.
- Format
csv: Semikolon-getrennte Datei für Excel/DATEV - Format
json: Maschinenlesbarer JSON-Array
Scope: receipts:read
Query-Parameter:
| Parameter | Typ | Standard | Beschreibung |
|---|---|---|---|
format | String | json | Exportformat: json oder csv |
status | String | - | Filter nach Status (z.B. APPROVED) |
dateFrom | String | - | Belegdatum ab (YYYY-MM-DD) |
dateTo | String | - | Belegdatum bis (YYYY-MM-DD) |
active | Boolean | - | true = nur aktive, false = nur inaktive, nicht gesetzt = alle |
cURL-Beispiel (CSV):
curl "https://app.spiritflow.team/api/v1/receipts/export?format=csv&status=SETTLED&dateFrom=2025-01-01&dateTo=2025-03-31" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-o "belege-q1-2025.csv"
Belege importieren (JSON-Batch)
POST /api/v1/receipts/import
Importiert bis zu 100 Belege in einem Batch. Alle importierten Belege erhalten den Status UPLOADED ohne Belegnummer. Jeder Beleg wird einzeln validiert; Teilerfolg ist möglich.
Scope: receipts:write
Request-Body:
| Feld | Typ | Pflicht | Standard | Beschreibung |
|---|---|---|---|---|
receipts | Array | Ja | - | Liste der zu importierenden Belege (max. 100, gleiche Felder wie beim Erstellen) |
skipDuplicates | boolean | Nein | false | Duplikate überspringen statt Fehler auszulösen |
Antwort (200 OK):
{
"data": {
"totalCount": 3,
"successCount": 2,
"skippedCount": 0,
"errorCount": 1,
"results": [
{ "index": 0, "status": "created", "receiptId": 101, "reason": null },
{ "index": 1, "status": "created", "receiptId": 102, "reason": null },
{ "index": 2, "status": "error", "receiptId": null, "reason": "grossAmount must not be negative" }
]
}
}
Ergebnis-Status pro Eintrag:
| Status | Bedeutung |
|---|---|
created | Beleg erfolgreich erstellt |
skipped | Duplikat übersprungen (bei skipDuplicates: true) |
error | Validierungsfehler, Beleg wurde nicht erstellt |
Zeiteinträge (Time Entries)
Basis-Pfad: /api/v1/time-entries
Zeiteinträge auflisten
GET /api/v1/time-entries
Scope: time_entries:read
Query-Parameter:
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
userId | Long | Nein | Nach Benutzer filtern |
projectId | Long | Nein | Nach Projekt filtern |
startDate | string | Nein | Startdatum (YYYY-MM-DD) |
endDate | string | Nein | Enddatum (YYYY-MM-DD) |
page | int | Nein | Seitennummer (Standard: 0) |
size | int | Nein | Einträge pro Seite, max. 100 (Standard: 50) |
Antwort-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
id | number | Eindeutige ID |
userId | number | ID des Benutzers |
userDisplayName | string | Anzeigename des Benutzers |
projectId | number? | ID des Projekts (null bei allgemeinen Aufgaben) |
projectName | string? | Name des Projekts |
taskId | number? | ID der verknüpften Aufgabe |
taskTitle | string? | Titel der verknüpften Aufgabe |
date | string | Datum des Eintrags (YYYY-MM-DD) |
startTime | string? | Startzeit (HH:mm) |
endTime | string? | Endzeit (HH:mm) |
durationMinutes | number | Dauer in Minuten |
durationHours | number | Dauer in Dezimalstunden |
description | string? | Beschreibung |
billable | boolean | Abrechenbar |
billed | boolean | Bereits abgerechnet |
hourlyRate | number? | Stundensatz zum Zeitpunkt des Eintrags |
revenue | number? | Berechneter Umsatz |
approvalStatus | string | PENDING, APPROVED oder REJECTED |
entryType | string | WORK, TRAVEL, BREAK oder FLAT_FEE |
externalReference | string? | Externe Referenz (z.B. ERP-Nummer) |
archived | boolean | Archiviert (gelöscht) |
createdAt | string | Erstellungszeitpunkt (ISO 8601) |
updatedAt | string | Letzter Änderungszeitpunkt (ISO 8601) |
cURL-Beispiel:
curl "https://app.spiritflow.team/api/v1/time-entries?startDate=2025-03-01&endDate=2025-03-31&projectId=5" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Einzelnen Zeiteintrag abrufen
GET /api/v1/time-entries/{id}
Scope: time_entries:read
Zeiteintrag erstellen
POST /api/v1/time-entries
Scope: time_entries:write
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
userId | number | Ja | ID des Benutzers |
projectId | number | Nein | ID des Projekts |
taskId | number | Nein | ID der Aufgabe |
date | string | Ja | Datum (YYYY-MM-DD) |
startTime | string | Nein | Startzeit (HH:mm) |
endTime | string | Nein | Endzeit (HH:mm) |
durationMinutes | number | Nein | Dauer in Minuten (min. 1) |
description | string | Nein | Beschreibung (max. 2000 Zeichen) |
billable | boolean | Nein | Abrechenbar (Standard: true) |
entryType | string | Nein | WORK (Standard), TRAVEL, BREAK oder FLAT_FEE |
externalReference | string | Nein | Externe Referenz (max. 255 Zeichen) |
Hinweis: Entweder
durationMinutesoderstartTime+endTimemüssen angegeben werden. Bei Angabe von Start- und Endzeit wird die Dauer automatisch berechnet.
Antwort (201 Created): Gleiche Felder wie bei der Zeiteintragsliste.
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/time-entries" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"userId": 2,
"projectId": 5,
"taskId": 42,
"date": "2025-03-15",
"durationMinutes": 120,
"description": "Frontend-Entwicklung: Kontaktformular",
"billable": true
}'
Zeiteintrag aktualisieren
PUT /api/v1/time-entries/{id}
Scope: time_entries:write
Alle Felder sind optional — nur die angegebenen Felder werden geändert.
Hinweis: Bereits abgerechnete Zeiteinträge (
billed: true) können nicht mehr geändert werden.
Zeiteintrag löschen (archivieren)
DELETE /api/v1/time-entries/{id}
Scope: time_entries:write
Antwort: 204 No Content
Hinweis: Bereits abgerechnete Zeiteinträge können nicht gelöscht werden.
Urlaub (Vacation Requests)
Basis-Pfad: /api/v1/vacation-requests
Urlaubsanträge auflisten
GET /api/v1/vacation-requests
Scope: vacation:read
Query-Parameter:
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
status | string | Nein | Filter: PENDING, APPROVED, REJECTED, CANCELLED |
ownerId | Long | Nein | Nach Benutzer filtern |
Antwort-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
id | number | Eindeutige ID |
title | string | Titel des Antrags |
type | string | VACATION, SPECIAL_LEAVE oder UNPAID_LEAVE |
status | string | PENDING, APPROVED, REJECTED oder CANCELLED |
ownerId | number | ID des Antragstellers |
ownerName | string | Name des Antragstellers |
startDate | string | Startdatum (YYYY-MM-DD) |
endDate | string | Enddatum (YYYY-MM-DD) |
daysCount | number | Anzahl der Urlaubstage |
reason | string? | Begründung |
createdAt | string | Erstellungszeitpunkt (ISO 8601) |
updatedAt | string | Letzter Änderungszeitpunkt (ISO 8601) |
Hinweis: Krankmeldungen (
SICK) werden NICHT über diesen Endpunkt zurückgegeben — siehe Krankmeldungen.
Urlaubsantrag erstellen
POST /api/v1/vacation-requests
Scope: vacation:write
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
ownerId | number | Ja | ID des Benutzers |
type | string | Nein | VACATION (Standard), SPECIAL_LEAVE oder UNPAID_LEAVE |
startDate | string | Ja | Startdatum (YYYY-MM-DD) |
endDate | string | Ja | Enddatum (YYYY-MM-DD) |
reason | string | Nein | Begründung |
title | string | Nein | Titel (wird automatisch generiert falls leer) |
autoApprove | boolean | Nein | Automatisch genehmigen (Standard: false) |
Fehler-Codes:
| HTTP-Status | Code | Beschreibung |
|---|---|---|
| 409 | CONFLICT | Überlappung mit bereits genehmigtem Urlaub |
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/vacation-requests" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"ownerId": 5,
"startDate": "2025-07-14",
"endDate": "2025-07-25",
"reason": "Sommerurlaub"
}'
Krankmeldungen (Sick Leaves)
Basis-Pfad: /api/v1/sick-leaves
DSGVO-Hinweis: Krankmeldungen sind Gesundheitsdaten und unterliegen besonderem Datenschutz. Der Zugriff erfordert separate Scopes (
sick_leave:read/sick_leave:write), die unabhängig vom Urlaubs-Scope vergeben werden.
Krankmeldungen auflisten
GET /api/v1/sick-leaves
Scope: sick_leave:read
Query-Parameter:
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
ownerId | Long | Nein | Nach Benutzer filtern |
Antwort: Gleiche Felder wie bei Urlaubsanträgen, jedoch mit type: "SICK".
Krankmeldung erstellen
POST /api/v1/sick-leaves
Scope: sick_leave:write
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
ownerId | number | Ja | ID des Benutzers |
startDate | string | Ja | Erster Krankheitstag (YYYY-MM-DD) |
endDate | string | Ja | Letzter Krankheitstag (YYYY-MM-DD) |
reason | string | Nein | Optionaler Hinweis |
Besonderes Verhalten:
- Krankmeldungen werden automatisch genehmigt (Status:
APPROVED) - Überlappende genehmigte Urlaubsanträge werden automatisch storniert
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/sick-leaves" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"ownerId": 5,
"startDate": "2025-03-10",
"endDate": "2025-03-12"
}'
Tags
Basis-Pfad: /api/v1/tags
Tags auflisten
GET /api/v1/tags
Scope: tags:read
Gibt alle Tags des Mandanten zurück (alphabetisch sortiert).
Antwort-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
id | number | Eindeutige ID |
name | string | Tag-Name |
color | string? | Farbcode im Hex-Format (z.B. #FF5733) |
createdAt | string | Erstellungszeitpunkt (ISO 8601) |
Tag erstellen
POST /api/v1/tags
Scope: tags:write
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
name | string | Ja | Tag-Name (pro Mandant eindeutig) |
color | string | Nein | Farbcode im Hex-Format (z.B. #3F51B5) |
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/tags" \
-H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Dringend",
"color": "#F44336"
}'
Interaktionen (Interactions)
Interaktionen dokumentieren Kontaktpunkte mit Kunden (Telefonate, E-Mails, Meetings). Die Interaktionen-Endpunkte sind unter dem jeweiligen Kunden verschachtelt.
Basis-Pfad: /api/v1/customers/{customerId}/interactions
Interaktionen auflisten
GET /api/v1/customers/{customerId}/interactions
Scope: interactions:read
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
page | number | 0 | Seitennummer (0-basiert) |
size | number | 20 | Einträge pro Seite (max. 100) |
Antwort:
{
"data": [
{
"id": 1,
"customerId": 42,
"interactionType": "PHONE_CALL",
"subject": "Rueckruf wegen Angebot",
"content": "Kunde hat Interesse an Enterprise-Paket bestätigt.",
"interactionDate": "2025-03-01T14:30:00Z",
"durationMinutes": 15,
"outcome": "Angebot wird per E-Mail nachgesendet",
"contactPersonId": 5,
"contactPersonName": "Anna Mueller",
"followUpDate": "2025-03-08T09:00:00Z",
"createdBy": "Florian Cremer",
"createdAt": "2025-03-01T14:45:00Z"
}
],
"pagination": { "page": 0, "size": 20, "totalElements": 1, "totalPages": 1 },
"meta": { "requestId": "...", "timestamp": "2025-03-01T15:00:00Z" }
}
cURL-Beispiel:
curl -X GET "https://app.spiritflow.team/api/v1/customers/42/interactions?page=0&size=20" \
-H "X-API-Key: sf_live_xxxxx"
Interaktion erstellen
POST /api/v1/customers/{customerId}/interactions
Scope: interactions:write
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
interactionType | string | Ja | Art der Interaktion (siehe unten) |
subject | string | Ja | Betreff (max. 255 Zeichen) |
content | string | Nein | Inhalt/Notizen (max. 5.000 Zeichen) |
interactionDate | string | Nein | ISO 8601 Zeitstempel. Default: aktuelle Zeit |
durationMinutes | number | Nein | Dauer in Minuten |
outcome | string | Nein | Ergebnis der Interaktion (max. 500 Zeichen) |
contactPersonId | number | Nein | ID der Kontaktperson beim Kunden |
followUpDate | string | Nein | ISO 8601 Zeitstempel für Wiedervorlage |
Interaktionstypen:
| Wert | Beschreibung |
|---|---|
PHONE_CALL | Telefonat |
EMAIL_IN | Eingehende E-Mail |
EMAIL_OUT | Ausgehende E-Mail |
MEETING | Besprechung |
VISIT | Vor-Ort-Besuch |
NOTE | Notiz/Vermerk |
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/customers/42/interactions" \
-H "X-API-Key: sf_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"interactionType": "PHONE_CALL",
"subject": "Rueckruf wegen Angebot",
"content": "Kunde hat Interesse an Enterprise-Paket bestätigt.",
"interactionDate": "2025-03-01T14:30:00Z",
"durationMinutes": 15,
"outcome": "Angebot wird per E-Mail nachgesendet",
"contactPersonId": 5,
"followUpDate": "2025-03-08T09:00:00Z"
}'
Antwort: HTTP 201 Created mit der erstellten Interaktion im data-Feld.
Nebeneffekte: Das Feld
lastContactDatedes Kunden wird automatisch auf den Interaktionszeitpunkt gesetzt. Wird einfollowUpDateangegeben, wirdnextContactDatedes Kunden aktualisiert.
Telefonie / CTI
Die CTI-Endpunkte (Computer-Telephony-Integration) ermöglichen die Anbindung einer externen CTI-Middleware an spiritflow. Typischer Anwendungsfall: Eine Middleware verbindet sich mit der Telefonanlage (z.B. FritzBox, Asterisk) und kommuniziert über diese API mit spiritflow.
Basis-Pfad: /api/v1/cti
Ablauf einer CTI-Integration
Telefonanlage <-> CTI-Middleware <-> spiritflow Public API
- Eingehender Anruf: TK-Anlage meldet Anruf → Middleware ruft
POST /events/incoming-call→ spiritflow zeigt Screen-Pop beim Benutzer - Click-to-Dial: Benutzer klickt Telefonnummer in spiritflow → Webhook
cti.dial_requested→ Middleware empfängt und initiiert Anruf über TK-Anlage - Reverse Lookup: Middleware oder Frontend fragt
GET /lookup→ spiritflow ordnet Nummer einem Kunden zu
Rufnummernsuche (Reverse Lookup)
GET /api/v1/cti/lookup?phone={nummer}
Scope: cti:read
Sucht anhand einer Telefonnummer nach passenden Kunden und Kontaktpersonen. Die Nummer wird automatisch normalisiert. Die Suche erfolgt per Suffix-Matching über die letzten 7 Ziffern.
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
phone | string | Ja | Telefonnummer (beliebiges Format) |
Antwort:
{
"data": [
{
"customerId": 42,
"customerName": "Muster GmbH",
"customerNumber": "K-2025-001",
"matchedField": "phone",
"matchedNumber": "+49 211 12345678",
"contactPerson": null
},
{
"customerId": 55,
"customerName": "Beispiel AG",
"customerNumber": "K-2025-015",
"matchedField": "mobile",
"matchedNumber": "+49 170 9876543",
"contactPerson": {
"id": 12,
"firstName": "Anna",
"lastName": "Mueller",
"matchedField": "mobile",
"matchedNumber": "+49 170 9876543"
}
}
],
"meta": { "requestId": "...", "timestamp": "2025-03-01T10:30:00Z" }
}
cURL-Beispiel:
curl -X GET "https://app.spiritflow.team/api/v1/cti/lookup?phone=+4921112345678" \
-H "X-API-Key: sf_live_xxxxx"
Die Suche durchsucht die Felder
phoneundmobilesowohl bei Kunden als auch bei aktiven Kontaktpersonen. Ergebnisse sind auf den eigenen Mandanten beschränkt. Archivierte Kunden werden nicht gefunden.
Nebenstellen-Zuordnung (Device Mappings)
GET /api/v1/cti/device-mappings
Scope: cti:read
Liefert eine Liste aller Benutzer, die eine CTI-Nebenstelle konfiguriert haben.
Antwort:
{
"data": [
{
"userId": 2,
"username": "florian.cremer",
"displayName": "Florian Cremer",
"device": "201"
},
{
"userId": 5,
"username": "anna.mueller",
"displayName": "Anna Mueller",
"device": "**610"
}
],
"meta": { "requestId": "...", "timestamp": "2025-03-01T10:30:00Z" }
}
Benutzer konfigurieren ihre Nebenstelle in spiritflow unter Einstellungen > Benutzerverwaltung > Arbeitseinstellungen > Telefonie (CTI).
Eingehenden Anruf melden (Screen-Pop)
POST /api/v1/cti/events/incoming-call
Scope: cti:write
Meldet einen eingehenden Anruf. spiritflow führt automatisch eine Rufnummernsuche durch und zeigt dem zugeordneten Benutzer eine Echtzeit-Benachrichtigung (Screen-Pop) mit Kundeninformationen an.
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
callerNumber | string | Ja | Anrufende Telefonnummer |
calledDevice | string | Ja | Angerufene Nebenstelle (z.B. "201", "**610") |
callId | string | Nein | Eindeutige Call-ID der TK-Anlage |
timestamp | string | Nein | ISO 8601 Zeitstempel des Anrufs |
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/cti/events/incoming-call" \
-H "X-API-Key: sf_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"callerNumber": "+49 211 12345678",
"calledDevice": "201",
"callId": "call-2025-03-01-001"
}'
Antwort: HTTP 202 Accepted
{
"data": {
"processed": true,
"targetUserId": 2,
"customerFound": true,
"customerId": 42
},
"meta": { "requestId": "...", "timestamp": "2025-03-01T10:30:00Z" }
}
Verhalten:
| Situation | Ergebnis |
|---|---|
| Nebenstelle zugeordnet + Kunde gefunden | Screen-Pop mit Kundenname, Klick navigiert zum Kunden |
| Nebenstelle zugeordnet + Kunde unbekannt | Screen-Pop mit Telefonnummer |
| Nebenstelle nicht zugeordnet | processed: false, keine Benachrichtigung |
Anruf angenommen melden (Call Connected)
POST /api/v1/cti/events/call-connected
Scope: cti:write
Meldet an spiritflow, dass ein Anruf auf einer bestimmten Nebenstelle angenommen wurde. Die Anruf-Benachrichtigungen werden bei allen anderen Benutzern entfernt.
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
callId | string | Ja | Eindeutige Call-ID der TK-Anlage |
device | string | Ja | Nebenstelle, die den Anruf angenommen hat |
callerNumber | string | Nein | Anrufende Telefonnummer |
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/cti/events/call-connected" \
-H "X-API-Key: sf_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"callId": "call-2025-03-01-001",
"device": "201"
}'
Antwort: HTTP 202 Accepted
{
"data": {
"processed": true,
"answeredByUserId": 2,
"notificationsDismissed": 3
},
"meta": {
"requestId": "...",
"timestamp": "2025-03-01T10:30:00Z"
}
}
Anruf-Ende melden (Call Ended)
POST /api/v1/cti/events/call-ended
Scope: cti:write
Meldet das Ende eines Anrufs an spiritflow. Bei nicht angenommenen Anrufen (answered: false) werden die zugehörigen Ring-Benachrichtigungen entfernt. Bei angenommenen Anrufen wird ein Aktivitäts-Eintrag im Rückblick angelegt.
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
callId | string | Ja | Eindeutige Call-ID der TK-Anlage |
duration | number | Nein | Dauer des Anrufs in Sekunden (null wenn nicht angenommen) |
answered | boolean | Nein | Ob der Anruf angenommen wurde (Standard: false) |
answeredByDevice | string | Nein | Nebenstelle, die den Anruf angenommen hat |
callerNumber | string | Nein | Anrufende Telefonnummer |
cURL-Beispiel:
curl -X POST "https://app.spiritflow.team/api/v1/cti/events/call-ended" \
-H "X-API-Key: sf_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"callId": "call-2025-03-01-001",
"duration": 120,
"answered": true,
"answeredByDevice": "201",
"callerNumber": "+49 211 12345678"
}'
Antwort: HTTP 202 Accepted
{
"data": {
"processed": true,
"notificationsRemoved": 1,
"activityCreated": true
},
"meta": {
"requestId": "...",
"timestamp": "2025-03-01T10:30:00Z"
}
}
Click-to-Dial (Webhook)
Click-to-Dial wird nicht über die Public API aufgerufen, sondern über einen Webhook-Event. Wenn ein Benutzer in spiritflow auf eine Telefonnummer klickt, wird der Webhook cti.dial_requested an alle konfigurierten Webhook-Endpunkte gesendet.
Webhook-Payload:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"event": "cti.dial_requested",
"created_at": "2025-03-01T10:30:00Z",
"source": "ui",
"actor": {
"id": 2,
"type": "user",
"name": "Florian Cremer"
},
"data": {
"targetNumber": "+49 211 12345678",
"sourceDevice": "201",
"userId": 2,
"username": "florian.cremer",
"customerId": 42,
"contactPersonId": null
},
"changes": {}
}
Voraussetzungen für Click-to-Dial:
- Webhook mit Event
cti.dial_requestedmuss eingerichtet sein - Der Benutzer muss eine Nebenstelle konfiguriert haben
- Die CTI-Middleware muss den Webhook empfangen und verarbeiten
Benachrichtigungen (Notifications)
Basis-Pfad: /api/v1/notifications
Über die Notification-Endpoints können externe Systeme Benachrichtigungen an spiritflow-Benutzer senden. Die Zustellung erfolgt über alle verfügbaren Kanäle: WebSocket (Echtzeit in der App), E-Mail (wenn vom Benutzer aktiviert) und Push-Notification (Mobile App).
Deaktivierte und anonymisierte Benutzer werden automatisch übersprungen.
Einzelne Benachrichtigung senden
POST /api/v1/notifications
Scope: notifications:write
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
recipientId | Long | Ja | ID des Empfängers (muss zum Mandanten gehören) |
title | String | Ja | Titel der Benachrichtigung (max. 255 Zeichen) |
message | String | Ja | Nachrichtentext (max. 5000 Zeichen) |
payload | String | Nein | Optionale JSON-Nutzdaten (max. 10000 Zeichen) |
targetType | String | Nein | Zieltyp für Verlinkung (z.B. TASK, PROJECT, CUSTOMER) |
targetId | Long | Nein | ID des verlinkten Objekts |
expiresAt | String | Nein | Ablaufdatum (ISO 8601, UTC) |
priority | Integer | Nein | Priorität: -1 (niedrig), 0 (normal, Standard), 1 (hoch/dringend) |
cURL-Beispiel:
curl -X POST https://app.spiritflow.team/api/v1/notifications \
-H "X-API-Key: sf_live_..." \
-H "Content-Type: application/json" \
-d '{
"recipientId": 7,
"title": "Deployment abgeschlossen",
"message": "Version 1.2.3 wurde erfolgreich deployt.",
"targetType": "PROJECT",
"targetId": 5,
"priority": 0
}'
Antwort (201 Created):
{
"data": {
"id": 42,
"type": "MANUAL_NOTIFICATION",
"title": "Deployment abgeschlossen",
"message": "Version 1.2.3 wurde erfolgreich deployt.",
"payload": null,
"targetType": "PROJECT",
"targetId": 5,
"recipientId": 7,
"priority": 0,
"expiresAt": null,
"createdAt": "2025-03-01T10:30:00Z"
},
"meta": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2025-03-01T10:30:00Z"
}
}
Bulk-Benachrichtigung senden
POST /api/v1/notifications/bulk
Scope: notifications:write
Sendet dieselbe Benachrichtigung an mehrere Benutzer gleichzeitig. Nicht existierende, deaktivierte oder mandantenfremde Benutzer werden übersprungen.
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
recipientIds | Long[] | Ja | Liste der Empfänger-IDs (mind. 1) |
title | String | Ja | Titel der Benachrichtigung |
message | String | Ja | Nachrichtentext |
payload | String | Nein | Optionale JSON-Nutzdaten |
targetType | String | Nein | Zieltyp für Verlinkung |
targetId | Long | Nein | ID des verlinkten Objekts |
expiresAt | String | Nein | Ablaufdatum (ISO 8601, UTC) |
priority | Integer | Nein | Priorität (-1, 0, 1) |
cURL-Beispiel:
curl -X POST https://app.spiritflow.team/api/v1/notifications/bulk \
-H "X-API-Key: sf_live_..." \
-H "Content-Type: application/json" \
-d '{
"recipientIds": [7, 8, 9],
"title": "Team-Meeting",
"message": "Heute um 15 Uhr im Konferenzraum."
}'
Antwort (201 Created):
{
"data": {
"sentCount": 3,
"skippedCount": 0,
"notifications": [
{ "id": 43, "type": "MANUAL_NOTIFICATION", "recipientId": 7 },
{ "id": 44, "type": "MANUAL_NOTIFICATION", "recipientId": 8 },
{ "id": 45, "type": "MANUAL_NOTIFICATION", "recipientId": 9 }
]
}
}
Broadcast-Benachrichtigung senden
POST /api/v1/notifications/broadcast
Scope: notifications:write
Sendet eine Benachrichtigung an alle aktiven Benutzer des Mandanten. Deaktivierte und anonymisierte Benutzer werden automatisch übersprungen.
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
title | String | Ja | Titel der Benachrichtigung |
message | String | Ja | Nachrichtentext |
payload | String | Nein | Optionale JSON-Nutzdaten |
targetType | String | Nein | Zieltyp für Verlinkung |
targetId | Long | Nein | ID des verlinkten Objekts |
expiresAt | String | Nein | Ablaufdatum (ISO 8601, UTC) |
priority | Integer | Nein | Priorität (-1, 0, 1) |
cURL-Beispiel:
curl -X POST https://app.spiritflow.team/api/v1/notifications/broadcast \
-H "X-API-Key: sf_live_..." \
-H "Content-Type: application/json" \
-d '{
"title": "Wartungsarbeiten",
"message": "Am Samstag 10-12 Uhr finden Wartungsarbeiten statt.",
"priority": 1
}'
Antwort (201 Created): Gleiches Format wie Bulk (mit sentCount, skippedCount, notifications).
Zustellkanäle
Jede Benachrichtigung wird automatisch über alle verfügbaren Kanäle zugestellt:
| Kanal | Beschreibung |
|---|---|
| WebSocket | Sofort sichtbar in der spiritflow-App (Benachrichtigungs-Glocke) |
| Wenn der Benutzer E-Mail-Benachrichtigungen aktiviert hat | |
| Push | Auf iOS- und Android-Mobile-App |
Webhook-Integration
Bei jeder erstellten Benachrichtigung wird automatisch ein Webhook-Event notification.created ausgelöst. Externe Systeme können dieses Event abonnieren, um über neue Benachrichtigungen informiert zu werden (siehe Webhooks).
Webhooks
Webhooks ermöglichen es, Echtzeit-Benachrichtigungen über Änderungen in spiritflow an externe Systeme zu senden. Wenn ein Ereignis eintritt, sendet spiritflow einen HTTP POST-Request an die konfigurierten Webhook-Endpunkte.
Wichtig: Webhooks werden sowohl bei Änderungen über die API als auch bei Änderungen über die spiritflow-Oberfläche ausgelöst.
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 auslösen (Dual-Delivery). Zum Beispiel löst eine Status-Änderung sowohl
task.updatedals auchtask.status_changedaus.
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 |
Benachrichtigungs-Events
| Event | Beschreibung |
|---|---|
notification.created | Neue Benachrichtigung wurde erstellt |
Webhook-Payload-Format
Jeder Webhook-Request wird als HTTP POST mit einem JSON-Body gesendet:
{
"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" oder "ui" |
actor.id | number | ID des Auslöser-Benutzers |
actor.type | string | Typ des Auslöser: "api_key" oder "user" |
actor.name | string | Name des API-Keys oder Benutzers |
data | object | Vollständiges Objekt nach der Änderung |
changes | object | Geänderte Felder mit from/to-Werten. Leer bei *.created-Events. |
HTTP-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 | a1b2c3d4-... |
X-spiritflow-Signature | HMAC-SHA256 Signatur | sha256=abc123... |
Signatur-Verifizierung
Jeder Webhook-Request wird mit einer HMAC-SHA256-Signatur versehen.
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
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 angezeigt. Verwende stets einen timing-safe Vergleich.
Retry-Verhalten
Wenn ein Webhook-Endpunkt nicht mit einem 2xx-Statuscode antwortet, versucht spiritflow die Zustellung automatisch erneut.
| Versuch | Verzögerung |
|---|---|
| 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, wird er automatisch deaktiviert.
Timeout: Webhook-Endpunkte müssen innerhalb von 10 Sekunden antworten.
Webhooks verwalten
Die Webhook-Verwaltung ist über die UI-API verfügbar (nicht über die Public API). Zugriff erfordert die Rolle TENANT_ADMIN.
Basis-URL: /api/webhooks
Authentifizierung: Session-basiert (JWT-Cookie), nicht über API-Key.
Verfügbare Event-Typen abrufen
GET /api/webhooks/event-types
Webhooks auflisten
GET /api/webhooks
Webhook erstellen
POST /api/webhooks
Request-Body:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
name | string | Ja | Name des Webhooks (max. 100 Zeichen) |
url | string | Ja | Ziel-URL (max. 2048 Zeichen) |
eventTypes | string[] | Ja | Mindestens ein Event-Typ |
description | string | Nein | Beschreibung (max. 500 Zeichen) |
Wichtig: Das
secretwird nur einmalig bei der Erstellung angezeigt! Speichere es sicher ab.
Limit: Pro Mandant sind maximal 10 Webhooks zulässig.
Webhook aktualisieren
PUT /api/webhooks/{id}
Alle Felder sind optional. Nur angegebene Felder werden aktualisiert.
Webhook löschen
DELETE /api/webhooks/{id}
Antwort: 204 No Content
Webhook aktivieren / deaktivieren
PATCH /api/webhooks/{id}/enable
PATCH /api/webhooks/{id}/disable
Webhook testen
POST /api/webhooks/{id}/test
Sendet ein Test-Ping-Event an den Webhook-Endpunkt.
Zustellungs-Historie abrufen
GET /api/webhooks/{id}/deliveries?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 |
Mandant / Organisation (Tenant)
Die Tenant-Endpunkte ermöglichen den Zugriff auf Stammdaten, Rechnungs-Einstellungen und Bankverbindung des eigenen Mandanten.
Mandanten-Daten abrufen
GET /api/v1/tenant
Scope: tenant:read
Antwort:
{
"data": {
"id": 1,
"name": "Muster GmbH",
"phone": "+49 211 123456",
"website": "https://www.muster-gmbh.de",
"industry": "IT-Dienstleistungen",
"teamSize": 25,
"defaultWeeklyWorkingHours": 40.0,
"annualVacationDays": 30,
"workingDays": "[\"MO\", \"TU\", \"WE\", \"TH\", \"FR\"]",
"createdAt": "2025-01-15T08:00:00Z"
}
}
Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
id | number | Interne ID des Mandanten |
name | string | Name der Organisation |
phone | string? | Telefonnummer |
website | string? | Website |
industry | string? | Branche |
teamSize | number? | Teamgröße (Anzahl Mitarbeiter) |
defaultWeeklyWorkingHours | number | Standard-Wochenarbeitsstunden |
annualVacationDays | number | Jährliche Urlaubstage |
workingDays | string? | Arbeitstage als JSON-Array (z.B. ["MO","TU","WE","TH","FR"]) |
createdAt | string? | Erstellungszeitpunkt (ISO 8601 UTC) |
Mandanten-Daten aktualisieren
PUT /api/v1/tenant
Scope: tenant:write
Partial Update — nur angegebene Felder werden geändert. Sicherheitsrelevante Felder (Status, Speicherkontingent etc.) können nicht verändert werden.
Request-Body:
{
"name": "Neue Firma GmbH",
"phone": "+49 211 654321",
"website": "https://www.neue-firma.de",
"industry": "Handwerk",
"teamSize": 10,
"defaultWeeklyWorkingHours": 38.5,
"annualVacationDays": 28,
"workingDays": "[\"MO\", \"TU\", \"WE\", \"TH\", \"FR\"]"
}
Antwort (200 OK): Aktualisiertes Mandanten-Objekt im data-Feld.
Rechnungs-Einstellungen abrufen
GET /api/v1/tenant/billing-settings
Scope: tenant:read
Antwort:
{
"data": {
"companyName": "Muster GmbH",
"street": "Musterstraße",
"houseNumber": "42",
"postalCode": "40210",
"city": "Düsseldorf",
"country": "Deutschland",
"phone": "+49 211 123456",
"email": "buchhaltung@muster-gmbh.de",
"website": "https://www.muster-gmbh.de",
"taxId": "123/456/78900",
"vatId": "DE123456789",
"hasLogo": true,
"logoOriginalFilename": "logo.png",
"defaultPaymentDays": 14,
"defaultTaxRate": 19.00,
"invoiceNumberFormat": "INV-{YEAR}-{COUNTER}",
"nextInvoiceCounter": 42,
"counterMinLength": 5,
"cancellationNumberFormat": "ST-{YEAR}-{COUNTER}",
"nextCancellationCounter": 1,
"templateStyle": "CLASSIC",
"logoPosition": "TOP_LEFT",
"logoScale": 100,
"primaryColor": "#333333",
"accentColor": "#7B1FA2",
"footerText": "Vielen Dank für Ihr Vertrauen.",
"closingText": null,
"attachReportDefault": false,
"includePaymentQrCode": true,
"defaultEInvoiceProfile": "EN16931",
"defaultLeitwegId": null,
"updatedAt": "2025-03-01T10:00:00"
}
}
Rechnungs-Einstellungen aktualisieren
PUT /api/v1/tenant/billing-settings
Scope: tenant:write
Partial Update — nur angegebene Felder werden geändert.
Request-Body (Beispiel):
{
"companyName": "Neue Firma GmbH",
"defaultPaymentDays": 30,
"defaultTaxRate": 19.00,
"footerText": "Wir freuen uns auf die weitere Zusammenarbeit."
}
| Feld | Typ | Beschreibung |
|---|---|---|
companyName | string? | Firmenname für Rechnungen |
street | string? | Straße |
houseNumber | string? | Hausnummer |
postalCode | string? | Postleitzahl |
city | string? | Stadt |
country | string? | Land |
phone | string? | Telefonnummer |
email | string? | E-Mail-Adresse für Rechnungen |
website | string? | Website |
taxId | string? | Steuernummer |
vatId | string? | USt-IdNr. |
defaultPaymentDays | number? | Standard-Zahlungsziel in Tagen |
defaultTaxRate | number? | Standard-Mehrwertsteuersatz in Prozent |
invoiceNumberFormat | string? | Rechnungsnummern-Format (z.B. INV-{YEAR}-{COUNTER}) |
counterMinLength | number? | Mindestlänge des Zählers mit führenden Nullen |
footerText | string? | Fußzeilen-Text der Rechnung |
closingText | string? | Abschlusstext unterhalb des Gesamtbetrags |
defaultEInvoiceProfile | string? | Standard E-Rechnungs-Profil (z.B. EN16931) |
defaultLeitwegId | string? | Standard Leitweg-ID |
Antwort (200 OK): Aktualisiertes BillingSettings-Objekt im data-Feld.
Bankverbindung abrufen
GET /api/v1/tenant/bank-account
Scope: tenant:read
Antwort:
{
"data": {
"id": 1,
"accountName": "Hauptkonto",
"accountHolder": "Muster GmbH",
"iban": "DE02120300000000202051",
"ibanFormatted": "DE02 1203 0000 0000 2020 51",
"bic": "BYLADEM1001",
"bankName": "Deutsche Bank",
"updatedAt": "2025-03-01T10:00:00"
}
}
Bankverbindung erstellen / aktualisieren
PUT /api/v1/tenant/bank-account
Scope: tenant:write
Partial Update. Bei Neuanlage (noch keine Bankverbindung vorhanden) sind accountHolder und iban Pflichtfelder.
Request-Body:
{
"accountName": "Hauptkonto",
"accountHolder": "Muster GmbH",
"iban": "DE02120300000000202051",
"bic": "BYLADEM1001",
"bankName": "Deutsche Bank"
}
| Feld | Typ | Pflicht (Neuanlage) | Beschreibung |
|---|---|---|---|
accountName | string? | Nein | Bezeichnung des Kontos |
accountHolder | string? | Ja | Kontoinhaber |
iban | string? | Ja | IBAN |
bic | string? | Nein | BIC/SWIFT-Code |
bankName | string? | Nein | Name der Bank |
Antwort (200 OK): Aktualisiertes Bankverbindungs-Objekt im data-Feld.
Fehler-Codes
Alle Fehlermeldungen werden im einheitlichen Fehlerformat zurückgegeben (siehe Response-Format).
| HTTP-Status | Code | Beschreibung |
|---|---|---|
| 400 | VALIDATION_ERROR | Eingabefehler: Pflichtfeld fehlt, Wert ungültig oder Längenbeschränkung überschritten |
| 400 | BAD_REQUEST | Allgemeiner Fehler in der Anfrage |
| 401 | UNAUTHORIZED | API-Key fehlt |
| 401 | INVALID_API_KEY | API-Key ungültig oder widerrufen |
| 401 | API_KEY_EXPIRED | API-Key ist abgelaufen |
| 403 | IP_NOT_ALLOWED | IP-Adresse nicht in der Whitelist des API-Keys |
| 403 | ACCESS_DENIED | API-Key hat nicht den erforderlichen Scope für diese Operation |
| 404 | NOT_FOUND | Ressource nicht gefunden oder nicht zu diesem Mandanten gehörig |
| 409 | CONFLICT | Ressourcenkonflikt (z.B. E-Mail oder Benutzername bereits vergeben) |
| 422 | VIRUS_DETECTED | Datei wurde als infiziert erkannt |
| 429 | RATE_LIMIT_EXCEEDED | Rate Limit überschritten |
| 500 | INTERNAL_ERROR | Interner Serverfehler |
| 503 | VIRUS_SCAN_FAILED | Virenscanner nicht verfügbar |
| 503 | VIRUS_SCAN_TIMEOUT | Virenscanner hat nicht rechtzeitig geantwortet |
Rate Limiting
Die Public API begrenzt die Anzahl der Anfragen pro API-Key.
Limit: 1.000 Anfragen pro Minute pro API-Key
Response-Header
| Header | Beschreibung |
|---|---|
X-RateLimit-Limit | Maximale Anfragen pro Minute |
X-RateLimit-Remaining | Verbleibende Anfragen im aktuellen Zeitfenster |
X-RateLimit-Reset | Unix-Timestamp, wann das Zeitfenster zurückgesetzt wird |
Antwort bei Rate Limit Überschreitung
Wenn das Rate Limit überschritten wird, antwortet der Server mit HTTP 429 Too Many Requests:
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. You have sent too many requests. Please wait before retrying.",
"status": 429,
"details": {
"retryAfterSeconds": 30
}
},
"meta": {
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2025-03-01T10:30:00Z"
}
}
Empfehlung: Implementiere Exponential Backoff in deiner Anwendung, um bei einem
429-Fehler nicht sofort erneut anzufragen.