Eviworx
Docs

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.

⏱️
Features
✓ For tickets, incidents and problems
✓ Response and resolution target per priority
✓ Business hours with time zone and holidays
✓ Status with configurable thresholds (sla-settings)
✓ Pause on ON_HOLD, incident link, sub-ticket
✓ Multi-level escalation (3 trigger types)
✓ Recurring reminders (repeatConfig)
✓ Continuous SLA monitor (background job)
✓ History report (compliance, MTTA/MTTR)
✓ Notifications (SLA_WARNING, SLA_BREACH …)

Status Model

Usage percentage (percentUsed) is computed business-hours-accurately from "remaining business time to deadline". When paused, status freezes at pausedAt.

StatusCondition
OKpercentUsed < 80%
WARNINGpercentUsed ≥ 80%
BREACHpercentUsed ≥ 100% (deadline exceeded)
CRITICAL> 60 minutes after breach
CANCELLEDTracking 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
}
PropertyBehavior
EffectApplies to the server-side status calculation (dashboard, ticket sidebar, monitor/escalation) — the status always comes from the backend.
LatencyA change via the settings API takes effect immediately.
RobustnessIf 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.

MethodEndpointPermissionDescription
GET/api/sla/trackingstickets.viewAlltickets.viewOwnActive trackings (filtered, with metrics)
GET/api/sla/statstickets.viewAlltickets.viewOwnAggregated counts per status
GET/api/sla/reporttickets.viewAll (or incidents/problems.viewAll per entityType)Historical compliance rates (met/missed, MTTA/MTTR)
GET/api/sla/holidaysany authHoliday 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

ParameterValues
entityTypeTICKET, INCIDENT, PROBLEM
statusOK, WARNING, BREACH, CRITICAL, CANCELLED, PAUSED
searchEntity number or title (max 200 chars, case-insensitive) — server-side, before pagination
limit1–100 (default 25)
offsetPagination 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.

ParameterValues
days1–365 (default 30, rolling from now)
from / toISO-8601; from must precede to (otherwise 400 VALIDATION_ERROR)
entityTypeTICKET, 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"
}
FieldMeaning
compliancePctmet / (met + missed) · rounded to one decimal; 100 when nothing was scored
avgResponseMinMTTA: avg responseAt − createdAt (calendar minutes); null without data
avgResolutionMinMTTR: avg resolvedAt − createdAt minus paused time; null without data
excluded / cancelledinformational counters that do not count towards the rates
scopeglobal = 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.

AreaEndpoints
SLA PoliciesGET /policies, GET /policies/:id, POST /policies, PATCH /policies/:id, DELETE /policies/:id
Escalation PoliciesGET /escalation-policies, GET /:id, POST, PATCH /:id, DELETE /:id
Business HoursGET /business-hours, GET /:id, POST, PATCH /:id, DELETE /:id
HolidaysGET /holidays, GET /:id, POST, POST /bulk, PATCH /:id, DELETE /:id, DELETE /year/:year
HelpersGET /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
}
FieldDescription
entityTypeTICKET, INCIDENT, PROBLEM
targetsMap priority → { responseMin?, resolutionMin }. responseMin optional (e.g. problems without response SLA).
businessHoursIdnull = 24/7; otherwise business-hours-aware deadlines
escalationPolicyIdoptional escalation policy
categoryIdsRestrict to specific categories (empty = all). Two ACTIVE policies of the same entityType must not share a category → 409 SLA_CATEGORY_OVERLAP.
pauseOnStatusstatuses that pause the SLA (default [ON_HOLD])
pauseOnIncidentLinkSLA pauses when the entity is linked to an open incident (default true)
pauseOnChildTicketSLA of the parent ticket pauses while one of its sub-tickets is open (default true, entityType TICKET only)
isDefaultdefault 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 typeConfiguration
NOTIFYnotifyTargets (ASSIGNEE, GROUP_LEAD, MANAGER, CUSTOM) · customUserIds[] (required with CUSTOM)
REASSIGNreassignToGroupId and/or reassignToUserId (at least one; if both are set, the group is assigned first, then the user)
ESCALATE_PRIORITYRaise priority (prioritySteps 1–99, default 1) → SLA reset to new targets
WEBHOOKexternal 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 minute
  • slaType: 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:

FieldDescription
enabledRepeat active
intervalMin15–10080 — business minutes since the last escalation/repeat
maxRepeats1–20 — then it stops
notifyTargetsat 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.

PropertyValue
ScheduleYearly 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).
IdempotencyRepeated runs create no duplicates — existing entries are detected and skipped.
ScopeGermany (nationwide and regional holidays). Holidays of other countries are maintained via bulk import.
ManagementVisible 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

⏱️
Core Principles
  • ✓ Targets per priority, policy per entityType
  • ✓ Business-hours-accurate deadlines & status
  • ✓ Pause on ON_HOLD / incident link / sub-ticket
  • ✓ Multi-level escalation + SLA monitor
🔐
Permissions (RBAC)
  • tickets.viewAlltickets.viewOwn – SLA dashboard (trackings/stats) and history (report); without viewAll only visible tickets and only with an agent profile
  • incidents.viewAll / problems.viewAll – history per entityType
  • settings.editSLA – policies/escalation/business-hours/holidays + thresholds (sla-settings). Critical permission: revocation takes effect immediately, denied attempts are logged.

Auth/role model: Permissions & RBAC

Related Documentation