Unified inbox SLA engine: first-response and resolution timers with breach alerts
An SLA policy answers two questions continuously: how long has this customer been waiting, and who should hear about it the moment the wait crosses the line you set. A policy hangs three clocks on every conversation — first response, next response, and resolution — sweeps them every two minutes, and fires the breach action you configured exactly once per clock. The dashboard badge shows the clock turning overdue to the operator working the queue; the breach action is what carries the same event to the place your team actually watches — a Slack channel, a Teams connector, your own webhook endpoint, an escalation to a supervisor. Everything here is per-workspace configuration. Creating, editing, and test-sending a policy or its breach action requires an owner or admin role; reading the clock state on a conversation is open to any operator.1. Attach a policy
A policy names the targets and the breach action. This one gives the WhatsApp channel a 15-minute first-response target, an 8-hour resolution target, and posts every breach to a Slack Incoming Webhook:scope takes org_wide, per_channel, or per_segment, and one policy can be flagged is_default as the fallback when nothing more specific matches. With business_hours_only: true the clocks pause outside your workspace’s posted hours — a 15-minute target set Friday afternoon is not breached before Monday morning.
Until you create a policy the workspace still reports against synthetic defaults — 4h first response, 24h resolution, business hours only — so the dashboard SLA badge works from day one, and your first policy replaces the defaults rather than switching the feature on.
2. The three clocks
Each conversation carries up to three timers, and each breaches independently:- First response starts on the customer’s first inbound message and stops when an operator sends the first outbound reply. An agent-initiated thread never false-breaches: if the customer has not written in yet, there is no clock to breach.
- Next response restarts whenever the customer replies again after that first answer, so follow-up waits are measured with the same rigor as the first.
- Resolution starts when the conversation opens and stops when it closes. It pauses while the thread sits in awaiting-customer, so a slow customer does not burn the agent’s resolution budget.
GET /inbox/sla/conversations/{conversationId} — due-at, target minutes, and breach flags — the same state the dashboard SLA badge renders.
Read many conversations at once
The per-conversation read works for a single thread in focus. Once you build anything that watches the whole queue — the dashboard’s visible page, an alerting proxy rolling SLA state into your own ops surface — polling one conversation at a time falls apart.POST /api/v1/inbox/sla/timers evaluates the same clocks for up to 50 conversations in one request:
channel_filter, and you can pass an optional now_ms to evaluate every clock against one fixed instant — handy when you need the batch to agree on what “now” is. The response carries one results entry per requested conversation, in input order, with each state mirroring the per-conversation payload exactly: due-at timestamps, resolved target minutes, breach flags with their first-breach timestamps, and the clock-active predicates — plus the org’s timezone and sla_enabled resolved once for the whole batch:
status "not_found" or "error" and state: null, while every other row in the batch still evaluates normally. Even a whole-batch failure — the database dropping the query mid-read — returns 200 with every row marked status:"error", never a 500. That is deliberate: an alert surface that goes blank on a transient blip hides breaches at exactly the wrong moment, so the contract guarantees you always get per-row data you can render, with degraded rows you can label “SLA unavailable” instead of losing the page. Handle all three statuses; never assume 200 means every row resolved.
3. Pick a breach action
breach_action_type chooses what fires when a timer crosses its target. The breach is detected by the recurring sweep and dispatched exactly once per clock, so a retried tick never double-posts:
Two things are true for every webhook-shaped destination. First, only
https: URLs are accepted, and the destination is re-validated at send time — URLs that resolve to internal or private network ranges are refused, so a breach alert can never be pointed at your own infrastructure. Second, the generic webhook action signs its body with the policy’s secret (HMAC-SHA256, sent as t=<unix>,v1=<hex> on X-Devotel-Signature); verify it exactly like any other Orbit webhook delivery. Slack and Teams Incoming Webhook URLs have no operator-held secret, so those posts go unsigned by design.
slack_webhook and teams_webhook cover the common case where you’d rather paste one Incoming Webhook URL from the chat tool’s admin UI than install the full OAuth app — the trade-off is that those posts are one-directional (no acknowledge or snooze buttons round-tripping back into Orbit).
Every destination in that table has the same shape: it announces the breach somewhere your team watches, and it cannot hear back. Acknowledgement is the counterpart on the Orbit side, covered next.
4. Acknowledge a breach after triage
When a breach fires, the operator who picks it up should ack it — the same gesture as acking a page in your on-call tool, recorded where the SLA state already lives.POST /api/v1/inbox/sla/breaches/ack stamps one or more fired alerts in a single request:
(conversation, kind) pair — kind is first_response, next_response, or resolution — with up to 50 per request and an optional note (≤280 characters) that lands in the audit trail. The stamp stores who acked and when, and it is what lets an alert surface distinguish “breached, nobody has seen this yet” from “breached, in hand.” The endpoint is idempotent: re-acking the same clock simply replaces the timestamp and operator, so a retried client call never creates a duplicate record. The response reports each pair with ok: true on a stamped ack, or ok: false with an error — a conversation id you do not have fails its pair without affecting the rest of the batch.
Two interactions to get right. First, against the fire-once semantics of the breach action: it already fired exactly once, and acking does not rewind that — the clock keeps its first-breach timestamp, the chat post already went out, and the due-at does not move. Ack quiets the alert (the row is no longer un-triaged); it does not un-breach the conversation. Second, against the destinations table: chat posts are one-directional announcements out of Orbit, while ack flows entirely inside it — stamped on the conversation, shown to other operators, and written to the audit log. If your proxy builds its own alert surface on top of the bulk read above, acking through the API keeps Orbit’s own badge and audit trail consistent with whatever state you maintain externally.
5. Build an escalation ladder with tiers
One breach action answers the moment a target is crossed. Most teams want a sequence: warn early, alert at the line, escalate hard when it’s blown.breach_action_tiers is that sequence — up to five steps, each with its own action, fired in ascending at_pct order as the clock crosses each percentage of its target:
at_pct ranges from 1 to 500 and each value must be unique within the policy. Percentages below 100 fire before the breach line; above 100 they fire after it. Every tier fires at most once. Tiers run the directly-fireable actions — notify, reassign, Slack, and the webhook destinations — with the same payload shapes as a single-action policy; sentiment-gated escalation (next section) stays a policy-level action rather than a ladder step, and the API refuses to save one as a tier.
6. Gate escalations on sentiment
A neutral conversation crossing its resolution target is overdue; a conversation the sentiment analysis already scores as clearly unhappy is overdue and on fire.sentiment_escalate exists for the second case: when a breach fires, the workspace’s per-message sentiment analysis is consulted, and only if the latest score sits at or below your threshold (between -0.9 and 0.0, defaulting to -0.3) does the escalation run — reassigning the thread or notifying a supervisor. Breaches on neutral or positive threads stop at the in-queue badge, which is usually the signal your team already has. When no sentiment is available yet for a conversation, the gate stays closed: a scoring gap never turns into an alert flood.
7. Test the destination before you rely on it
Before a policy goes live, fire the action against a synthetic test conversation — same payload shape, same signing, same destination validation as the real path, nothing written to any queue:See also
- Omnichannel Inbox setup — channels, routing, macros, and the rest of the queue around the SLA engine
- On-call alerting — page a rotation when breach actions fire
- Inbox API reference — full request/response schemas for every endpoint above
- Webhook delivery semantics — verifying Orbit signatures on the generic
webhookaction