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

SituationHTTP-StatusFehlercode
API-Key fehlt oder leer401UNAUTHORIZED
API-Key ungültig oder widerrufen401INVALID_API_KEY
API-Key abgelaufen401API_KEY_EXPIRED
IP-Adresse nicht erlaubt403IP_NOT_ALLOWED
Scope fehlt403ACCESS_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:

ParameterTypStandardBeschreibung
pageInteger0Seitennummer (0-basiert)
sizeInteger20Einträge pro Seite (max. 100)
sortStringabhängig vom EndpunktSortierfeld und Richtung, Format: feld,richtung

Beispiele für sort:

  • createdAt,desc - Neueste zuerst
  • title,asc - Alphabetisch aufsteigend
  • dueDate,asc - Frühestes Fälligkeitsdatum zuerst

Scopes

Jeder API-Key trägt eine oder mehrere Berechtigungen (Scopes). Die verfügbaren Scopes sind:

ScopeBeschreibungErlaubte Operationen
tasks:readAufgaben lesenGET-Anfragen auf /api/v1/tasks
tasks:writeAufgaben schreibenPOST, PUT, PATCH, DELETE auf /api/v1/tasks
customers:readKunden lesenGET-Anfragen auf /api/v1/customers
customers:writeKunden schreibenPOST, PUT, DELETE auf /api/v1/customers
projects:readProjekte lesenGET-Anfragen auf /api/v1/projects
projects:writeProjekte schreibenPOST, PUT, DELETE auf /api/v1/projects
invoices:readRechnungen lesenGET-Anfragen auf /api/v1/invoices (Read-Only)
interactions:readInteraktionen lesenGET-Anfragen auf /api/v1/customers/{id}/interactions
interactions:writeInteraktionen schreibenPOST auf /api/v1/customers/{id}/interactions
cti:readCTI-Daten lesenGET-Anfragen auf /api/v1/cti/lookup und /api/v1/cti/device-mappings
cti:writeCTI-Events sendenPOST auf /api/v1/cti/events/incoming-call
users:readBenutzer lesenGET-Anfragen auf /api/v1/users
users:writeBenutzer anlegenPOST auf /api/v1/users, POST auf /api/v1/users/{id}/avatar
time_entries:readZeiteinträge lesenGET-Anfragen auf /api/v1/time-entries
time_entries:writeZeiteinträge schreibenPOST, PUT, DELETE auf /api/v1/time-entries
vacation:readUrlaubsanträge lesenGET-Anfragen auf /api/v1/vacation-requests
vacation:writeUrlaubsanträge erstellenPOST auf /api/v1/vacation-requests
sick_leave:readKrankmeldungen lesenGET-Anfragen auf /api/v1/sick-leaves
sick_leave:writeKrankmeldungen erstellenPOST auf /api/v1/sick-leaves
tags:readTags lesenGET-Anfragen auf /api/v1/tags
tags:writeTags erstellenPOST auf /api/v1/tags
webhooks:readWebhooks lesenGET-Anfragen auf /api/v1/webhooks
webhooks:writeWebhooks verwaltenPOST, PUT, DELETE auf /api/v1/webhooks
receipts:readBelege lesenGET-Anfragen auf /api/v1/receipts
receipts:writeBelege erstellen und bearbeitenPOST, PATCH, DELETE auf /api/v1/receipts
notifications:writeBenachrichtigungen sendenPOST auf /api/v1/notifications, /bulk, /broadcast
tenant:readMandantendaten lesenGET-Anfragen auf /api/v1/tenant
tenant:writeMandantendaten ändernPUT-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:

FeldTypBeschreibung
apiKeyIdnumberID des API-Keys
apiKeyNamestringName des API-Keys (z.B. “AI-Worker”)
tenantIdnumberID des zugehörigen Mandanten
createdByUserIdnumberID des Erstellers
scopesstring[]Liste der zugewiesenen Scopes
expiresAtstring?Ablaufzeitpunkt (ISO 8601), null = kein Ablauf
excludedProjectIdsnumber[]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:

FeldTypPflichtBeschreibung
nameStringJaBezeichnung des Keys (max. 100 Zeichen)
scopesString[]JaMindestens ein gültiger Scope
expiresAtInstantNeinAblaufdatum (ISO 8601 UTC), null = kein Ablauf
allowedIpsStringNeinIP-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 apiKey im 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):

FeldTypBeschreibung
nameStringNeue Bezeichnung (max. 100 Zeichen)
scopesString[]Neue Scope-Liste (ersetzt bestehende)
allowedIpsStringNeue 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

StatusBedeutung
BACKLOGBacklog (kein Assignee erforderlich)
DRAFTEntwurf (kein Assignee erforderlich)
PLANNEDGeplant
IN_PROGRESSIn Bearbeitung
READY_FOR_REVIEWBereit zur Prüfung (erfordert einen Prüfer/Reviewer)
IN_REVIEWIn Prüfung
COMPLETEDAbgeschlossen
BLOCKEDBlockiert
REJECTEDAbgelehnt
ON_HOLDPausiert

Hinweis: Alle Status außer BACKLOG und DRAFT erfordern einen zugewiesenen Benutzer (assigneeId). Wird ein Task ohne Assignee auf einen anderen Status gesetzt, gibt die API einen 400 VALIDATION_ERROR zurück.

Aufgaben-Prioritäts-Werte

PrioritätBedeutung
LOWNiedrig
MEDIUMMittel
HIGHHoch
CRITICALKritisch

Aufgaben auflisten

GET /api/v1/tasks

Scope: tasks:read

Query-Parameter:

