Absences API
Die Absences API verwaltet Abwesenheiten (Urlaub, Krank, Schulung …) inklusive Vertreter (Substitute) und Manager-Genehmigung. Abwesenheiten steuern die Verfügbarkeit bei der Zuweisung: abwesende Agents werden ausgefiltert, und Zuweisungen an Abwesende werden zum Zuweisungszeitpunkt auf den Vertreter umgeleitet.
Typen & Status
AbsenceType: VACATION, SICK, TRAINING, BUSINESS_TRIP, PARENTAL, COMPENSATORY, OTHER
AbsenceStatus: PENDING → APPROVED / REJECTED, CANCELLED
Wird eine bereits genehmigte Abwesenheit wesentlich geändert (z.B. Zeitraum), wechselt sie zurück auf PENDING und muss erneut genehmigt werden.
Endpoints /api/absences
| Method | Endpoint | Permission | Beschreibung |
|---|---|---|---|
GET | / | absences.viewAll | Kalender-Fenster, Antwort { data } (Filter: startDate/endDate/userId/groupId/type/status; der type-Filter verlangt zusätzlich viewDetails) |
GET | /my | absences.viewOwn | Eigene Abwesenheiten, { data } (die 20 jüngsten) |
GET | /team | absences.viewTeam | Team-Abwesenheiten je Zeile mit canEdit/canApprove (die 100 jüngsten) |
GET | /agents | tickets.assignable | Auswahlliste für die Vertretung, { data }. Erfordert tickets.assignable (das Recht, das jemanden als Vertretung infrage kommen lässt) statt eines Anlege-Rechts, weil die Liste Namen und E-Mail-Adressen aller aktiven Agents enthält. |
GET | /groups | absences.viewAll ‖ viewTeam | Agent-Gruppen für den Kalender-Filter, { data } |
GET | /user/:userId/status | nur Anmeldung | Präsenz-Signal für Zuweisungs-Flächen — die EINZIGE Route der Domäne, die auch ein API-Key aufrufen darf. Eine unbekannte User-ID liefert 200 mit isAbsent: false. |
POST | / | absences.create / createForTeam / createForAll | Abwesenheit anlegen |
PATCH | /:id | editOwn / editTeam / editAll | Teil-Update (ggf. zurück auf PENDING). Der Inhaber (userId) ist nicht änderbar. |
POST | /:id/approve | absences.approve + Vorgesetzten-Bezug | Genehmigen (nur im Status PENDING) |
POST | /:id/reject | absences.reject + Vorgesetzten-Bezug | Ablehnen — { reason } ist PFLICHT (leer oder fehlend = 400) |
DELETE | /:id | cancelOwn / cancelOwnApproved / cancelTeam / cancelAll | Stornieren → 204. Die eigene, bereits genehmigte Abwesenheit verlangt cancelOwnApproved. |
Manager-Logik: Entscheiden darf, wer das passende Recht trägt (approve für Genehmigen, reject für Ablehnen) UND entweder DIREKTER Manager des Antragstellers ist oder absences.editAll besitzt. Die Manager-Beziehung ist bewusst direkt und NICHT transitiv: der Manager des Managers entscheidet nicht mit, solange er nicht selbst eingetragen ist. Die eigene Abwesenheit kann niemand entscheiden (403), auch nicht mit editAll. Dieselben Regeln gelten für den Weg über die Genehmigungs-Inbox — beide laufen durch dieselbe Prüfung. Grundlage ist die Manager-Hierarchie aus Users & Roles.
Art und Grund: eingeschränkte Sichtbarkeit
Die ART einer Abwesenheit ist ein Gesundheitsdatum, sobald sie SICK lauten kann (Art. 9 DSGVO), der GRUND ein Freitext daneben. Beide sind deshalb enger gestellt als die Abwesenheit selbst, die Kalender und Team-Tab jedem Berechtigten zeigen. Art und Grund sieht nur, wer mindestens eine dieser drei Bedingungen erfüllt:
absences.viewDetails— organisationsweit (Administration/Personal)- DIREKTER Manager der Zeile — nur für die eigenen Mitarbeiter
- der Inhaber selbst
Allen anderen liefert JEDER Leseweg — Kalender, Team-Tab und Status-Route — type: OTHER und reason: null. Die Felder sind damit kein Beleg dafür, was in der Datenbank steht. Der TeamLead zählt bewusst NICHT dazu: das ist eine Queue-Rolle für Zuweisung und Skills, keine Personalrolle; er sieht den Team-Tab weiterhin, aber ohne Art und Grund.
Der Typ-Filter ist gesperrt, nicht stillgelegt: GET /api/absences?type=SICK verlangt absences.viewDetails und antwortet ohne dieses Recht 403 ABSENCE_VIEW_FORBIDDEN. Würde der Filter stattdessen ignoriert, ließe sich die maskierte Art durch Vergleich der Trefferzahlen zurückrechnen.
Aus demselben Grund verlassen Art und Grund das Produkt auch nicht über Benachrichtigungen: weder E-Mail-Betreff noch Body, Webex-, Teams- oder In-App-Nachricht tragen sie. Die Ablehnungs-Begründung bleibt enthalten — sie ist die Nachricht der Führungskraft an den Betroffenen.
Abwesenheit anlegen
POST /api/absences
{
"userId": "clx-user",
"type": "VACATION",
"startDate": "2026-02-10",
"endDate": "2026-02-21",
"allDay": true,
"reason": "Annual vacation",
"substituteId": "clx-substitute"
}
Halbtags/zeitgenau: allDay=false → startTime und endTime (HH:MM) sind dann Pflicht. Regeln: endDate ≥ startDate; substituteId ≠ userId; Overlap mit bestehenden Abwesenheiten desselben Users wird abgelehnt. createForTeam/createForAll erlauben das Anlegen für andere.
// allDay=false (partial day)
{ "userId": "clx-user", "type": "OTHER", "startDate": "2026-01-30", "endDate": "2026-01-30",
"allDay": false, "startTime": "08:00", "endTime": "12:00", "reason": "Doctor appointment" }
Eingabe-Regeln
- Unbekannte Felder werden abgelehnt: ein unbekanntes Feld im Body oder ein Fremd-Parameter an der Kalender-Abfrage ist 400 VALIDATION_ERROR mit Feld-Pfad. Das gilt auch für userId im PATCH: der Inhaber einer Abwesenheit ist nicht änderbar.
- Leeren geht mit null: im PATCH leeren reason und substituteId das Feld, wenn sie als null kommen; ein fehlender Schlüssel lässt es unverändert. Beim Anlegen lässt man ein leeres Feld einfach weg.
- Datumsangaben sind ISO-Strings — ein unlesbares Datum meldet den Feld-Pfad, nicht einen Datenbank-Fehler.
- Parallele Aufrufe: Anlage, Änderung, Stornierung und Entscheidung sind gegen Wettläufe abgesichert. Von zwei gleichzeitigen Anlagen im selben Zeitraum gewinnt eine (die andere bekommt 409), von zwei gleichzeitigen Stornierungen oder Entscheidungen ebenfalls (400) — es gibt kein zweites „Erfolgreich".
Fehlercodes
| errorCode | HTTP | Bedeutung |
|---|---|---|
ABSENCE_OVERLAP | 409 | Der Zeitraum überschneidet sich mit einer bestehenden Abwesenheit desselben Benutzers. Die ID der kollidierenden Zeile steht in details.overlappingAbsenceId; die Meldung nennt bewusst KEINE Art — der Kollisionspartner kann eine Krankmeldung sein. |
ABSENCE_NOT_EDITABLE | 400 | Bearbeiten ist nur in PENDING und APPROVED möglich. |
ABSENCE_NOT_CANCELLABLE | 400 | Stornieren ist nur in PENDING und APPROVED möglich. |
ABSENCE_NOT_PENDING | 400 | Entschieden wird nur, was noch PENDING ist. |
REASON_REQUIRED | 400 | Eine Ablehnung ohne Begründung — auf beiden Wegen (Domain-Route und Genehmigungs-Inbox). Reine Leerzeichen zählen nicht als Begründung. |
ABSENCE_VIEW_FORBIDDEN | 403 | Der Typ-Filter wurde ohne absences.viewDetails benutzt. |
ABSENCE_MANAGE_FORBIDDEN | 403 | Anlegen, Ändern oder Stornieren ohne das passende Recht (eigene / Team / alle). |
ABSENCE_APPROVE_FORBIDDEN | 403 | Entscheiden ohne Recht oder ohne Manager-Bezug — auch der Versuch, die eigene Abwesenheit zu entscheiden. |
ABSENCE_NOT_FOUND | 404 | Keine Abwesenheit mit dieser ID. |
VALIDATION_ERROR | 400 | Schema-Verstoß mit Feld-Pfad in details — unbekanntes Feld, unlesbares Datum, endDate vor startDate, Vertretung gleich Inhaber, oder fehlende Uhrzeiten bei allDay: false. |
Verfügbarkeit & Vertretung
Abwesenheiten wirken an zwei Stellen, jeweils im Moment der Zuweisung:
- Filterung: /api/users/assignable und die Assignment-Engine schließen aktuell abwesende Agents aus.
- Substitute-Redirect: Wird einem abwesenden Agent dennoch zugewiesen (z.B. direkt), leitet das System auf dessen Vertreter um. Ist auch dieser abwesend, folgt es der Vertretungskette weiter — über höchstens drei Stationen und ohne Kreise. Bewusstes Zuweisen an einen Abwesenden ist mit ignoreSubstitution=true möglich (siehe assign-Endpoints in Tickets/Incidents/Problems/Changes). Wer den Vorgang am Ende bekommt, ist geprüft verfügbar: die Vertretung muss dieselben Hürden nehmen wie ein direkt gewählter Agent (Postfach, Agent-Gruppe, Zuweisbarkeit), und die Kette muss auf jemandem enden, der selbst nicht abwesend ist. Sonst bleibt der Vorgang beim abwesenden Original — sichtbar dort liegend statt scheinbar in Arbeit. Bei Tickets hält der Verlauf die Tatsache fest, dass die Vertretung nicht übernehmen konnte. Den GRUND nennt nur das Audit-Log (SUBSTITUTE_BLOCKED, wenn der Vertretung eine Berechtigung fehlt; SUBSTITUTE_UNAVAILABLE, wenn die Kette auf einer abwesenden Person endet): dass einer dritten Person ein Recht fehlt, gehört nicht in einen Vorgang, den jeder Beteiligte liest. Beide Einträge hängen an der betroffenen Person, nicht am Vorgang — im Audit-Log sucht man sie also über die Vertretung.
GET /api/absences/user/:userId/status liefert kompakt { isAbsent, absenceType, absenceEndDate, substituteId, substituteName } — z.B. für UI-Hinweise. Die Felder sind gestaffelt: isAbsent bekommt jeder angemeldete Aufrufer, Vertretung und Enddatum (ISO-8601) verlangen absences.viewStatus, und absenceType zusätzlich die Berechtigung für Art und Grund (siehe oben) — sonst steht dort OTHER.
- ✓ 7 Typen, Workflow PENDING→APPROVED/REJECTED
- ✓ Genehmigung durch den direkten Vorgesetzten
- ✓ Umleitung auf die Vertretung im Moment der Zuweisung
- ✓ Schutz vor Überschneidungen, erneute Genehmigung nach Änderung
absences.viewOwn/viewTeam/viewAll/viewDetails/viewStatusabsences.create/createForTeam/createForAllabsences.editOwn/editTeam/editAllabsences.cancelOwn/cancelOwnApproved/cancelTeam/cancelAllabsences.approve/reject– nur als direkter Vorgesetzter oder mit editAll
Auth-/Rollenmodell: Permissions & RBAC
- Users, Roles & Agent Groups – Manager-Hierarchie, Agents, Verfügbarkeit
- Tickets API – Zuweisung & ignoreSubstitution
- SLA Management – Verfügbarkeit im Eskalations-Kontext