> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Omnichannel queue routing for digital channels

> Route inbound chat, email, and social conversations through skill-based digital queues with attribute and affinity rules, preview the SLA impact on-queue, and verify every decision with queue analytics.

Omnichannel queue routing decides which agent (or overflow bucket) an inbound **digital** conversation lands with — chat, email, WhatsApp, SMS, RCS, social, and the APAC OTT channels — using the same skill-queue machinery your voice queues already use. Voice covers the PSTN/call leg; digital covers the inbox. This guide walks the operator loop for digital channels: model queues, attach attribute and affinity policies, preview SLA behavior before a conversation arrives, and verify the decision in analytics.

Inbound routing decides *where* a conversation enters; digital queue routing decides *who answers it* once it is in the inbox. Outbound messaging and voice termination are unaffected — none of these controls change how outbound calls or messages are placed; they govern inbound queueing and assignment only. Outbound MT voice and SMS continue to exit exclusively through the Devotel wholesale softswitch — these routes are tenant-owned inbound routing config, never a provider-side or carrier-side signalling change.

Every control here is **tenant-owned**: queue definitions, attribute rules, affinity weights, SLA targets, and overflow actions live in your organization's settings and take effect on the next inbound item. Suppression and quiet-hours are honored the same way they are for messaging; routing does not bypass DNC or consent gates.

**Base paths:** `/api/v1/inbox` and `/api/v1/voice`

**Authentication:** Clerk session (`Authorization: Bearer <token>`) or API key (`X-API-Key`).

**Scope:** write operations require an owner or admin role (`voice:write` scope on the API-key route, plus the `inbox` surface for digital queues); reads require a signed-in session or API key. Supervisors reach the queue-monitoring surfaces.

***

## 1. Concepts — digital queues vs voice queues

A **voice queue** (`/api/v1/voice/queues`) is an ACD target for PSTN and in-app voice: callers wait in FIFO, the platform dispatches to an eligible available agent, and overflow plays out as voicemail / forward / disconnect. A **digital queue** (`/api/v1/inbox/digital-queues`) routes inbox conversations through the same skill-match / SLA / overflow machinery but the work item is a conversation thread on a digital channel, not a call leg.

The two models differ in the places a CCaaS operator cares about:

|                  | Voice queue                                | Digital queue                                                                                                                                     |
| ---------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Item             | a call leg (`acd_call_queue_entries`)      | a conversation (per-queue Redis waiting set)                                                                                                      |
| Channel          | `voice` (and video)                        | `email`, `web_chat`, `whatsapp`, `sms`, `rcs`, `viber`, `instagram`, `messenger`, `line`, `telegram`, `apple_messages`, `kakao`, `zalo`, `wechat` |
| Overflow actions | voicemail, forward, disconnect, callback   | `routing_rules` (fall back to the org inbox routing-rules engine), `overflow_queue` (chain into another digital queue), `auto_responder`, `hold`  |
| Storage          | one row per queue in the tenant ACD tables | JSONB array in `organizations.settings.digital_acd_queues`                                                                                        |
| SLA timers       | `max_wait_seconds` per queue               | `sla_seconds` per queue; the SLA sweep re-evaluates waiting items past their deadline                                                             |

Voice and digital queues converge at the **monitoring surface**: the omnichannel queue view (`GET /api/v1/voice/supervisor/omnichannel-queue`) blends both backlogs into ONE position-numbered, wait/SLA/priority-ordered list — the same ordering the routing engine assigns by — so a supervisor sees the shared queue, not two silos.

## 2. Attribute routing and affinity rules

Before a work item ever enters a queue, two optional server-side policies can reshape it. Both are opt-in (each disabled by default) and both operate on the inbound conversation's contact attributes and the agent roster — never on outbound signalling.

### Attribute routing

`GET` / `PUT /api/v1/voice/attribute-routing` reads and replaces your attribute-routing policy: an ordered list of rules that append extra `requiredSkills` and an optional `priorityBoost` to a queue entry based on the contact's tags and CDP attributes. Storage is a single JSONB key (`organizations.settings.acd_attribute_routing`), so a change takes effect on the next enqueue with no migration.

A rule has the shape:

```json theme={null}
{
  "kind": "tag",
  "value": "vip",
  "extraSkills": ["vip-handler"],
  "priorityBoost": 5,
  "label": "VIP tier"
}
```