ParameterTypPflichtBeschreibung
statusStringNeinFiltern nach Status (z.B. IN_PROGRESS)
priorityStringNeinFiltern nach Priorität (z.B. HIGH)
assigneeIdLongNeinFiltern nach zugewiesenem Benutzer (ID)
projectIdLongNeinFiltern nach Projekt (ID)
archivedBooleanNeinArchivierte einschließen (Standard: false)
searchStringNeinVolltextsuche in Titel und Beschreibung (case-insensitive)
searchNotesBooleanNeinWenn true, wird die Suche auf Kommentare/Notizen erweitert (Standard: false)
createdAfterInstantNeinNur Aufgaben, die nach diesem Zeitpunkt erstellt wurden (ISO 8601 UTC)
createdBeforeInstantNeinNur Aufgaben, die vor diesem Zeitpunkt erstellt wurden (ISO 8601 UTC)
updatedAfterInstantNeinNur Aufgaben, die nach diesem Zeitpunkt aktualisiert wurden (ISO 8601 UTC)
updatedBeforeInstantNeinNur Aufgaben, die vor diesem Zeitpunkt aktualisiert wurden (ISO 8601 UTC)
dueDateAfterInstantNeinNur Aufgaben mit Fälligkeitsdatum nach diesem Zeitpunkt (ISO 8601 UTC)
dueDateBeforeInstantNeinNur Aufgaben mit Fälligkeitsdatum vor diesem Zeitpunkt (ISO 8601 UTC)
plannedStartAfterInstantNeinNur Aufgaben mit geplantem Start nach diesem Zeitpunkt (ISO 8601 UTC)
plannedStartBeforeInstantNeinNur Aufgaben mit geplantem Start vor diesem Zeitpunkt (ISO 8601 UTC)
pageIntegerNeinSeite (Standard: 0)
sizeIntegerNeinEinträge pro Seite, max. 100 (Standard: 20)
sortStringNeinSortierung (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

ParameterTypBeschreibung
idLongID der Aufgabe

Antwort (200 OK): Vollständiges PublicTaskDto-Objekt (gleiche Struktur wie in der Liste)

HTTP-StatusCodeBeschreibung
404NOT_FOUNDAufgabe 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:

ParameterTypBeschreibung
ticketNumberStringTicketnummer im Format PREFIX-NUMMER, z.B. PROJ-0042 oder SPIR-0276

Antwort: Identisch mit GET /api/v1/tasks/{id}.

Fehler-Codes:

HTTP-StatusCodeBeschreibung
404NOT_FOUNDAufgabe 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:

FeldTypPflichtStandardBeschreibung
titleStringJa-Titel der Aufgabe (max. 500 Zeichen)
descriptionStringNeinnullBeschreibung
statusStringNeinBACKLOGStatus (siehe Status-Werte). Achtung: Alle Status außer BACKLOG und DRAFT erfordern eine assigneeId.
priorityStringNeinMEDIUMPriorität (siehe Prioritäts-Werte)
assigneeIdLongNeinnullID des zugewiesenen Benutzers
reviewerIdLongNeinnullID des Prüfers
projectIdLongNeinnullID des zugehörigen Projekts
dueDateInstantNeinnullFälligkeitsdatum (ISO 8601 UTC)
plannedStartDateInstantNeinnullGeplantes Startdatum (ISO 8601 UTC)
timeBudgetMinutesIntegerNeinnullZeitbudget in Minuten
tagIdsLong[]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-StatusCodeBeschreibung
400VALIDATION_ERRORPflichtfeld fehlt oder ungültig
404NOT_FOUNDReferenziertes 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):

FeldTypBeschreibung
titleStringNeuer Titel (max. 500 Zeichen)
descriptionStringNeue Beschreibung (null = löschen)
statusStringNeuer Status
priorityStringNeue Priorität
assigneeIdLongNeue Zuweisung (null = entfernen)
reviewerIdLongNeuer Prüfer (null = entfernen)
projectIdLongNeues Projekt (null = aus Projekt entfernen)
dueDateInstantNeues Fälligkeitsdatum (null = entfernen)
plannedStartDateInstantNeues geplantes Startdatum (null = entfernen)
timeBudgetMinutesIntegerNeues Zeitbudget in Minuten
progressIntegerFortschritt in Prozent (0-100)
tagIdsLong[]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=true abgerufen 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:

FeldTypBeschreibung
idnumberEindeutige ID des Kommentars
contentstringInhalt des Kommentars
noteTypestringTyp: COMMENT, FIELD_REPORT oder SYSTEM
authorIdnumberID des Autors
authorNamestringAnzeigename des Autors
createdAtstringErstellungszeitpunkt (ISO 8601, UTC)
updatedAtstringLetzte 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:

FeldTypPflichtBeschreibung
contentstringJaInhalt des Kommentars
noteTypestringNeinTyp 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:

ParameterTypBeschreibung
taskIdLongID der Aufgabe

Antwort-Felder:

FeldTypBeschreibung
idnumberEindeutige ID des Anhangs
fileNamestringAngezeigter Dateiname
fileSizenumberDateigröße in Bytes
contentTypestringMIME-Typ der Datei
attachedBystring?Anzeigename des Uploaders
createdAtstringZeitpunkt 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:

ParameterTypBeschreibung
taskIdLongID der Aufgabe
attachmentIdLongID des Anhangs

Antwort (200 OK): Datei als Binärdaten

Response-Header:

HeaderBeispielwert
Content-Typeapplication/pdf (je nach Dateityp)
Content-Dispositionattachment; 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:

ParameterTypBeschreibung
taskIdLongID der Aufgabe

Formular-Felder:

FeldTypPflichtBeschreibung
fileFileJaDie hochzuladende Datei
descriptionStringNeinOptionale 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-StatusCodeBeschreibung
400BAD_REQUESTDatei zu groß oder Speicherkontingent erschöpft
404NOT_FOUNDAufgabe nicht gefunden
422VIRUS_DETECTEDDatei wurde als infiziert erkannt
503VIRUS_SCAN_FAILEDVirenscanner nicht verfügbar
503VIRUS_SCAN_TIMEOUTVirenscanner 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:

FeldTypBeschreibung
idnumberEindeutige ID des Benutzers
displayNamestringAnzeigename
emailstringE-Mail-Adresse
rolesstring[]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:

ParameterTypBeschreibung
idLongID des Benutzers

Antwort (200 OK): Identisch mit den Einträgen der Benutzerliste.


Benutzer anlegen

POST /api/v1/users

Scope: users:write

Request-Body:

FeldTypPflichtBeschreibung
usernamestringJaBenutzername (eindeutig innerhalb des Mandanten)
emailstringJaE-Mail-Adresse (systemweit eindeutig)
firstNamestringJaVorname
lastNamestringJaNachname
passwordstringJaPasswort (wird serverseitig gehasht)
rolesstring[]NeinRollen, Standard: ["USER"]. Erlaubt: USER, TEAM_LEADER, PROJECT_MANAGER, EXTERNAL_MEMBER, FIELD_USER
employmentStartDatestringNeinBeschäftigungsbeginn (YYYY-MM-DD)
weeklyWorkingHoursnumberNeinWochenstunden (überschreibt Mandanten-Standard)
workingDaysstring[]NeinArbeitstage, z.B. ["MONDAY","TUESDAY","WEDNESDAY","THURSDAY","FRIDAY"]

Antwort (201 Created): Gleiche Felder wie bei der Benutzerliste.

Fehler-Codes:

HTTP-StatusCodeBeschreibung
402LICENSE_SEAT_LIMIT_REACHEDLizenz-Sitzplatzlimit erreicht
409CONFLICTE-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:

ParameterTypBeschreibung
idLongID des Benutzers

Request-Body (alle Felder optional):

FeldTypBeschreibung
firstNameStringVorname (max. 100 Zeichen)
lastNameStringNachname (max. 100 Zeichen)
displayNameStringAnzeigename (max. 201 Zeichen). Wird in firstName und lastName aufgeteilt, wenn diese nicht explizit angegeben sind.
usernameStringBenutzername (eindeutig innerhalb des Mandanten, max. 100 Zeichen)
rolesString[]Rollen. Erlaubt: USER, TEAM_LEADER, PROJECT_MANAGER, EXTERNAL_MEMBER, FIELD_USER. Nicht erlaubt: TENANT_ADMIN, SUPER_ADMIN.
employmentStartDateStringEintrittsdatum (YYYY-MM-DD)
employmentEndDateStringAustrittsdatum (YYYY-MM-DD). null = noch beschäftigt
weeklyWorkingHoursnumberIndividuelle Wochenarbeitsstunden
workingDaysString[]Arbeitstage, z.B. ["MONDAY","TUESDAY","WEDNESDAY","THURSDAY","FRIDAY"]
defaultHourlyRatenumberStandard-Stundensatz in Euro
annualVacationDaysnumberIndividuelle Urlaubstage pro Jahr
jobTitleStringBerufsbezeichnung (max. 100 Zeichen)
mobilePhoneStringMobiltelefonnummer (max. 20 Zeichen)
enabledbooleanBenutzerkonto 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-StatusCodeBeschreibung
400VALIDATION_ERRORUngültige Feldwerte oder verbotene Rolle
404NOT_FOUNDBenutzer nicht gefunden
409CONFLICTBenutzername 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:

ParameterTypBeschreibung
idLongID des Benutzers

Antwort: 204 No Content

Fehler-Codes:

HTTP-StatusCodeBeschreibung
403ACCESS_DENIEDVersuch den Tenant-Admin zu löschen
404NOT_FOUNDBenutzer 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

StatusBedeutung
ACTIVEAktiver Kunde
LEADLead / Interessent
PROSPECTPotenzieller Kunde
LOSTVerlorener Kunde
INACTIVEInaktiv
SUSPENDEDGesperrt
ARCHIVEDArchiviert

Kunden-Typen

TypBedeutung
COMPANYUnternehmen
INDIVIDUALPrivatperson

Kunden auflisten

GET /api/v1/customers

Scope: customers:read

Query-Parameter:

