SLA Management API
Das Unified SLA System überwacht Service-Level-Ziele für Tickets, Incidents und Problems: Reaktions- und Lösungsziele je Priorität und Policy, Fristen nach Geschäftszeiten inklusive Feiertagen, Pause bei ON_HOLD, offenem Incident-Link oder offenem Sub-Ticket, mehrstufige Eskalation und einen laufenden SLA-Monitor. Auch der Status (OK/WARNING/BREACH/CRITICAL) wird nach Geschäftszeiten berechnet.
Status-Modell
Der prozentuale Verbrauch (percentUsed) wird business-hours-genau aus „verbleibender Geschäftszeit bis zur Deadline" berechnet. Bei Pause friert der Status zum pausedAt-Zeitpunkt ein.
| Status | Bedingung |
|---|---|
OK | percentUsed < 80% |
WARNING | percentUsed ≥ 80% |
BREACH | percentUsed ≥ 100% (Deadline überschritten) |
CRITICAL | > 60 Minuten nach Breach |
CANCELLED | Tracking storniert (Entity gelöscht/irrelevant) |
Schwellwerte konfigurieren
Beide Schwellen sind einstellbar; 80 % und 60 Minuten sind die Standardwerte. Sie liegen unter dem Settings-Key sla-settings (Admin-Center → Service-Konfiguration → SLA & Eskalation → Schwellwerte). Lesen und Schreiben erfordern settings.editSLA:
PUT /api/settings/sla-settings
{
"warningThresholdPercent": 80, // 50–99
"criticalThresholdMinutes": 60 // 5–1440
}
| Eigenschaft | Verhalten |
|---|---|
| Wirkung | Gilt für die serverseitige Statusberechnung (Dashboard, Ticket-Sidebar, Monitor/Eskalation) — der Status kommt überall aus dem Backend. |
| Wirksamkeit | Eine Änderung über die Settings-API wirkt sofort. |
| Robustheit | Fehlt das Setting oder liegen Werte außerhalb der Grenzen, gelten die Standardwerte 80 / 60. Die Statusberechnung läuft also immer. |
Public Endpoints
Basis-Pfad /api/sla. Das SLA-Dashboard ist eine Arbeitssicht für Agenten: Mit viewAll zeigt es alle Trackings, ohne viewAll nur die der Tickets, die der Agent sehen darf.
| Method | Endpoint | Permission | Beschreibung |
|---|---|---|---|
GET | /api/sla/trackings | tickets.viewAll ‖ tickets.viewOwn | Aktive Trackings (gefiltert, mit Metriken) |
GET | /api/sla/stats | tickets.viewAll ‖ tickets.viewOwn | Aggregierte Counts pro Status |
GET | /api/sla/report | tickets.viewAll (bzw. incidents/problems.viewAll je entityType) | Historische Compliance-Quoten (met/missed, MTTA/MTTR) |
GET | /api/sla/holidays | jede Auth | Feiertagskalender (?year, ?country) → {data} |
Berechtigung: trackings und stats erfordern tickets.viewAll oder tickets.viewOwn. Ohne viewAll ist zusätzlich ein aktives Agenten-Profil nötig, Endanwender haben also keinen Zugriff. Die Antwort enthält dann nur TICKET-Trackings, und zwar für genau die Tickets, die der Agent auch in der Ticket-Liste sieht (eigene, über Gruppe, Mailbox oder Beteiligung, inklusive Vertretung). Incident- und Problem-Trackings erfordern incidents.viewAll bzw. problems.viewAll.
Trackings, deren Entität der Aufrufer nicht sehen darf, erscheinen nicht in der Antwort; total zählt nur die sichtbaren.
GET /trackings — Query-Parameter
| Parameter | Werte |
|---|---|
entityType | TICKET, INCIDENT, PROBLEM |
status | OK, WARNING, BREACH, CRITICAL, CANCELLED, PAUSED |
search | Entity-Nummer oder Titel (max. 200 Zeichen, case-insensitive) — serverseitig vor der Pagination |
limit | 1–100 (Default 25) |
offset | Pagination-Offset |
Statusfilter, Sortierung (verbrauchte Zeit absteigend) und Pagination laufen vollständig serverseitig; der Statusfilter nutzt den vom SLA-Monitor berechneten Status. Trackings, die der Monitor noch nicht bewertet hat, gelten als OK.
Pausierte zählen getrennt: status=PAUSED filtert alle pausierten Uhren — unabhängig davon, welcher Status bei der Pause eingefroren wurde. Umgekehrt schließen die Statusfilter OK, WARNING, BREACH und CRITICAL Pausierte aus. So stimmen Liste und Zähler aus /stats überein. Ohne Statusfilter sind Pausierte in der Liste enthalten.
GET /api/sla/trackings?entityType=TICKET&status=WARNING&search=TK-000123&limit=20
{
"data": [
{
"id": "clx-tracking",
"entityType": "TICKET",
"entityId": "clx-ticket",
"entityNumber": "TK-000123",
"entityTitle": "Database connection timeout",
"priority": "HIGH",
"status": "WARNING",
"percentUsed": 85.5,
"responseMet": true,
"resolutionMet": null,
"responseDeadline": "2026-01-28T10:00:00.000Z",
"resolutionDeadline": "2026-01-28T17:00:00.000Z",
"isPaused": false,
"pauseReason": null,
"currentEscalationLevel": 1,
"minutesToDeadline": 83,
"minutesAfterBreach": null
}
],
"pagination": { "total": 42, "limit": 20, "offset": 0 }
}
pauseReason nennt den Grund einer LAUFENDEN Pause und ist sonst null. Bei den beiden Verknüpfungs-Gründen trägt der Wert die Nummer der Gegenseite: LINKED_TO_INCIDENT:INC-000042 für einen offenen Incident-Link, CHILD_TICKET:TK-000456 für ein offenes Sub-Ticket. Eine Pause aus dem Status heraus (pauseOnStatus) trägt keinen solchen Wert. Welche Gründe es gibt und wann sie greifen, beschreibt SLA-System (Architektur).
GET /stats
GET /api/sla/stats
{ "total": 102, "ok": 80, "warning": 15, "breach": 5, "critical": 2, "paused": 7 }
stats zählt den AKTUELLEN Zustand (offene Trackings je Status, pausierte separat) — es ist keine Compliance-Kennzahl. Historische Erfüllungsquoten liefert ausschließlich /report.
GET /report
Historische Auswertung über abgeschlossene Trackings (resolvedAt im Zeitraum). Trackings mit excludeFromReporting oder Status CANCELLED gehen NICHT in die Quoten ein, werden aber informativ mitgezählt. Zeitraum entweder rollierend über days oder explizit über from/to.
| Parameter | Werte |
|---|---|
days | 1–365 (Default 30, rollierend ab jetzt) |
from / to | ISO-8601; from muss vor to liegen (sonst 400 VALIDATION_ERROR) |
entityType | TICKET, INCIDENT, PROBLEM (ohne Angabe: alle) |
GET /api/sla/report?days=30
GET /api/sla/report?from=2026-06-01T00:00:00Z&to=2026-07-01T00:00:00Z&entityType=INCIDENT
{
"from": "2026-06-16T00:00:00.000Z",
"to": "2026-07-16T00:00:00.000Z",
"totals": {
"resolved": 412,
"met": 383,
"missed": 29,
"compliancePct": 92.9,
"responseMet": 396,
"responseMissed": 16,
"responseCompliancePct": 96.1,
"excluded": 7,
"cancelled": 3,
"avgResponseMin": 24,
"avgResolutionMin": 268
},
"byEntityType": [
{ "entityType": "TICKET", "met": 350, "missed": 24, "compliancePct": 93.6 }
],
"byPriority": [
{ "entityType": "TICKET", "priority": "HIGH", "met": 88, "missed": 9, "responseMet": 94, "responseMissed": 3 }
],
"scope": "global"
}
| Feld | Bedeutung |
|---|---|
compliancePct | met / (met + missed) · auf eine Nachkommastelle gerundet; ohne gewertete Trackings = 100 |
avgResponseMin | MTTA: Ø responseAt − createdAt (Kalenderminuten); null ohne Datenbasis |
avgResolutionMin | MTTR: Ø resolvedAt − createdAt abzüglich Pausenzeit; null ohne Datenbasis |
excluded / cancelled | informative Zähler, die nicht in die Quoten eingehen |
scope | global = alle Daten (viewAll) · own = nur die für den Agenten sichtbaren Tickets. Wer die Zahlen anzeigt, sollte das Feld auswerten — eine 92%-Quote über die eigenen Tickets ist etwas völlig anderes als 92% über alle. |
Die nötige Berechtigung hängt von entityType ab: INCIDENT erfordert incidents.viewAll, PROBLEM problems.viewAll, alles andere (auch der Gesamtreport ohne entityType) tickets.viewAll. Ohne viewAll gilt wie beim Dashboard die Agenten-Sicht: Mit tickets.viewOwn und aktivem Agenten-Profil liefert der Report die Historie der Tickets, die der Agent sehen darf (dieselbe Auswahl wie bei /trackings). Das gilt nur für Tickets; eine Incident- oder Problem-Historie erfordert immer das jeweilige viewAll (sonst 403).
Admin Endpoints
Basis-Pfad /api/sla/admin. Alle Endpoints erfordern settings.editSLA.
| Bereich | Endpoints |
|---|---|
| SLA Policies | GET /policies, GET /policies/:id, POST /policies, PATCH /policies/:id, DELETE /policies/:id |
| Escalation Policies | GET /escalation-policies, GET /:id, POST, PATCH /:id, DELETE /:id |
| Business Hours | GET /business-hours, GET /:id, POST, PATCH /:id, DELETE /:id |
| Holidays | GET /holidays, GET /:id, POST, POST /bulk, PATCH /:id, DELETE /:id, DELETE /year/:year |
| Helpers | GET /agent-groups, GET /users (für Eskalations-Ziele) |
Änderungen laufen per PATCH. Die Default-Policy wird über das Feld isDefault festgelegt: Beim Speichern mit isDefault=true verliert die bisherige Default-Policy desselben entityType diesen Status automatisch.
Änderungen an einer Business-Hours-Definition (Zeitplan, Zeitzone, Feiertagsbezug) wirken sofort auf die Deadline-Berechnung. Die Helfer-Listen für Eskalations-Ziele (/agent-groups, /users) führen nur aktive, nicht archivierte Konten ohne System-Benutzer.
SLA-Policy
POST /api/sla/admin/policies
{
"name": "Premium Support SLA",
"description": "Aggressive targets, 24/7",
"entityType": "TICKET",
"targets": {
"CRITICAL": { "responseMin": 15, "resolutionMin": 120 },
"HIGH": { "responseMin": 30, "resolutionMin": 240 },
"MEDIUM": { "responseMin": 60, "resolutionMin": 480 },
"LOW": { "responseMin": 120, "resolutionMin": 1440 }
},
"businessHoursId": null,
"escalationPolicyId": "clx-escalation",
"categoryIds": [],
"pauseOnStatus": ["ON_HOLD"],
"pauseOnIncidentLink": true,
"pauseOnChildTicket": true,
"isDefault": false,
"isActive": true
}
| Feld | Beschreibung |
|---|---|
entityType | TICKET, INCIDENT, PROBLEM |
targets | Map Priorität → { responseMin?, resolutionMin }. responseMin optional (z.B. Problems ohne Response-SLA). |
businessHoursId | null = 24/7; sonst business-hours-aware Deadlines |
escalationPolicyId | optionale Eskalations-Policy |
categoryIds | Einschränkung auf bestimmte Kategorien (leer = alle). Zwei AKTIVE Policies desselben entityType dürfen sich keine Kategorie teilen → 409 SLA_CATEGORY_OVERLAP. |
pauseOnStatus | Status, bei denen die SLA pausiert (Default [ON_HOLD]) |
pauseOnIncidentLink | SLA pausiert, wenn die Entity an einen offenen Incident gelinkt ist (Default true) |
pauseOnChildTicket | SLA des Elterntickets pausiert, solange eines seiner Sub-Tickets offen ist (Default true, nur entityType TICKET) |
isDefault | Default-Policy für diesen entityType |
Eskalations-Policy
levels ist ein JSON-Array. Jede Stufe hat einen Trigger und Aktionen:
POST /api/sla/admin/escalation-policies
{
"name": "Standard Escalation",
"levels": [
{
"level": 1,
"triggerType": "PERCENTAGE", // PERCENTAGE | BREACH | TIME_AFTER_BREACH
"triggerValue": 80,
"slaType": "RESOLUTION", // RESPONSE | RESOLUTION
"actions": [
{ "type": "NOTIFY", "notifyTargets": ["ASSIGNEE"] }
]
},
{
"level": 2,
"triggerType": "BREACH",
"triggerValue": 0,
"slaType": "RESOLUTION",
"actions": [
{ "type": "NOTIFY", "notifyTargets": ["ASSIGNEE", "GROUP_LEAD", "MANAGER", "CUSTOM"], "customUserIds": ["clx-user-1"] },
{ "type": "REASSIGN", "reassignToGroupId": "clx-escalation-group" }
]
},
{
"level": 3,
"triggerType": "TIME_AFTER_BREACH",
"triggerValue": 60,
"slaType": "RESOLUTION",
"actions": [ { "type": "ESCALATE_PRIORITY", "prioritySteps": 1 } ]
}
],
"repeatConfig": {
"enabled": true,
"intervalMin": 120,
"maxRepeats": 5,
"notifyTargets": ["ASSIGNEE", "GROUP_LEAD"]
},
"isActive": true
}
Action type | Konfiguration |
|---|---|
NOTIFY | notifyTargets (ASSIGNEE, GROUP_LEAD, MANAGER, CUSTOM) · customUserIds[] (Pflicht bei CUSTOM) |
REASSIGN | reassignToGroupId und/oder reassignToUserId (mindestens eines; sind beide gesetzt, wird erst die Gruppe, dann der User zugewiesen) |
ESCALATE_PRIORITY | Priorität anheben (prioritySteps 1–99, Default 1) → SLA-Reset auf neue Targets |
WEBHOOK | externer Webhook-Aufruf (webhookUrl Pflicht) |
Die Eskalations-Policy legt die Empfänger fest. Über welchen Kanal (In-App/E-Mail/Push/Webex/Teams) eine SLA-Benachrichtigung zugestellt wird, bestimmen die Notification-Typ-Konfiguration und die Nutzer-Präferenzen. Empfänger werden zur Laufzeit zusätzlich gegen die Sichtbarkeit der Entity gefiltert.
Validierung der Stufen
- level: eindeutig und streng aufsteigend sortiert
PERCENTAGE: triggerValue 1–200 ·TIME_AFTER_BREACH: triggerValue ≥ 1 MinuteslaType: RESPONSE oder RESOLUTION- NOTIFY mit Ziel CUSTOM erfordert mindestens einen Eintrag in customUserIds; REASSIGN erfordert ein Ziel; WEBHOOK erfordert webhookUrl
Wiederkehrende Breach-Reminder (repeatConfig)
Jede Eskalationsstufe wird genau einmal ausgelöst. Damit ein dauerhaft verletztes SLA nicht in Vergessenheit gerät, kann die Policy nach der letzten Stufe Erinnerungen wiederholen:
| Feld | Beschreibung |
|---|---|
enabled | Wiederholung aktiv |
intervalMin | 15–10080 — Geschäftsminuten seit der letzten Eskalation/Wiederholung |
maxRepeats | 1–20 — danach ist Schluss |
notifyTargets | mindestens ein Ziel (ASSIGNEE, GROUP_LEAD, MANAGER, CUSTOM); CUSTOM erfordert customUserIds |
Jede erfolgte Wiederholung steht als REPEAT-Eintrag in der Eskalations-Historie. repeatConfig: null im PATCH entfernt die Wiederholung; ein weggelassenes Feld lässt sie unangetastet.
Business Hours & Feiertage
POST /api/sla/admin/business-hours
{
"name": "German Business Hours",
"schedule": {
"monday": { "start": "09:00", "end": "17:00" },
"tuesday": { "start": "09:00", "end": "17:00" },
"wednesday": { "start": "09:00", "end": "17:00" },
"thursday": { "start": "09:00", "end": "17:00" },
"friday": { "start": "09:00", "end": "17:00" },
"saturday": null,
"sunday": null
},
"timezone": "Europe/Berlin",
"excludeHolidays": true,
"holidayCountry": "DE",
"holidayRegion": "BY",
"isDefault": true
}
// POST /api/sla/admin/holidays (or /holidays/bulk with { holidays: [...] })
{ "name": "Tag der Deutschen Einheit", "date": "2026-10-03", "isRecurring": true, "country": "DE", "region": null }
Doppelte Feiertage: Der Einzel-POST lehnt einen bereits vorhandenen Feiertag mit 409 SLA_HOLIDAY_DUPLICATE ab. Der Bulk-Import gleicht vorher gegen den Bestand ab und meldet die Zahl der übersprungenen Einträge als skippedExisting — ein zweiter Import derselben Liste legt also nichts doppelt an.
region entscheidet über die Reichweite: region: null bedeutet landesweit — die Uhr steht dann ÜBERALL still. Regionale Feiertage (etwa Fronleichnam oder Allerheiligen) gehören deshalb mit ihrer Region eingetragen, sonst pausieren sie die SLA auch dort, wo gearbeitet wird.
Automatischer Feiertags-Import
Deutsche Feiertage müssen nicht von Hand gepflegt werden: ein Hintergrund-Job berechnet sie (inkl. der beweglichen über die Osterformel) und legt sie für das aktuelle UND das nächste Jahr an — und zwar für jede Land/Region-Kombination, die in einer Business-Hours-Definition tatsächlich verwendet wird. Genau dieses Fenster liest die Deadline-Berechnung.
| Eigenschaft | Wert |
|---|---|
| Zeitplan | Jährlich am 1. November, 04:00 — rechtzeitig, bevor die Deadline-Berechnung das Folgejahr braucht. Zusätzlich beim Start des Workers (füllt frische Installationen und verpasste Jahre sofort). |
| Idempotenz | Mehrfachläufe erzeugen keine Dubletten — bestehende Einträge werden erkannt und übersprungen. |
| Umfang | Deutschland (bundesweite und regionale Feiertage). Feiertage anderer Länder werden über den Bulk-Import gepflegt. |
| Verwaltung | Als Cronjob „Holiday Auto-Import" sichtbar und dort abschaltbar (CronJobs) |
Beispiel: Business-Hours-Deadline
Business Hours: Mo–Fr 09:00–17:00 (8h/Tag) Ticket erstellt: Freitag 14:00 · Target: 480 min (8 Geschäftsstunden) Fr 14:00 → 17:00 = 180 min (übrig 300) Sa/So = übersprungen Mo 09:00 → 14:00 = 300 min (übrig 0) → Resolution-Deadline: Montag 14:00 (fällt Montag auf einen Feiertag → Dienstag 14:00)
SLA-Lebenszyklus
- Erstellung (automatisch): Beim Anlegen einer Entity wählt der Service die passende Policy (Kategorie-spezifisch → sonst Default des entityType), kopiert die Priority-Targets in ein SLATracking und berechnet die Deadlines (24/7 oder business-hours).
- Pause/Resume: Status in pauseOnStatus (Default ON_HOLD) ODER offener Incident-Link (pauseOnIncidentLink) ODER offenes Sub-Ticket am Elternticket (pauseOnChildTicket) pausiert die Uhr; beim Fortsetzen wird die Deadline um die pausierte Geschäftszeit verschoben (pausedTotalSec, pauseHistory) — fortgesetzt wird erst, wenn kein Grund mehr greift. Jede Pause, Fortsetzung und Stornierung schreibt zusätzlich einen sichtbaren Eintrag in die Timeline der betroffenen Entität — ein ON_HOLD-Wechsel erzeugt deshalb bewusst zwei Einträge: den Statuswechsel und die SLA-Pause.
- Priority-Change → Reset: Neue Targets aus der Policy, Deadlines ab jetzt neu, Eskalationslevel zurückgesetzt.
- Response Met: Bei Tickets zählt die erste öffentliche Antwort eines Agenten — egal ob im Web, per E-Mail-Antwort oder durch eine ausgehende E-Mail, mit der das Ticket entsteht. Eine bloße Zuweisung (an Agent, Gruppe, Mailbox oder Queue) erfüllt die Response-SLA NICHT. Bei Incidents und Problems gilt die ITIL-Semantik: Die Zuweisung bzw. Annahme gilt als Reaktion.
- Resolution Met: Schließen bzw. Lösen setzt resolutionMet: true, wenn die Resolution-Deadline gehalten wurde, sonst false (breachAt ist gesetzt).
- SLA-Monitor: Ein Background-Job im job-worker berechnet periodisch Status (calculatedStatus) und feuert fällige Eskalationsstufen.
Benachrichtigungen
SLA-Ereignisse erzeugen Notifications über das zentrale Notification-System: SLA_WARNING, SLA_RESPONSE_WARNING, SLA_BREACH, SLA_RESPONSE_BREACH, SLA_CRITICAL, SLA_ESCALATION, SLA_MET.
Kanäle & Empfänger steuert die Notification-Konfiguration.
Absences & Vertretung
Abwesenheiten (Urlaub/Krank/…) und Vertreterregelungen sind eine eigene Domain (/api/absences) und beeinflussen Zuweisung/Substitution, nicht die SLA-Uhr direkt. Absence-Typen: VACATION, SICK, TRAINING, BUSINESS_TRIP, PARENTAL, COMPENSATORY, OTHER. Absences API
- ✓ Targets pro Priorität, Policy pro entityType
- ✓ Business-hours-genaue Deadlines & Status
- ✓ Pause bei ON_HOLD / Incident-Link / Sub-Ticket
- ✓ Mehrstufige Eskalation + SLA-Monitor
tickets.viewAll‖tickets.viewOwn– SLA-Dashboard (trackings/stats) und Historie (report); ohne viewAll nur sichtbare Tickets und nur mit Agenten-Profilincidents.viewAll/problems.viewAll– Historie je entityTypesettings.editSLA– Policies/Eskalation/Business-Hours/Feiertage + Schwellwerte (sla-settings). Kritische Berechtigung: Ein Entzug wirkt sofort, verweigerte Versuche werden protokolliert.
Auth-/Rollenmodell: Permissions & RBAC
- SLA-System (Architektur)
- Tickets API – SLA-Spalte, Sortierung (sort=sla) und Filter (f.slaStatus) der Ticket-Liste · Incidents API · Problems API
- Notification-System – SLA_*