Rule kinds (validated by the server, hard-capped at 50 rules per tenant, 16 extra skills per rule):

| `kind`             | Required field(s)  | Effect                                | Example                                    |
| ------------------ | ------------------ | ------------------------------------- | ------------------------------------------ |
| `tag`              | `value`            | Contact carries the tag               | `value: "vip"`                             |
| `attribute_equals` | `key`, `value`     | Contact attribute equals the value    | `key: "language"`, `value: "es"`           |
| `attribute_gte`    | `key`, `threshold` | Numeric attribute meets the threshold | `key: "account_value"`, `threshold: 50000` |

`extraSkills` entries are validated against your tenant's active skill catalog — an unknown skill slug returns `422 UNKNOWN_SKILL` instead of being persisted. `priorityBoost` is an integer from 0 to 99 (rejected out of range), and `label` is a free-text operator name shown in the settings panel.

Replace the policy from the dashboard at **Settings → Voice → Attribute routing** (no JSON editing), or over the API with a full-document PUT:

```bash cURL theme={null}
curl -X PUT "https://api.orbit.devotel.io/api/v1/voice/attribute-routing" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "rules": [
      { "kind": "tag", "value": "vip", "extraSkills": ["vip-handler"], "priorityBoost": 5, "label": "VIP tier" },
      { "kind": "attribute_equals", "key": "language", "value": "es", "extraSkills": ["spanish"] },
      { "kind": "attribute_gte", "key": "account_value", "threshold": 50000, "extraSkills": ["enterprise"], "priorityBoost": 3 }
    ]
  }'
```

The write is a surgical JSONB update, so a concurrent edit to a different settings key can never be clobbered, and the change is written to your audit log.

### Affinity routing

`GET` / `PUT /api/v1/voice/affinity-routing` reads and replaces your **behavioral affinity** config: the pairing objective that routes an inbound contact to the agent *best-matched* to them (customer-segment ↔ agent-affinity fit) rather than the globally best-scoring agent. Storage is `organizations.settings.acd_affinity_routing`; both writes are audited.

```json theme={null}
{
  "enabled": true,
  "treatment_percent": 100,
  "affinity_weight": 0.75,
  "headroom_weight": 0.25,
  "agent_profiles": {
    "user_2bFzq9W": ["segment:enterprise", "style:analytical"]
  }
}
```

* **`enabled`** — opt-in; defaults to false. A malformed or disabled config resolves to the safe disabled default in the router.
* **`treatment_percent`** — share of eligible queue items (0–100) routed on the affinity arm. Ramp the rollout instead of flipping globally.
* **`affinity_weight` / `headroom_weight`** — server-clamped to 0–1; the pair balances contact↔agent fit against agent headroom.
* **`agent_profiles`** — map of Clerk user id to affinity tags the agent pairs well with, e.g. `segment:enterprise`, `style:analytical`.

Affinity tuning is supervisor/admin-managed from the same Settings → Voice surface, layered under the queue's own skills. It never decides a *destination* (queue or overflow); it decides **which eligible agent** an already-created work item pairs with, on the same live candidate snapshot the voice ACD uses, so voice and digital share one candidate pool.

## 3. Routing-rules evaluator

Once a conversation arrives in the inbox, the **routing-rules engine** (`/api/v1/inbox/routing-rules`) assigns it. Rules are evaluated in priority order (lower `priority` first, firewall-style first-match-wins) and AND-combine their conditions.

### Conditions

| Condition                                   | Shape                                  | What it matches                    |
| ------------------------------------------- | -------------------------------------- | ---------------------------------- |
| Channel                                     | one channel from the digital enum      | `whatsapp`, `email`, `web_chat`, … |
| Skill                                       | a skill slug in your tenant catalog    | `spanish`, `billing`               |
| Language                                    | ISO 639-1 from the built-in detector   | `es`, `de`                         |
| Sentiment                                   | `positive` / `neutral` / `negative`    | inferred live signal               |
| Contact attribute                           | equals / greater-than / contains       | any CDP attribute                  |
| Business hours                              | in-window / out-of-window (+ holidays) | your schedule                      |
| Queue depth / wait / AHT / agents available | numeric                                | live queue telemetry               |

### Actions

