SLA Management API
The Unified SLA System tracks service-level targets for tickets, incidents and problems: prioritized response/resolution targets per policy, business-hours-aware deadlines with holidays, pause on ON_HOLD, an open incident link or an open sub-ticket, multi-level escalation and a continuous SLA monitor. Status calculation (OK/WARNING/BREACH/CRITICAL) is business-hours-accurate.
Status Model
Usage percentage (percentUsed) is computed business-hours-accurately from "remaining business time to deadline". When paused, status freezes at pausedAt.
| Status | Condition |
|---|---|
OK | percentUsed < 80% |
WARNING | percentUsed ≥ 80% |
BREACH | percentUsed ≥ 100% (deadline exceeded) |
CRITICAL | > 60 minutes after breach |
CANCELLED | Tracking cancelled (entity deleted/irrelevant) |
Configuring the thresholds
Both thresholds are configurable; 80% and 60 minutes are the defaults. They are stored under the settings key sla-settings (Admin Center → Service Configuration → SLA & Escalation → Thresholds). Reading and writing require settings.editSLA:
PUT /api/settings/sla-settings
{
"warningThresholdPercent": 80, // 50–99
"criticalThresholdMinutes": 60 // 5–1440
}
| Property | Behavior |
|---|---|
| Effect | Applies to the server-side status calculation (dashboard, ticket sidebar, monitor/escalation) — the status always comes from the backend. |
| Latency | A change via the settings API takes effect immediately. |
| Robustness | If the setting is missing or values lie outside the bounds, the defaults 80 / 60 apply, so the status calculation always runs. |
Public Endpoints
Base path /api/sla. The SLA dashboard is a work view for agents: with viewAll it shows all trackings, without it only those of tickets the agent may see.
| Method | Endpoint | Permission | Description |
|---|---|---|---|
GET | /api/sla/trackings | tickets.viewAll ‖ tickets.viewOwn | Active trackings (filtered, with metrics) |
GET | /api/sla/stats | tickets.viewAll ‖ tickets.viewOwn | Aggregated counts per status |
GET | /api/sla/report | tickets.viewAll (or incidents/problems.viewAll per entityType) | Historical compliance rates (met/missed, MTTA/MTTR) |
GET | /api/sla/holidays | any auth | Holiday calendar (?year, ?country) → {data} |
Permissions: trackings and stats require tickets.viewAll or tickets.viewOwn. Without viewAll an active agent profile is also required, so end users have no access. The response then contains TICKET trackings only, for exactly the tickets the agent also sees in the ticket list (own, via group, mailbox or participation, including substitution). Incident and problem trackings require incidents.viewAll or problems.viewAll respectively.
Trackings whose entity the caller may not see are omitted from the response; total counts only the visible ones.
GET /trackings — Query Parameters
| Parameter | Values |
|---|---|
entityType | TICKET, INCIDENT, PROBLEM |
status | OK, WARNING, BREACH, CRITICAL, CANCELLED, PAUSED |
search | Entity number or title (max 200 chars, case-insensitive) — server-side, before pagination |
limit | 1–100 (default 25) |
offset | Pagination offset |
Status filter, sorting (percent used, descending) and pagination are fully server-side; the status filter uses the status computed by the SLA monitor. Trackings the monitor has not evaluated yet count as OK.
Paused trackings count separately: status=PAUSED filters all paused clocks — regardless of which status was frozen when the pause began. Conversely, the status filters OK, WARNING, BREACH and CRITICAL exclude paused ones, so the list matches the counters from /stats. Without a status filter, paused trackings are included in the list.
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 names the reason of a RUNNING pause and is null otherwise. For the two link-based reasons the value carries the counterpart's number: LINKED_TO_INCIDENT:INC-000042 for an open incident link, CHILD_TICKET:TK-000456 for an open sub-ticket. A pause coming from the status (pauseOnStatus) carries no such value. Which reasons exist and when they apply is described in SLA System (architecture).
GET /stats
GET /api/sla/stats
{ "total": 102, "ok": 80, "warning": 15, "breach": 5, "critical": 2, "paused": 7 }
stats counts the CURRENT state (open trackings per status, paused separately) — it is not a compliance metric. Historical fulfilment rates come exclusively from /report.
GET /report
Historical evaluation over completed trackings (resolvedAt within the range). Trackings with excludeFromReporting or status CANCELLED do NOT count towards the rates but are reported informationally. Range either rolling via days or explicit via from/to.
| Parameter | Values |
|---|---|
days | 1–365 (default 30, rolling from now) |
from / to | ISO-8601; from must precede to (otherwise 400 VALIDATION_ERROR) |
entityType | TICKET, INCIDENT, PROBLEM (omitted: all) |
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"
}
| Field | Meaning |
|---|---|
compliancePct | met / (met + missed) · rounded to one decimal; 100 when nothing was scored |
avgResponseMin | MTTA: avg responseAt − createdAt (calendar minutes); null without data |
avgResolutionMin | MTTR: avg resolvedAt − createdAt minus paused time; null without data |
excluded / cancelled | informational counters that do not count towards the rates |
scope | global = all data (viewAll) · own = only the tickets visible to the agent. Whoever displays the numbers should evaluate this field — a 92% rate over your own tickets means something entirely different from 92% over all of them. |
The required permission depends on entityType: INCIDENT requires incidents.viewAll, PROBLEM problems.viewAll, everything else (including the overall report without entityType) tickets.viewAll. Without viewAll the agent view applies, as on the dashboard: with tickets.viewOwn and an active agent profile the report returns the history of the tickets the agent may see (same selection as /trackings). This applies to tickets only; an incident or problem history always requires the respective viewAll (otherwise 403).
Admin Endpoints
Base path /api/sla/admin. All endpoints require settings.editSLA.
| Area | 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 (for escalation targets) |
Updates use PATCH. The default policy is set via the isDefault field: saving with isDefault=true automatically removes the default flag from the previous default policy of the same entityType.
Changes to a business-hours definition (schedule, time zone, holiday reference) take effect on the deadline calculation immediately. The helper lists for escalation targets (/agent-groups, /users) contain only active, non-archived accounts without system users.
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
}
| Field | Description |
|---|---|
entityType | TICKET, INCIDENT, PROBLEM |
targets | Map priority → { responseMin?, resolutionMin }. responseMin optional (e.g. problems without response SLA). |
businessHoursId | null = 24/7; otherwise business-hours-aware deadlines |
escalationPolicyId | optional escalation policy |
categoryIds | Restrict to specific categories (empty = all). Two ACTIVE policies of the same entityType must not share a category → 409 SLA_CATEGORY_OVERLAP. |
pauseOnStatus | statuses that pause the SLA (default [ON_HOLD]) |
pauseOnIncidentLink | SLA pauses when the entity is linked to an open incident (default true) |
pauseOnChildTicket | SLA of the parent ticket pauses while one of its sub-tickets is open (default true, entityType TICKET only) |
isDefault | default policy for this entityType |
Escalation Policy
levels is a JSON array. Each level has a trigger and actions:
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 | Configuration |
|---|---|
NOTIFY | notifyTargets (ASSIGNEE, GROUP_LEAD, MANAGER, CUSTOM) · customUserIds[] (required with CUSTOM) |
REASSIGN | reassignToGroupId and/or reassignToUserId (at least one; if both are set, the group is assigned first, then the user) |
ESCALATE_PRIORITY | Raise priority (prioritySteps 1–99, default 1) → SLA reset to new targets |
WEBHOOK | external webhook call (webhookUrl required) |
The escalation policy defines the recipients. Which channel (in-app/email/push/Webex/Teams) delivers an SLA notification is determined by the notification type configuration and user preferences. Recipients are additionally filtered at runtime against entity visibility.
Level validation
- level: unique and strictly ascending
PERCENTAGE: triggerValue 1–200 ·TIME_AFTER_BREACH: triggerValue ≥ 1 minuteslaType: RESPONSE or RESOLUTION- NOTIFY targeting CUSTOM requires at least one entry in customUserIds; REASSIGN requires a target; WEBHOOK requires webhookUrl
Recurring breach reminders (repeatConfig)
Each escalation level fires exactly once. So that a permanently breached SLA is not forgotten, the policy can repeat reminders after the last level:
| Field | Description |
|---|---|
enabled | Repeat active |
intervalMin | 15–10080 — business minutes since the last escalation/repeat |
maxRepeats | 1–20 — then it stops |
notifyTargets | at least one target (ASSIGNEE, GROUP_LEAD, MANAGER, CUSTOM); CUSTOM requires customUserIds |
Each repeat is recorded as a REPEAT entry in the escalation history. repeatConfig: null in a PATCH removes the repeat; omitting the field leaves it untouched.
Business Hours & Holidays
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 }
Duplicate holidays: The single POST rejects an already existing holiday with 409 SLA_HOLIDAY_DUPLICATE. The bulk import compares against the existing set beforehand and reports the number of skipped entries as skippedExisting — importing the same list twice therefore creates nothing twice.
region decides the reach: region: null means nationwide — the clock then stands still EVERYWHERE. Regional holidays (such as Corpus Christi or All Saints) therefore belong in with their region, otherwise they pause the SLA even where people are working.
Automatic holiday import
German public holidays do not need manual maintenance: a background job calculates them (including the movable ones via the Easter formula) and creates them for the current AND next year — for every country/region combination actually used by a business-hours definition. That is exactly the window the deadline calculation reads.
| Property | Value |
|---|---|
| Schedule | Yearly on 1 November, 04:00 — in time before the deadline calculation needs the following year. Additionally on worker startup (fills fresh installations and missed years immediately). |
| Idempotency | Repeated runs create no duplicates — existing entries are detected and skipped. |
| Scope | Germany (nationwide and regional holidays). Holidays of other countries are maintained via bulk import. |
| Management | Visible as the cronjob "Holiday Auto-Import" and switchable there (CronJobs) |
Example: Business-Hours Deadline
Business Hours: Mo–Fr 09:00–17:00 (8h/day) Ticket created: Freitag 14:00 · Target: 480 min (8 business hours) Fr 14:00 → 17:00 = 180 min (left 300) Sa/So = skipped Mo 09:00 → 14:00 = 300 min (left 0) → Resolution-Deadline: Monday 14:00 (if Monday is a holiday → Tuesday 14:00)
SLA Lifecycle
- Creation (automatic): On entity creation the service picks the matching policy (category-specific → else default for entityType), copies the priority targets into an SLATracking and computes deadlines (24/7 or business hours).
- Pause/Resume: A status in pauseOnStatus (default ON_HOLD) OR an open incident link (pauseOnIncidentLink) OR an open sub-ticket of the parent ticket (pauseOnChildTicket) pauses the clock; on resume the deadline shifts by the paused business time (pausedTotalSec, pauseHistory) — it resumes only once no reason applies any more. Every pause, resume and cancellation additionally writes a visible entry into the affected entity's timeline — an ON_HOLD switch therefore deliberately produces two entries: the status change and the SLA pause.
- Priority change → reset: New targets from the policy, deadlines recomputed from now, escalation level reset.
- Response Met: For tickets, the first public agent reply counts — via web, email reply, or an outbound email that creates the ticket. A mere assignment (to agent, group, mailbox or queue) does NOT fulfil the response SLA. For incidents and problems ITIL semantics apply: assignment/acknowledgement counts as the reaction.
- Resolution Met: Closing/resolving sets resolutionMet: true if the resolution deadline was met, otherwise false (breachAt is set).
- SLA-Monitor: A background job in the job-worker periodically computes status (calculatedStatus) and fires due escalation levels.
Notifications
SLA events produce notifications via the central notification system: SLA_WARNING, SLA_RESPONSE_WARNING, SLA_BREACH, SLA_RESPONSE_BREACH, SLA_CRITICAL, SLA_ESCALATION, SLA_MET.
Channels & recipients are controlled by the notification configuration.
Absences & Substitution
Absences (vacation/sick/…) and substitution are a separate domain (/api/absences) affecting assignment/substitution, not the SLA clock directly. Absence types: VACATION, SICK, TRAINING, BUSINESS_TRIP, PARENTAL, COMPENSATORY, OTHER. Absences API
- ✓ Targets per priority, policy per entityType
- ✓ Business-hours-accurate deadlines & status
- ✓ Pause on ON_HOLD / incident link / sub-ticket
- ✓ Multi-level escalation + SLA monitor
tickets.viewAll‖tickets.viewOwn– SLA dashboard (trackings/stats) and history (report); without viewAll only visible tickets and only with an agent profileincidents.viewAll/problems.viewAll– history per entityTypesettings.editSLA– policies/escalation/business-hours/holidays + thresholds (sla-settings). Critical permission: revocation takes effect immediately, denied attempts are logged.
Auth/role model: Permissions & RBAC
- SLA System (architecture)
- Tickets API – SLA column, sorting (sort=sla) and filter (f.slaStatus) of the ticket list · Incidents API · Problems API
- Notification System – SLA_*