ParameterTypPflichtBeschreibung
statusStringNeinFiltern nach Status (z.B. ACTIVE)
customerTypeStringNeinFiltern nach Typ (COMPANY oder INDIVIDUAL)
searchStringNeinVolltextsuche in Name, E-Mail, Kundennummer
pageIntegerNeinSeite (Standard: 0)
sizeIntegerNeinEinträge pro Seite, max. 100 (Standard: 20)
sortStringNeinSortierung (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:

FeldTypPflichtStandardBeschreibung
nameStringJa-Name des Kunden (max. 255 Zeichen)
customerNumberStringNeinnullKundennummer (max. 50 Zeichen)
emailStringNeinnullE-Mail-Adresse (max. 255 Zeichen)
phoneStringNeinnullTelefonnummer (max. 50 Zeichen)
mobileStringNeinnullMobilnummer (max. 50 Zeichen)
streetStringNeinnullStrasse und Hausnummer
postalCodeStringNeinnullPostleitzahl (max. 10 Zeichen)
cityStringNeinnullStadt (max. 100 Zeichen)
countryStringNeinnullLand (max. 100 Zeichen)
websiteStringNeinnullWebseite (max. 100 Zeichen)
descriptionStringNeinnullNotizen / Beschreibung
taxNumberStringNeinnullSteuernummer (max. 50 Zeichen)
vatIdStringNeinnullUSt-IdNr. (max. 50 Zeichen)
statusStringNeinACTIVEStatus (siehe Status-Werte)
industryStringNeinnullBranche (max. 100 Zeichen)
customerTypeStringNeinCOMPANYKundentyp

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:

ParameterTypBeschreibung
idLongID des Kunden

Form-Parameter:

ParameterTypBeschreibung
filemultipartBilddatei (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:

ParameterTypBeschreibung
idLongID 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:

ParameterTypBeschreibung
customerIdLongID des Kunden

Antwort-Felder:

FeldTypBeschreibung
idnumberEindeutige ID der Kontaktperson
customerIdnumberID des zugehörigen Kunden
salutationstring?Anrede
firstNamestringVorname
lastNamestringNachname
positionstring?Position / Funktion
departmentstring?Abteilung
emailstring?E-Mail-Adresse
phonestring?Telefonnummer
mobilestring?Mobilnummer
isPrimarybooleanIst Hauptansprechpartner
notesstring?Notizen
preferredLanguagestring?Bevorzugte Sprache (ISO-Code)
createdAtstringErstellungszeitpunkt (ISO 8601)
updatedAtstringLetzter Ä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:

FeldTypPflichtBeschreibung
firstNamestringJaVorname (max. 100 Zeichen)
lastNamestringJaNachname (max. 100 Zeichen)
salutationstringNeinAnrede (max. 20 Zeichen)
positionstringNeinPosition (max. 200 Zeichen)
departmentstringNeinAbteilung (max. 100 Zeichen)
emailstringNeinE-Mail-Adresse
phonestringNeinTelefonnummer (max. 50 Zeichen)
mobilestringNeinMobilnummer (max. 50 Zeichen)
isPrimarybooleanNeinHauptansprechpartner (Standard: false)
notesstringNeinNotizen
preferredLanguagestringNeinSprachcode (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

StatusBedeutung
PLANNEDGeplant
ACTIVEAktiv
COMPLETEDAbgeschlossen
ARCHIVEDArchiviert

Projekt-Prioritäts-Werte

PrioritätBedeutung
LOWNiedrig
NORMALNormal
HIGHHoch
CRITICALKritisch

Projekte auflisten

GET /api/v1/projects

Scope: projects:read

Query-Parameter:

ParameterTypPflichtBeschreibung
statusStringNeinFiltern nach Status (z.B. ACTIVE)
searchStringNeinVolltextsuche in Titel und Beschreibung
pageIntegerNeinSeite (Standard: 0)
sizeIntegerNeinEinträge pro Seite, max. 100 (Standard: 20)
sortStringNeinSortierung (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:

FeldTypPflichtStandardBeschreibung
titleStringJa-Projekttitel (max. 255 Zeichen)
numberPrefixStringJa-Ticket-Präfix (Grossbuchstaben/Ziffern, 1-10 Zeichen)
descriptionStringNeinnullProjektbeschreibung
colorStringNein#3F51B5Farbe als Hex-Code (#RRGGBB)
statusStringNeinPLANNEDStatus
priorityStringNeinNORMALPriorität
startDateLocalDateNeinnullStartdatum (YYYY-MM-DD)
endDateLocalDateNeinnullEnddatum (YYYY-MM-DD)
assigneeIdLongNeinnullID des Projektleiters
teamMemberIdsLong[]Nein[]IDs der Projektmitglieder
customerIdLongNeinnullID 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):

FeldTypBeschreibung
titleStringNeuer Titel (max. 255 Zeichen)
descriptionStringNeue Beschreibung
colorStringNeue Farbe (#RRGGBB)
statusStringNeuer Status
priorityStringNeue Priorität
startDateLocalDateNeues Startdatum
endDateLocalDateNeues Enddatum
assigneeIdLongNeuer Projektleiter
teamMemberIdsLong[]Neue Teammitglieder (ersetzt alle bisherigen)
customerIdLongNeuer 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:

ParameterTypBeschreibung
idLongID des Projekts

Form-Parameter:

ParameterTypBeschreibung
filemultipartBilddatei (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:

ParameterTypBeschreibung
idLongID 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)

ParameterTypPflichtBeschreibung
pageIntegerNeinSeite (Standard: 0)
sizeIntegerNeinEinträge pro Seite, max. 100 (Standard: 20)
sortStringNeinSortierung (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

StatusBedeutung
DRAFTEntwurf
SENTVersendet
PAIDBezahlt
OVERDUEÜberfällig
CANCELLEDStorniert

Rechnungen auflisten

GET /api/v1/invoices

Scope: invoices:read

Query-Parameter:

ParameterTypPflichtBeschreibung
statusStringNeinFiltern nach Status (z.B. PAID)
customerIdLongNeinFiltern nach Kunden-ID
searchStringNeinSuche in Rechnungsnummer und Titel
pageIntegerNeinSeite (Standard: 0)
sizeIntegerNeinEinträge pro Seite, max. 100 (Standard: 20)
sortStringNeinSortierung (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) ist invoiceNumber null.


Rechnungs-PDF herunterladen

GET /api/v1/invoices/{id}/pdf

Scope: invoices:read

Antwort (200 OK): PDF-Datei als Binärdaten

Response-Header:

HeaderBeispielwert
Content-Typeapplication/pdf
Content-Dispositionattachment; 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

StatusBedeutung
UPLOADEDHochgeladen, noch nicht eingereicht
SUBMITTEDEingereicht, wartet auf Prüfung
IN_REVIEWIn Prüfung
APPROVEDGenehmigt
SETTLEDAbgerechnet / erledigt
WITHDRAWNZurückgezogen (durch Einreicher)
REJECTEDAbgelehnt (durch Prüfenden)
VOIDEDEntwertet

Zahlungsstatus-Werte

StatusBedeutung
OPENOffen / noch nicht bezahlt
PAIDBezahlt
OVERDUEÜberfällig

Beleg-Typen

TypBedeutung
INVOICEEingangsrechnung (Standard)
RECEIPTKassenbeleg
CREDIT_NOTEGutschrift
TRAVEL_EXPENSEReisekostenabrechnung
OTHERSonstiger Beleg

Belege auflisten

GET /api/v1/receipts

Scope: receipts:read

Query-Parameter:

ParameterTypPflichtBeschreibung
statusStringNeinFiltert nach Belegstatus (z.B. APPROVED)
paymentStatusStringNeinFiltert nach Zahlungsstatus (OPEN, PAID, OVERDUE)
categoryIdLongNeinFiltert nach Kategorie-ID
receiptDateAfterStringNeinBelegdatum ab (YYYY-MM-DD)
receiptDateBeforeStringNeinBelegdatum bis (YYYY-MM-DD)
supplierNameStringNeinTextsuche im Lieferantennamen
activeBooleanNeintrue = nur aktive Belege (UPLOADED..SETTLED), false = nur inaktive (WITHDRAWN/REJECTED/VOIDED), nicht gesetzt = alle
pageIntegerNeinSeitennummer (Standard: 0)
sizeIntegerNeinEinträge pro Seite, max. 100 (Standard: 20)
sortStringNeinSortierung (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:

ParameterTypBeschreibung
idLongID 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:

FeldTypPflichtStandardBeschreibung
receiptDateStringJa-Belegdatum (YYYY-MM-DD)
grossAmountnumberJa-Bruttobetrag (>= 0)
externalInvoiceNumberStringNeinnullExterne Rechnungsnummer (max. 255 Zeichen)
typeStringNeinINVOICEBelegtyp (siehe Beleg-Typen)
supplierNameStringNeinnullLieferantenname (max. 255 Zeichen)
dueDateStringNeinnullFälligkeitsdatum (YYYY-MM-DD)
netAmountnumberNeinnullNettobetrag
vatRatenumberNeinnullMehrwertsteuersatz in Prozent
vatAmountnumberNeinnullMehrwertsteuerbetrag
currencyStringNeinEURWährung (3-stelliger ISO-Code)
paymentStatusStringNeinnullZahlungsstatus
paymentMethodStringNeinnullZahlungsart
paymentDateStringNeinnullZahlungsdatum (YYYY-MM-DD)
categoryIdLongNeinnullID der Kategorie
categoryNameStringNeinnullKategoriename (alternativ zu categoryId)
projectIdLongNeinnullID des zugehörigen Projekts
notesStringNeinnullNotizen (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:

ParameterTypBeschreibung
idLongID 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:

  • UPLOADEDSUBMITTED
  • SUBMITTEDIN_REVIEW
  • IN_REVIEWAPPROVED
  • APPROVEDSETTLED

Für Zurückziehen, Ablehnen und Entwerten die dedizierten Endpoints verwenden.

Scope: receipts:write

Pfad-Parameter:

ParameterTypBeschreibung
idLongID des Belegs

Request-Body:

FeldTypPflichtBeschreibung
statusStringJaZielstatus

Antwort (200 OK): Aktualisiertes Belegobjekt


Beleg zurückziehen

POST /api/v1/receipts/{id}/withdraw

Zieht einen eingereichten Beleg zurück (SUBMITTEDWITHDRAWN). Nur möglich aus Status SUBMITTED. Eine Begründung ist Pflicht.

Scope: receipts:write

Pfad-Parameter:

ParameterTypBeschreibung
idLongID des Belegs

Request-Body:

FeldTypPflichtBeschreibung
reasonStringJaBegrü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_REVIEWREJECTED). Nur möglich aus Status IN_REVIEW. Eine Begründung ist Pflicht.

Scope: receipts:write

Pfad-Parameter:

ParameterTypBeschreibung
idLongID des Belegs

Request-Body:

FeldTypPflichtBeschreibung
reasonStringJaBegrü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/REJECTEDVOIDED). Eine Begründung ist Pflicht.

Scope: receipts:write

Pfad-Parameter:

ParameterTypBeschreibung
idLongID des Belegs

Request-Body:

FeldTypPflichtBeschreibung
reasonStringJaBegründung für das Entwerten
replacedByReceiptIdLongNeinID 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:

ParameterTypBeschreibung
idLongID des Belegs

Antwort-Felder:

FeldTypBeschreibung
fromStatusString?Ausgangsstatus (null beim ersten Eintrag)
toStatusStringZielstatus
changedByNameStringName des Akteurs
changedAtStringZeitpunkt der Änderung (ISO 8601, UTC)
reasonString?Begründung (bei Ablehnen, Zurückziehen, Entwerten)
sourceStringQuelle 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:

ParameterTypBeschreibung
idLongID des Belegs

Antwort-Felder:

FeldTypBeschreibung
idnumberID des Anhangs
displayFilenameStringDateiname
contentTypeString?MIME-Typ
sizeBytesnumberDateigröße in Bytes
isPrimarybooleanIst dieser Anhang der Hauptbeleg
attachedByNameStringName des Hochladers
attachedAtStringHochladezeitpunkt (ISO 8601, UTC)

Anhang herunterladen

GET /api/v1/receipts/{id}/attachments/{attachmentId}/download

Lädt eine Anhang-Datei herunter.

Scope: receipts:read

Pfad-Parameter:

ParameterTypBeschreibung
idLongID des Belegs
attachmentIdLongID 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:

ParameterTypBeschreibung
idLongID des Belegs

Form-Parameter:

ParameterTypPflichtBeschreibung
filemultipartJaHochzuladende Datei
isPrimarybooleanNeinOb 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:

FeldTypBeschreibung
idnumberID der Kategorie
nameStringName der Kategorie
datevAccountNumberString?DATEV-Kontonummer für Steuerexport
colorString?Farbe der Kategorie (#RRGGBB)
sortOrdernumberSortierposition

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:

ParameterTypStandardBeschreibung
formatStringjsonExportformat: json oder csv
statusString-Filter nach Status (z.B. APPROVED)
dateFromString-Belegdatum ab (YYYY-MM-DD)
dateToString-Belegdatum bis (YYYY-MM-DD)
activeBoolean-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:

FeldTypPflichtStandardBeschreibung
receiptsArrayJa-Liste der zu importierenden Belege (max. 100, gleiche Felder wie beim Erstellen)
skipDuplicatesbooleanNeinfalseDuplikate ü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:

StatusBedeutung
createdBeleg erfolgreich erstellt
skippedDuplikat übersprungen (bei skipDuplicates: true)
errorValidierungsfehler, 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:

ParameterTypPflichtBeschreibung
userIdLongNeinNach Benutzer filtern
projectIdLongNeinNach Projekt filtern
startDatestringNeinStartdatum (YYYY-MM-DD)
endDatestringNeinEnddatum (YYYY-MM-DD)
pageintNeinSeitennummer (Standard: 0)
sizeintNeinEinträge pro Seite, max. 100 (Standard: 50)

Antwort-Felder:

FeldTypBeschreibung
idnumberEindeutige ID
userIdnumberID des Benutzers
userDisplayNamestringAnzeigename des Benutzers
projectIdnumber?ID des Projekts (null bei allgemeinen Aufgaben)
projectNamestring?Name des Projekts
taskIdnumber?ID der verknüpften Aufgabe
taskTitlestring?Titel der verknüpften Aufgabe
datestringDatum des Eintrags (YYYY-MM-DD)
startTimestring?Startzeit (HH:mm)
endTimestring?Endzeit (HH:mm)
durationMinutesnumberDauer in Minuten
durationHoursnumberDauer in Dezimalstunden
descriptionstring?Beschreibung
billablebooleanAbrechenbar
billedbooleanBereits abgerechnet
hourlyRatenumber?Stundensatz zum Zeitpunkt des Eintrags
revenuenumber?Berechneter Umsatz
approvalStatusstringPENDING, APPROVED oder REJECTED
entryTypestringWORK, TRAVEL, BREAK oder FLAT_FEE
externalReferencestring?Externe Referenz (z.B. ERP-Nummer)
archivedbooleanArchiviert (gelöscht)
createdAtstringErstellungszeitpunkt (ISO 8601)
updatedAtstringLetzter Ä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:

FeldTypPflichtBeschreibung
userIdnumberJaID des Benutzers
projectIdnumberNeinID des Projekts
taskIdnumberNeinID der Aufgabe
datestringJaDatum (YYYY-MM-DD)
startTimestringNeinStartzeit (HH:mm)
endTimestringNeinEndzeit (HH:mm)
durationMinutesnumberNeinDauer in Minuten (min. 1)
descriptionstringNeinBeschreibung (max. 2000 Zeichen)
billablebooleanNeinAbrechenbar (Standard: true)
entryTypestringNeinWORK (Standard), TRAVEL, BREAK oder FLAT_FEE
externalReferencestringNeinExterne Referenz (max. 255 Zeichen)

Hinweis: Entweder durationMinutes oder startTime+endTime mü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:

ParameterTypPflichtBeschreibung
statusstringNeinFilter: PENDING, APPROVED, REJECTED, CANCELLED
ownerIdLongNeinNach Benutzer filtern

Antwort-Felder:

FeldTypBeschreibung
idnumberEindeutige ID
titlestringTitel des Antrags
typestringVACATION, SPECIAL_LEAVE oder UNPAID_LEAVE
statusstringPENDING, APPROVED, REJECTED oder CANCELLED
ownerIdnumberID des Antragstellers
ownerNamestringName des Antragstellers
startDatestringStartdatum (YYYY-MM-DD)
endDatestringEnddatum (YYYY-MM-DD)
daysCountnumberAnzahl der Urlaubstage
reasonstring?Begründung
createdAtstringErstellungszeitpunkt (ISO 8601)
updatedAtstringLetzter Ä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:

FeldTypPflichtBeschreibung
ownerIdnumberJaID des Benutzers
typestringNeinVACATION (Standard), SPECIAL_LEAVE oder UNPAID_LEAVE
startDatestringJaStartdatum (YYYY-MM-DD)
endDatestringJaEnddatum (YYYY-MM-DD)
reasonstringNeinBegründung
titlestringNeinTitel (wird automatisch generiert falls leer)
autoApprovebooleanNeinAutomatisch genehmigen (Standard: false)

Fehler-Codes:

HTTP-StatusCodeBeschreibung
409CONFLICTÜ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:

ParameterTypPflichtBeschreibung
ownerIdLongNeinNach 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:

FeldTypPflichtBeschreibung
ownerIdnumberJaID des Benutzers
startDatestringJaErster Krankheitstag (YYYY-MM-DD)
endDatestringJaLetzter Krankheitstag (YYYY-MM-DD)
reasonstringNeinOptionaler 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:

FeldTypBeschreibung
idnumberEindeutige ID
namestringTag-Name
colorstring?Farbcode im Hex-Format (z.B. #FF5733)
createdAtstringErstellungszeitpunkt (ISO 8601)

Tag erstellen

POST /api/v1/tags

Scope: tags:write

Request-Body:

FeldTypPflichtBeschreibung
namestringJaTag-Name (pro Mandant eindeutig)
colorstringNeinFarbcode 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

ParameterTypDefaultBeschreibung
pagenumber0Seitennummer (0-basiert)
sizenumber20Einträ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:

FeldTypPflichtBeschreibung
interactionTypestringJaArt der Interaktion (siehe unten)
subjectstringJaBetreff (max. 255 Zeichen)
contentstringNeinInhalt/Notizen (max. 5.000 Zeichen)
interactionDatestringNeinISO 8601 Zeitstempel. Default: aktuelle Zeit
durationMinutesnumberNeinDauer in Minuten
outcomestringNeinErgebnis der Interaktion (max. 500 Zeichen)
contactPersonIdnumberNeinID der Kontaktperson beim Kunden
followUpDatestringNeinISO 8601 Zeitstempel für Wiedervorlage

Interaktionstypen:

WertBeschreibung
PHONE_CALLTelefonat
EMAIL_INEingehende E-Mail
EMAIL_OUTAusgehende E-Mail
MEETINGBesprechung
VISITVor-Ort-Besuch
NOTENotiz/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 lastContactDate des Kunden wird automatisch auf den Interaktionszeitpunkt gesetzt. Wird ein followUpDate angegeben, wird nextContactDate des 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
  1. Eingehender Anruf: TK-Anlage meldet Anruf → Middleware ruft POST /events/incoming-call → spiritflow zeigt Screen-Pop beim Benutzer
  2. Click-to-Dial: Benutzer klickt Telefonnummer in spiritflow → Webhook cti.dial_requested → Middleware empfängt und initiiert Anruf über TK-Anlage
  3. 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.

ParameterTypPflichtBeschreibung
phonestringJaTelefonnummer (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 phone und mobile sowohl 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:

FeldTypPflichtBeschreibung
callerNumberstringJaAnrufende Telefonnummer
calledDevicestringJaAngerufene Nebenstelle (z.B. "201", "**610")
callIdstringNeinEindeutige Call-ID der TK-Anlage
timestampstringNeinISO 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:

SituationErgebnis
Nebenstelle zugeordnet + Kunde gefundenScreen-Pop mit Kundenname, Klick navigiert zum Kunden
Nebenstelle zugeordnet + Kunde unbekanntScreen-Pop mit Telefonnummer
Nebenstelle nicht zugeordnetprocessed: 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:

FeldTypPflichtBeschreibung
callIdstringJaEindeutige Call-ID der TK-Anlage
devicestringJaNebenstelle, die den Anruf angenommen hat
callerNumberstringNeinAnrufende 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:

FeldTypPflichtBeschreibung
callIdstringJaEindeutige Call-ID der TK-Anlage
durationnumberNeinDauer des Anrufs in Sekunden (null wenn nicht angenommen)
answeredbooleanNeinOb der Anruf angenommen wurde (Standard: false)
answeredByDevicestringNeinNebenstelle, die den Anruf angenommen hat
callerNumberstringNeinAnrufende 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:

  1. Webhook mit Event cti.dial_requested muss eingerichtet sein
  2. Der Benutzer muss eine Nebenstelle konfiguriert haben
  3. 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:

FeldTypPflichtBeschreibung
recipientIdLongJaID des Empfängers (muss zum Mandanten gehören)
titleStringJaTitel der Benachrichtigung (max. 255 Zeichen)
messageStringJaNachrichtentext (max. 5000 Zeichen)
payloadStringNeinOptionale JSON-Nutzdaten (max. 10000 Zeichen)
targetTypeStringNeinZieltyp für Verlinkung (z.B. TASK, PROJECT, CUSTOMER)
targetIdLongNeinID des verlinkten Objekts
expiresAtStringNeinAblaufdatum (ISO 8601, UTC)
priorityIntegerNeinPrioritä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:

FeldTypPflichtBeschreibung
recipientIdsLong[]JaListe der Empfänger-IDs (mind. 1)
titleStringJaTitel der Benachrichtigung
messageStringJaNachrichtentext
payloadStringNeinOptionale JSON-Nutzdaten
targetTypeStringNeinZieltyp für Verlinkung
targetIdLongNeinID des verlinkten Objekts
expiresAtStringNeinAblaufdatum (ISO 8601, UTC)
priorityIntegerNeinPrioritä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:

FeldTypPflichtBeschreibung
titleStringJaTitel der Benachrichtigung
messageStringJaNachrichtentext
payloadStringNeinOptionale JSON-Nutzdaten
targetTypeStringNeinZieltyp für Verlinkung
targetIdLongNeinID des verlinkten Objekts
expiresAtStringNeinAblaufdatum (ISO 8601, UTC)
priorityIntegerNeinPrioritä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:

KanalBeschreibung
WebSocketSofort sichtbar in der spiritflow-App (Benachrichtigungs-Glocke)
E-MailWenn der Benutzer E-Mail-Benachrichtigungen aktiviert hat
PushAuf 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

EventBeschreibung
task.createdNeue Aufgabe wurde erstellt
task.updatedAufgaben-Felder wurden geändert
task.status_changedAufgaben-Status wurde geändert
task.assignedBearbeiter einer Aufgabe wurde geändert
task.deletedAufgabe 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.updated als auch task.status_changed aus.

Projekt-Events

EventBeschreibung
project.createdNeues Projekt wurde erstellt
project.updatedProjekt-Felder wurden geändert
project.archivedProjekt wurde archiviert/gelöscht

Kunden-Events

EventBeschreibung
customer.createdNeuer Kunde wurde erstellt
customer.updatedKunden-Felder wurden geändert
customer.archivedKunde wurde archiviert/gelöscht

CTI-Events (Telefonie)

EventBeschreibung
cti.incoming_callEingehender Anruf wurde gemeldet
cti.dial_requestedAnruf wurde über Click-to-Dial angefordert

Benachrichtigungs-Events

EventBeschreibung
notification.createdNeue 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

FeldTypBeschreibung
idstringEindeutige Event-ID (UUID)
eventstringEvent-Typ (z.B. task.created, project.updated)
created_atstringISO 8601 Zeitstempel in UTC
sourcestringQuelle des Events: "api" oder "ui"
actor.idnumberID des Auslöser-Benutzers
actor.typestringTyp des Auslöser: "api_key" oder "user"
actor.namestringName des API-Keys oder Benutzers
dataobjectVollständiges Objekt nach der Änderung
changesobjectGeänderte Felder mit from/to-Werten. Leer bei *.created-Events.

HTTP-Header

HeaderBeschreibungBeispiel
Content-TypeImmer application/jsonapplication/json
User-AgentAbsender-Kennungspiritflow-Webhooks/1.0
X-spiritflow-EventEvent-Typtask.updated
X-spiritflow-DeliveryEindeutige Delivery-IDa1b2c3d4-...
X-spiritflow-SignatureHMAC-SHA256 Signatursha256=abc123...

Signatur-Verifizierung

Jeder Webhook-Request wird mit einer HMAC-SHA256-Signatur versehen.

Algorithmus:

  1. Den vollständigen Request-Body als UTF-8-String nehmen
  2. HMAC-SHA256 mit dem Webhook-Secret als Schlüssel berechnen
  3. Das Ergebnis als Hex-String formatieren
  4. 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.

VersuchVerzögerung
1Sofort
21 Minute
35 Minuten
430 Minuten
52 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:

FeldTypPflichtBeschreibung
namestringJaName des Webhooks (max. 100 Zeichen)
urlstringJaZiel-URL (max. 2048 Zeichen)
eventTypesstring[]JaMindestens ein Event-Typ
descriptionstringNeinBeschreibung (max. 500 Zeichen)

Wichtig: Das secret wird 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:

StatusBeschreibung
PENDINGZustellung steht aus
DELIVEREDErfolgreich zugestellt
FAILEDZustellung endgültig fehlgeschlagen
RETRYINGErneuter 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:

FeldTypBeschreibung
idnumberInterne ID des Mandanten
namestringName der Organisation
phonestring?Telefonnummer
websitestring?Website
industrystring?Branche
teamSizenumber?Teamgröße (Anzahl Mitarbeiter)
defaultWeeklyWorkingHoursnumberStandard-Wochenarbeitsstunden
annualVacationDaysnumberJährliche Urlaubstage
workingDaysstring?Arbeitstage als JSON-Array (z.B. ["MO","TU","WE","TH","FR"])
createdAtstring?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."
}
FeldTypBeschreibung
companyNamestring?Firmenname für Rechnungen
streetstring?Straße
houseNumberstring?Hausnummer
postalCodestring?Postleitzahl
citystring?Stadt
countrystring?Land
phonestring?Telefonnummer
emailstring?E-Mail-Adresse für Rechnungen
websitestring?Website
taxIdstring?Steuernummer
vatIdstring?USt-IdNr.
defaultPaymentDaysnumber?Standard-Zahlungsziel in Tagen
defaultTaxRatenumber?Standard-Mehrwertsteuersatz in Prozent
invoiceNumberFormatstring?Rechnungsnummern-Format (z.B. INV-{YEAR}-{COUNTER})
counterMinLengthnumber?Mindestlänge des Zählers mit führenden Nullen
footerTextstring?Fußzeilen-Text der Rechnung
closingTextstring?Abschlusstext unterhalb des Gesamtbetrags
defaultEInvoiceProfilestring?Standard E-Rechnungs-Profil (z.B. EN16931)
defaultLeitwegIdstring?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"
}
FeldTypPflicht (Neuanlage)Beschreibung
accountNamestring?NeinBezeichnung des Kontos
accountHolderstring?JaKontoinhaber
ibanstring?JaIBAN
bicstring?NeinBIC/SWIFT-Code
bankNamestring?NeinName 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-StatusCodeBeschreibung
400VALIDATION_ERROREingabefehler: Pflichtfeld fehlt, Wert ungültig oder Längenbeschränkung überschritten
400BAD_REQUESTAllgemeiner Fehler in der Anfrage
401UNAUTHORIZEDAPI-Key fehlt
401INVALID_API_KEYAPI-Key ungültig oder widerrufen
401API_KEY_EXPIREDAPI-Key ist abgelaufen
403IP_NOT_ALLOWEDIP-Adresse nicht in der Whitelist des API-Keys
403ACCESS_DENIEDAPI-Key hat nicht den erforderlichen Scope für diese Operation
404NOT_FOUNDRessource nicht gefunden oder nicht zu diesem Mandanten gehörig
409CONFLICTRessourcenkonflikt (z.B. E-Mail oder Benutzername bereits vergeben)
422VIRUS_DETECTEDDatei wurde als infiziert erkannt
429RATE_LIMIT_EXCEEDEDRate Limit überschritten
500INTERNAL_ERRORInterner Serverfehler
503VIRUS_SCAN_FAILEDVirenscanner nicht verfügbar
503VIRUS_SCAN_TIMEOUTVirenscanner 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

HeaderBeschreibung
X-RateLimit-LimitMaximale Anfragen pro Minute
X-RateLimit-RemainingVerbleibende Anfragen im aktuellen Zeitfenster
X-RateLimit-ResetUnix-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.