| Action               | Effect                                                                                                          |
| -------------------- | --------------------------------------------------------------------------------------------------------------- |
| `assign_user`        | assign to one specific user                                                                                     |
| `assign_team`        | assign to a team                                                                                                |
| `assign_round_robin` | balance across a pool                                                                                           |
| `assign_skill_based` | pick the org user whose skills best match the rule's `required_skills`; falls back to round-robin on no overlap |
| `set_tag`            | tag the conversation                                                                                            |
| `set_priority`       | raise/lower queue priority (`urgent` / `high` / `normal` / `low`)                                               |
| `trigger_agent`      | hand to an AI agent                                                                                             |

Create a rule dynamically over the API:

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/inbox/routing-rules" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "WhatsApp Spanish → LATAM pod",
    "priority": 10,
    "conditions": [
      { "kind": "channel", "value": "whatsapp" },
      { "kind": "language", "value": "es" }
    ],
    "actions": [
      { "kind": "assign_skill_based", "required_skills": ["spanish"] },
      { "kind": "set_tag", "value": "latam" }
    ]
  }'
```

A digital queue's `overflow_action: "routing_rules"` falls back to this same engine — so a queue that cannot place an item never silently hijacks the conversation; it defers to the universal escape hatch.

## 4. On-queue SLA forecast preview

After an item joins a queue (digital or voice), the **SLA forecast** estimates breach-before-answer and suggests the callback/overflow capacity that absorbs the backlog *before* the SLA lands. This is advisory — no dispatch fires from the preview.

`POST /api/v1/voice/queues/:id/sla-forecast` takes a live queue snapshot and returns the forecast. The response tells you the expected head-of-line wait, the projected breach over the queue's SLA threshold, and how much callback capacity would absorb the backlog pre-breach — so a supervisor sees "offer 3 callback slots" *before* the wallboard turns red, not after.

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/queues/queue_abc123/sla-forecast" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "offeredCalls": 42,
    "answeredWithinSla": 27,
    "shortAbandons": 2,
    "currentDepth": 9,
    "oldestQueuedSeconds": 84,
    "agentsAvailable": 3,
    "avgHandleSeconds": 305,
    "windowMinutes": 15
  }'
```

The server resolves the queue's SLA target from your persisted policy (body fields override when present); the forecast itself is a pure, dependency-free module that the wallboard tick reuses, so the preview and the live supervisor surface cannot drift. The SLA-forecast paired with the [queue SLA escalation policies](/voice/queue-sla-escalation-policies) covers the full pre-breach → post-breach loop.

## 5. Verification — queue analytics stream

Every routing decision lands in observable queues. Three read-only surfaces let you verify end to end:

* **`GET /api/v1/voice/queues/:id/analytics`** — aggregated per-queue metrics (offered/answered, service-level rate, ASA, AHT, abandon rate) against your SLA target. Use this to confirm a rule change actually shifted the numbers you intended.
* **`GET /api/v1/voice/supervisor/omnichannel-queue`** — the blended voice + digital backlog as one position-numbered, wait/SLA/priority-ordered list. Confirm the item that should sit at position N actually does, and that a `priorityBoost` rule bumps a VIP chat above older normal-priority items.
* **Queue live SSE stream (`GET /api/v1/voice/queues/:id/live`)** — Server-Sent Events pushing real-time queue state every 5 s (keepalive every 15 s, sessions capped at 30 min, hard-capped to 100 concurrent connections per tenant). Subscribe while you toggle a rule and watch the queue respond — the stream is tenant-scoped and CORS-locked to trusted origins.

```bash cURL theme={null}
curl -N "https://api.orbit.devotel.io/api/v1/voice/queues/queue_abc123/live" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Accept: text/event-stream"
```

Combine the three for the operator loop: change an attribute or affinity rule, watch the live stream pick the item up in the blended queue, then read the analytics endpoint to confirm the SLA/ASA shift you expected. For digital channels, also check the per-queue live metrics (`GET /api/v1/inbox/digital-queues/:id`) — one queue's `waiting` / `assigned` / `sla_breached` counters, mirroring the voice surface.

Next steps: [Set up and run voice queues](/guides/voice-queues) for the voice-side counterpart, [Inbox setup](/guides/inbox-setup) to wire the inbox's digital channels, [Queue SLA escalation policies](/voice/queue-sla-escalation-policies), and [Supervisor live monitoring — digital](/guides/supervisor-live-monitoring-digital) for the blended supervisor surface.
