Agent KPIs
Agent KPIs are computed by the per-account PresenceEngine directly from its agent-state event log (agent_event). Because every state transition is already recorded durably, the metrics are a pure function of that log over a time window, so there’s no separate metrics pipeline to keep in sync.
All KPIs take a since timestamp (epoch ms) and report over the window [since, now].
State-time
Section titled “State-time”For each agent, the brain walks agent_event in order and sums the time spent in each bucket. The duration of a transition into bucket S at ts[i] runs until the next transition ts[i+1], or until now for the most recent one:
statusMs = { ready: …, incall: …, wrapup: …, paused: …, idle: …, … } // ms per bucketloggedInMs = Σ statusMs excluding `offline`statusMs is the raw building block; loggedInMs is total time logged in (everything except offline).
Occupancy
Section titled “Occupancy”Occupancy answers “while available, how much of the time was the agent actually working a call?” It’s a measure of how hard staffed agents are being worked.
handle = incall + wrapupoccupancyPct = round( 100 × handle / (handle + ready) ) // 0 if denominator is 0The denominator is handle + ready: productive time plus time sitting idle-but-available. Time paused, offline, or unreachable is excluded entirely, so occupancy isolates “of the time you were on the floor ready to work, what fraction was spent on calls.”
Utilization
Section titled “Utilization”Utilization answers “of the agent’s whole logged-in shift, how much was spent handling calls?” It’s handle time against everything except being logged out.
utilizationPct = round( 100 × handle / loggedInMs ) // loggedInMs excludes offlineBecause the denominator includes paused and idle time, utilization ≤ occupancy for the same agent over the same window. Occupancy measures intensity while ready; utilization measures share of the whole shift.
Reading the report
Section titled “Reading the report”Agent KPIs are read from the public API at GET /v1/agents (the agents scope):
curl 'https://api.sip.io/v1/agents?since=1719500000000' -H 'x-api-key: sk_…'{ "agents": [ { "user_id": "1001", "statusMs": { "ready": 5400000, "incall": 2700000, "wrapup": 300000, "paused": 600000 }, "loggedInMs": 9000000, "occupancyPct": 35, "utilizationPct": 33, "adherencePct": 92 } ]}adherencePct is included when the agent has a work schedule; see that page for its meaning and the null / "bypass" cases.
Historical rollups: GET /v1/reports/agents
Section titled “Historical rollups: GET /v1/reports/agents”The KPIs above are computed live, directly from PresenceEngine’s event log, over whatever window you ask for. A separate per-account MetricsDO drains PresenceEngine’s agent_event table on a 5-minute cron and folds it into its own durable rollup tables, at 15-minute (aq15, as15) and daily (aq1d, as1d) grain. This is additive: it doesn’t replace the live endpoint above, it gives you pre-aggregated history beyond the live window without re-walking the raw event log.
Rollup rows store numerator/denominator pairs rather than pre-divided averages, and the division happens once at read time. This avoids a mean-of-means error you’d otherwise get by averaging pre-computed percentages across buckets.
Read the rollups from the public API at GET /v1/reports/agents (the reports scope):
curl 'https://api.sip.io/v1/reports/agents?since=1719500000000&grain=15m' -H 'x-api-key: sk_…'Occupancy and utilization — a different formula
Section titled “Occupancy and utilization — a different formula”GET /v1/reports/agents computes occupancy and utilization differently from the live GET /v1/agents endpoint above. Both are legitimate; they answer slightly different questions, so don’t expect the two numbers to match:
| Formula | Endpoint | |
|---|---|---|
| Live occupancy | handle / (handle + ready) | GET /v1/agents |
| Rollup occupancy | handle / (login − paused) | GET /v1/reports/agents |
| Rollup utilization | handle / login | GET /v1/reports/agents |
Where login = handle + ready + paused + idle + ring + extcall (total on-clock time). The rollup occupancy formula divides handle time by on-clock time excluding pauses, rather than by handle+ready — so it isolates “of the time the agent was actually staffed and not on a break, how much was spent on calls.” Rollup utilization is close to, but not necessarily identical to, the live utilization figure above, since it’s computed from the rollup’s login definition rather than loggedInMs.
Teams: agent-level rollups by team × queue
Section titled “Teams: agent-level rollups by team × queue”Agents can be grouped into teams for reporting purposes. A team is a named set of agents; rollups can then be sliced by team × queue in addition to plain agent × queue.
Manage teams via GET/POST /v1/agent-teams and GET/POST /v1/agent-teams/{id} (the reports scope):
curl -X POST 'https://api.sip.io/v1/agent-teams' -H 'x-api-key: sk_…' \ -d '{ "name": "Tier 2 Support", "agent_ids": ["1001", "1002"] }'Team-rolled-up reports are read from GET /v1/reports/teams; see Queue Metrics & CDR for the queue-side shape of that report.
Pause reasons: GET /v1/reports/pauses
Section titled “Pause reasons: GET /v1/reports/pauses”When an agent pauses, they can supply a free-text reason string (not a fixed enum). If omitted, it’s bucketed under "unspecified". MetricsDO tracks pause time per reason at 15-minute and daily grain (ap15, ap1d), and GET /v1/reports/pauses returns the breakdown:
{ "pauses": [ { "reason": "lunch", "count": 14, "totalMs": 25200000 }, { "reason": "training", "count": 3, "totalMs": 5400000 }, { "reason": "unspecified", "count": 2, "totalMs": 900000 } ]}This is the finer per-segment breakdown of paused time that was previously only a single bucket in state-time above.
Roadmap
Section titled “Roadmap”- Raw-event export: periodic flush of
agent_eventto the data lake for ad-hoc and historical analysis beyond the live window (superseded for KPI purposes by theMetricsDOrollups above, though the raw log itself isn’t yet separately exported).