RWI Events Developer Reference

August 6, 2026 · View on GitHub

Source code: src/rwi/proto.rs | Protocol version: 1.0


1. Overview

RustPBX streams real-time call, IVR, recording, queue, agent, and extension events through the RWI (Real-time WebSocket Interface). Developers can receive events via two channels:

ChannelProtocolUse Case
WebSocket subscriptionws(s)://<host>/rwi/v1Real-time bidirectional interaction (bots, softphones, dashboards)
Webhook callbackHTTP POSTAsync notifications (CRM, recording systems, analytics)

Dispatch Methods

MethodRecipientMeaning
call_ownerWS session owning the call_idPer-call fine-grained events
fan_outAll WS sessions subscribed to the contextIncoming call notifications, IVR events
broadcastAll online WS sessionsGlobal events (agent state, DN registration, etc.)
webhookConfigured HTTP endpointAll events forwarded (filterable)

2. Connection & Authentication

WebSocket

GET /rwi/v1 HTTP/1.1
Upgrade: websocket
Authorization: Bearer <token>

Or via query parameter: GET /rwi/v1?token=<token>

Webhook Configuration (rustpbx.toml)

[rwi_webhook]
url = "https://myapp.example.com/rwi-events"
timeout_ms = 5000
headers = { Authorization = "Bearer your-token" }
# empty = all events (recommended). To allow-list, use valid event types.
# Note: agent status is "agent_state_changed" (the old "dn_state_changed" was
# removed); recording data (download URL, file size) is delivered via
# "recording_metadata_available" and "record_end" — "record_stopped" alone
# carries no recording URL.
# Example allow-list:
# events = ["call_hangup", "record_stopped", "recording_metadata_available", "record_end", "agent_state_changed"]
events = []
FieldTypeDefaultDescription
urlString(required)HTTP endpoint receiving POST requests
timeout_msu645000HTTP request timeout in milliseconds
headersHashMap(optional)Custom HTTP headers sent with every request
eventsVec<String>[] (all)Event type whitelist; empty forwards all events

3. Envelope Format

WebSocket Event

{
  /* Event fields are flattened directly at the top level, no extra wrapping */
}

Example:

{
  "call_id": "call-abc123",
  "caller_name": "330909",
  "callee_name": "9242000001",
  "direction": "inbound"
}

WebSocket events are sent with the event fields directly as top-level JSON keys, without a "rwi" or event-type-name wrapper. Clients identify events through the subscription rules negotiated at connection time.

Webhook Envelope

{
  "rwi": "1.0",
  "sequence": 42,
  "timestamp": 1716212345,
  "call_id": "call-abc123",
  "event_type": "call_ringing",
  "event": {
    /* identical to WS event content (no event_type wrapper) */
  }
}

| Field | Type | Description |
|-------|------|-------------|
| `rwi` | string | Protocol version `"1.0"` |
| `sequence` | u64 | Monotonically increasing event sequence number (for dedup and resume) |
| `timestamp` | u64 | Unix epoch seconds |
| `call_id` | string | Call identifier (empty string for broadcast-only events) |
| `event_type` | string | snake_case event type name |
| `event` | object | Event payload with fields flattened directly (no event_type wrapper) |

---

## 4. Flat Call Context (EventCallContext)

All call-scoped events use `#[serde(flatten)]` to embed the following fields **directly into the event JSON** (no nested object). `None` values are automatically omitted.

| Field | Type | Description |
|-------|------|-------------|
| `caller` | Option\<String\> | Caller SIP URI |
| `callee` | Option\<String\> | Callee SIP URI |
| `caller_name` | Option\<String\> | Calling party number (normalized digits) |
| `callee_name` | Option\<String\> | Dialed number / DNIS |
| `direction` | Option\<String\> | `inbound` / `outbound` / `internal` |
| `trunk` | Option\<String\> | SIP trunk name |
| `app_id` | Option\<String\> | IVR application ID |
| `routing_target` | Option\<String\> | Current routing target |
| `root` | Option\<Object\> | Root call identity (see below) |

**Root call (`root`)** — nested object identifying the root call of this call
tree:

| Field | Type | Description |
|-------|------|-------------|
| `caller` | Option\<String\> | Root call caller SIP URI |
| `caller_name` | Option\<String\> | Root call caller name |
| `callee` | Option\<String\> | Root call callee SIP URI |
| `callee_name` | Option\<String\> | Root call callee name |
| `call_id` | Option\<String\> | Root call identifier |
| `start_time` | Option\<String\> | Root call start time (RFC3339) |

Populated with the session's own call context (`root = self`). Transferred
legs that run in a separate session keep their own context — there is no
cross-session root propagation.

**Notes**: the flat context never contains `agent_id`/`agent_name` — those are
event-specific fields (e.g. `cc_*` events, `record_stopped`) that only appear
when the event itself carries them. A `call_*` event without agent involvement
never has agent-related values.

**Notes**:
- `ani` vs `caller`: `ani` is a plain number (for business logic), `caller` is the full SIP URI
- `dnis` vs `callee`: same distinction
- Context is injected by `CallMetaStore` at gateway dispatch time — event producers never fill it manually

### Field Overlap Explanation

Some events (e.g., `RecordStopped`, `IvrNodeEntered`) carry their own `ani`/`dnis` fields. When an event's own field is `None`, `enrich()` automatically backfills from context. Webhook consumers always receive the merged result.

---

## 5. Subscription & Session Resume

### Subscribe to Contexts

```json
{
  "rwi": "1.0",
  "action_id": "sub-001",
  "action": "session.subscribe",
  "params": { "contexts": ["queue:support", "agent:*"] }
}
Context FormatDescription
queue:<queue_id>Subscribe to queue events
agent:<agent_id>Subscribe to agent events
*Wildcard — receive all broadcast events

Session Resume (Reconnection)

{
  "rwi": "1.0",
  "action_id": "resume-001",
  "action": "session.resume",
  "params": { "last_sequence": 42 }
}

Server buffers the latest 1000 events (60-second retention). After reconnection, all events after last_sequence are replayed.

Webhook Deduplication

The webhook handler deduplicates using (call_id, sequence) tuples in a 4096-entry ring buffer. Duplicate events are silently dropped.


6. Complete Event Dictionary

In the tables below, +ctx means the event carries flat context fields. ? indicates an Option<T> field — omitted from JSON when null.

6.1 Call Lifecycle

call_incoming

Dispatch: fan_out_to_context

New call enters the system. First event in any call flow.

FieldTypeDescription
call_idStringUnique call identifier
contextStringDialplan context
callerStringCaller SIP URI
calleeStringCallee SIP URI
dial_directionStringinbound / outbound / internal
trunkOption<String>SIP trunk name
sip_headersMap<String, String>Whitelisted SIP headers
root_call_idOption<String>Root call ID (constant across transfers)
caller_nameOption<String>Calling party number
callee_nameOption<String>Dialed number / DNIS
called_phoneOption<String>Actual called number (outbound scenario)
app_idOption<String>IVR application ID
routing_targetOption<String>Routing target
uuidOption<String>Global UUID (for recording linkage)
routing_pathOption<Vec<String>>Routing path sequence

Note: call_incoming uses dial_direction; other events' context uses direction.

{
  "rwi": "1.0",
  "call_incoming": {
    "call_id": "call-abc",
    "context": "inbound",
    "caller": "sip:13800138000@pbx.local",
    "callee": "sip:4000@pbx.local",
    "dial_direction": "inbound",
    "trunk": "trunk_sip",
    "sip_headers": { "X-Tenant": "corp_a" },
    "root_call_id": "call-root-42",
    "caller_name": "13800138000",
    "callee_name": "4000",
    "called_phone": null,
    "app_id": "ivr_sales",
    "routing_target": "queue:support",
    "uuid": "uuid-abc-123",
    "routing_path": ["menu:root", "queue:level1"]
  }
}

call_ringing / call_early_media / call_answered / call_unbridged / call_no_answer / call_busy

Dispatch: call_owner

FieldTypeDescription
call_idStringCall identifier
+ctxFlat context fields
{
  "rwi": "1.0",
  "call_ringing": {
    "call_id": "call-abc",
    "caller": "sip:13800138000@pbx.local",
    "callee": "sip:4000@pbx.local",
    "caller_name": "13800138000",
    "callee_name": "4000",
    "direction": "inbound"
  }
}

call_bridged

Dispatch: call_owner (both legs receive it)

FieldTypeDescription
leg_aStringA-leg call_id
leg_bStringB-leg call_id

call_hangup

Dispatch: call_owner

FieldTypeDescription
call_idStringCall identifier
reasonOption<String>Hangup reason (see table below)
hangup_byOption<String>Normalized initiator: agent | caller | system | transfer | unknown. Same vocabulary as cc_hangup.hangup_by. A callee hangup is reported as agent only when the call actually involved a CC agent (queue-routed or resolved_agent_id); otherwise it is callee.
sip_statusOption<u16>SIP response code
+ctxFlat context fields

reason values:

ValueDescription
callerCaller hung up
calleeCallee hung up
referREFER transfer hangup
systemSystem hangup
autohangupAuto hangup (timeout)
noAnswerNo answer (408/480/487)
rejectedRejected/busy (486/600/603)
canceledCanceled (487)
failedGeneric failure (other 4xx)
serverUnavailableServer unavailable (5xx)
rtpTimeoutRTP timeout
{
  "rwi": "1.0",
  "call_hangup": {
    "call_id": "call-abc",
    "reason": "caller",
    "hangup_by": "caller",
    "sip_status": null,
    "caller": "sip:13800138000@pbx.local",
    "callee": "sip:4000@pbx.local",
    "caller_name": "13800138000",
    "callee_name": "4000",
    "direction": "inbound"
  }
}

cc_hangup

Contact-center layer hangup (CC-routed calls only). Emitted alongside call_hangup; named cc_hangup for consistency with the core event. reason uses the same Display vocabulary as call_hangup.reason (e.g. caller, callee, abandoned — NOT the Debug form). hangup_by makes it explicit whether the agent, the caller, the system, or a transfer ended the call — this is critical for contact-center reporting.

Dispatch: broadcast (delivered to the configured [rwi_webhook]). Broadcast events carry the primary call's flat context (caller/callee/names/ direction) via gateway enrichment, like all call-scoped events.

cc_* events (including cc_ringing/cc_answered) are emitted only when the call actually involves a registered CC agent — a plain extension-to-extension call produces no cc_* events.

FieldTypeDescription
call_idStringCall identifier
agent_idOption<String>CC agent identifier (callee leg)
agent_nameOption<String>CC agent display name
queue_idOption<String>Queue/skill-group the call was routed through
reasonStringNormalized reason, same vocabulary as call_hangup.reason
hangup_byOption<String>agent | caller | system | transfer | unknown
duration_secsu64Talk time in seconds (0 for unanswered)
+ctxFlat context fields

cc_ringing / cc_answered / cc_held / cc_unheld also carry agent_id (canonical agent id, resolved from endpoint → primary_endpoint → agent_id) and agent_name.

{
  "rwi": "1.0",
  "event_type": "cc_hangup",
  "event": {
    "call_id": "call-abc",
    "agent_id": "1001",
    "queue_id": "support",
    "reason": "callee",
    "hangup_by": "agent",
    "duration_secs": 42
  }
}

Previously this event was named cc_ended and carried reason as the Debug form of the internal enum (e.g. "ByCallee"). Both were normalized to match call_hangup.

6.2 Transfer Events

call_transferred / call_transfer_accepted

Dispatch: call_owner

FieldTypeDescription
call_idStringCall identifier
transfer_targetOption<String>Original transfer target string (e.g. queue:queue-name?target=skillgroup:tech-support_G). None when the target is unavailable (e.g. SIP REFER Replaces takeover).
+ctxFlat context fields

call_transfer_failed

Dispatch: call_owner

FieldTypeDescription
call_idStringCall identifier
sip_statusOption<u16>SIP status code
reasonOption<String>Failure reason
transfer_targetOption<String>Original transfer target string (see above)
+ctxFlat context fields

6.3 Media Events

media_hold_started / media_hold_stopped / media_stream_started / media_stream_stopped

Dispatch: call_owner

FieldTypeDescription
call_idStringCall identifier
+ctxFlat context fields

media_ringback_passthrough_started / media_ringback_passthrough_stopped

Dispatch: call_owner

FieldTypeDescription
sourceStringSource leg call_id
targetStringTarget leg call_id

media_play_started / media_play_finished

FieldTypeDescription
call_idStringCall identifier
leg_idOption<String>Target leg
track_idStringPlayback track ID
interruptedboolmedia_play_finished only: whether interrupted by DTMF
+ctxFlat context fields

dtmf

Dispatch: fan_out_to_context

FieldTypeDescription
call_idStringCall identifier
digitStringDTMF digit (0-9, *, #)
leg_idOption<String>Leg that generated the DTMF
extraOption<Object>Extra data (extension field, defaults to null)
+ctxFlat context fields

dtmf_collected / dtmf_collection_timeout

Dispatch: call_owner

FieldTypeDescription
call_idStringCall identifier
leg_idStringLeg that provided the digits
digitsStringdtmf_collected only: collected digit string
+ctxFlat context fields

6.4 Recording Events

record_started / record_paused / record_resumed / record_failed

Dispatch: call_owner

Trigger: Via RecordStart / RecordPause / RecordResume / RecordStop RWI commands. Not automatic — recording does not start automatically when a call is answered.

FieldTypeDescription
call_idStringCall identifier
errorStringrecord_failed only: error message
+ctxFlat context fields

record_stopped (Enhanced)

Dispatch: call_owner

FieldTypeDescription
call_idStringCall identifier
duration_secsOption<u64>Recording duration in seconds
filenameOption<String>Recording filename
unique_idOption<String>Recording UUID
file_sizeOption<u64>File size in bytes
download_urlOption<String>Download URL
caller_nameOption<String>Calling party number
callee_nameOption<String>Dialed number
called_phoneOption<String>Actual called number
call_typeOption<String>inbound/outbound/internal/consult
agent_idOption<String>Agent ID
agent_nameOption<String>Agent name
call_start_timeOption<String>Call start timestamp (ISO 8601)
call_end_timeOption<String>Call end timestamp
upload_timeOption<String>Upload completion timestamp
switch_flagOption<String>Site identifier (e.g., ks, bj)
root_call_idOption<String>Root call ID

Note: record_stopped does not carry flat context, but includes its own ani/dnis fields. enrich() backfills None fields from context.

{
  "rwi": "1.0",
  "record_stopped": {
    "call_id": "call-abc",
    "duration_secs": 51,
    "filename": "uuid_2026-05-14_08-11-49.mp3",
    "unique_id": "uuid-abc-123",
    "file_size": 149517,
    "download_url": "https://storage.example.com/rec.mp3",
    "caller_name": "330909",
    "callee_name": "9242000001",
    "called_phone": "018659727661",
    "call_type": "outbound",
    "agent_id": "451447",
    "agent_name": "luoxiaofeng90_v",
    "call_start_time": "2026-05-14T08:11:35Z",
    "call_end_time": "2026-05-14T08:12:26Z",
    "upload_time": "2026-05-14T16:14:46Z",
    "switch_flag": "ks",
    "root_call_id": "call-root-42"
  }
}

recording_metadata_available

Dispatch: call_owner

Triggered when the recording file upload completes, containing full metadata.

FieldTypeDescription
call_idStringCall identifier
metadataRecordingMetadataRecording metadata (see below)

RecordingMetadata fields:

FieldTypeDescription
filenameStringRecording filename (required)
unique_idStringRecording UUID (required)
file_sizeu64File size in bytes (required)
download_urlOption<String>Download URL
caller_nameOption<String>Calling party number
callee_nameOption<String>Dialed number
called_phoneOption<String>Actual called number
call_typeStringCall type (required)
agent_idOption<String>Agent ID
agent_nameOption<String>Agent name
call_start_timeOption<String>Call start timestamp
call_end_timeOption<String>Call end timestamp
upload_timeOption<String>Upload completion timestamp
switch_flagOption<String>Site identifier
process_flagOption<String>Process identifier (e.g., ks_22_normal)
root_call_idOption<String>Root call ID

agent_id / agent_name are populated from the session extensions when the call was routed to a CC agent (agent_id is the canonical agent id resolved via endpoint → primary_endpoint → agent_id; agent_name is the agent display name). For calls without CC agent involvement they are absent.

{
  "rwi": "1.0",
  "recording_metadata_available": {
    "call_id": "call-abc",
    "metadata": {
      "filename": "uuid_2026-05-14.mp3",
      "unique_id": "uuid-abc-123",
      "file_size": 149517,
      "download_url": "https://storage.example.com/rec.mp3",
      "caller_name": "330909",
      "callee_name": "9242000001",
      "called_phone": null,
      "call_type": "inbound",
      "agent_id": "451447",
      "agent_name": "luoxiaofeng90_v",
      "call_start_time": "2026-05-14T08:11:35Z",
      "call_end_time": "2026-05-14T08:12:26Z",
      "upload_time": "2026-05-14T16:14:46Z",
      "switch_flag": "ks",
      "process_flag": "ks_22_normal",
      "root_call_id": "call-root-42"
    }
  }
}

record_end

Dispatch: call_owner

Recording finalisation event. Emitted after the recording upload completes; if no upload is configured, it fires when the local recording file is ready (using the local path as url). Also emitted after SipFlow media upload completes.

Trigger conditions:

  • Regular recording: automatically emitted by RecordingUploadHook after CallRecordManager processes the record
  • SipFlow recording: emitted after SipFlow media file upload to S3/HTTP completes
  • Not triggered by the RecordStop command — unlike record_started/record_stopped which require an explicit command
FieldTypeDescription
call_idStringCall identifier
urlOption<String>Upload URL (if uploaded), local file path (no upload), or SipFlow media file URL
duration_secsu64Recording duration (seconds)
file_sizeu64File size (bytes)

6.5 IVR Events

All IVR events carry flat context fields.

ivr_node_entered

Dispatch: fan_out_to_context

Call enters an IVR node (menu, prompt, etc.).

FieldTypeDescription
call_idStringCall identifier
node_idStringNode ID
node_nameStringNode name
node_typeStringNode type (menu, prompt, transfer, etc.)
app_idStringIVR application ID
entry_timeStringEntry timestamp (ISO 8601)
caller_nameOption<String>Calling party number
callee_nameOption<String>Dialed number
routing_targetOption<String>Routing target
previous_node_idOption<String>Previous node ID
+ctxFlat context fields

ivr_node_exited

Dispatch: fan_out_to_context

Call exits an IVR node.

Also emitted on session termination: when the sip_session is terminated mid-flow (caller hangup, system cancel, etc.), the built-in (tree-mode) IVR emits this event to record the node the caller was on. In that case hangup_reason is populated (e.g. cancelled, remote_hangup, hangup, error) and call_result is "hangup".

FieldTypeDescription
call_idStringCall identifier
node_idStringNode ID
node_nameStringNode name
result_valueOption<String>User DTMF or branch result
duration_msu32Node dwell time in milliseconds
exit_timeStringExit timestamp
next_node_idOption<String>Next node ID
hangup_reasonOption<String>Hangup reason (on session termination: cancelled/remote_hangup/hangup, etc.)
call_resultOption<String>Call result
+ctxFlat context fields

ivr_flow_transitioned

Dispatch: fan_out_to_context

Call transitions between IVR applications.

FieldTypeDescription
call_idStringCall identifier
from_app_idStringSource application ID
to_app_idStringTarget application ID
from_node_idStringSource node ID
to_node_idStringTarget node ID
transition_reasonStringTransition reason (menu_choice, transfer, overflow, etc.)
transition_timeStringTransition timestamp
next_routing_targetOption<String>Next routing target
+ctxFlat context fields

ivr_flow_completed

Dispatch: fan_out_to_context

IVR flow completes (terminal action executed: Transfer, Queue, Voicemail, Hangup).

Also emitted on session termination: when the built-in (tree-mode) IVR is terminated mid-flow by the sip_session (caller hangup remote_hangup, system cancel cancelled, etc.), it is emitted with final_result set to the termination reason and total_nodes_traversed populated. final_result values: transferred, queue, voicemail, hangup, abandoned, cancelled, remote_hangup, error, etc.

FieldTypeDescription
call_idStringCall identifier
app_idStringIVR application ID
total_nodes_traversedu32Total nodes traversed
total_duration_msu32Total IVR duration in milliseconds
final_resultStringFinal result (transferred, voicemail, abandoned, cancelled, remote_hangup, etc.)
completion_timeStringCompletion timestamp
final_routing_targetOption<String>Final routing target
+ctxFlat context fields
{
  "rwi": "1.0",
  "ivr_flow_completed": {
    "call_id": "call-abc",
    "app_id": "ivr-sales",
    "total_nodes_traversed": 3,
    "total_duration_ms": 15200,
    "final_result": "transferred",
    "completion_time": "2026-05-14T17:55:00Z",
    "final_routing_target": "queue:support",
    "caller": "13800138000",
    "direction": "inbound"
  }
}

ivr_step_trace

Dispatch: fan_out_to_context

Step-mode IVR trace event. Emitted on each provider round-trip or action execution completion.

Session-end entry (session_end): when the IVR session ends (including caller hangup RemoteHangup and system cancel Cancelled), an extra trace entry with trigger.type="session_end" is emitted. action_type/step_id/step_name record the last executed node, and end_reason/end_detail describe how the whole session ended. The external provider /end webhook is not called on RemoteHangup/Cancelled (the local trace event is still emitted).

FieldTypeDescription
call_idStringCall identifier
session_idStringSession ID
callerStringCaller
calleeStringCallee
step_indexu32Step index
triggerObjectStructured trigger info for this step, see below
action_typeStringAction type (e.g., Transfer, Prompt, DtmfMenu)
action_jsonOption<String>Action details JSON
result_kindStringResult type (terminal, continue, error)
duration_msu64Step execution duration (ms), always present
errorOption<String>Error message
step_idOption<String>Current node ID, returned by provider via ActionNode.step_id
step_nameOption<String>Current node name, returned by provider via ActionNode.step_name
step_start_timeOption<String>Current step start time (ISO UTC)
step_end_timeOption<String>Current step end time (ISO UTC). Only present when step execution completes (terminal/error); null during WaitFor (waiting for user input)
extraOption<JSON Object>Transparent passthrough data from provider. Provider returns the complete object in ActionNode.extra each time; RustPBX stores and outputs it as-is
end_reasonOption<String>Present only on the session-end (session_end) entry; identifies how the whole IVR session ended (normal, transfer, transfer_to_queue, hangup, user_hangup, timeout, error, etc.)
end_detailOption<String>Companion detail for end_reason (e.g. transfer target, error message)

trigger field:

Describes what caused the current step to execute, as an object:

{ "type": "dtmf", "detail": { "digit": "2" } }
Sub-fieldTypeDescription
typeStringTrigger source type: session_start, session_end, dtmf, dtmf_menu, dtmf_menu_timeout, audio_complete, action_execute, chained, api_response, phone_collected, recording_complete, input_voice, error, dtmf_menu_invalid, unknown
detailOption<JSON Object>Structured trigger detail, omitted when none. Common values: DTMF → {"digit":"2"}; API response → {"status":200}; phone collection → {"number":"13800138000"}

Timing fields:

  • step_start_time — when the current step started (previous step end or session start)
  • step_end_time — when the step ended (only on completion)

Duration fields:

  • duration_ms — step execution duration (ms), always present, includes provider round-trip and action execution time

6.6 Queue / ACD Events

Event origin: Queue-related events come in two families, produced by different subsystems and may co-occur:

  • queue_* (queue lifecycle): produced by the Queue app (src/call/app/queue.rs) and the CC ACD engine bridge. Covers the generic lifecycle: join, ringing, connected, abandon, timeout, fallback.
  • skill_group_* (skill-group scheduling decisions): produced exclusively by the CC addon's ACD adapter (src/addons/cc/agent_registry_adapter.rs) when the queue asks the ACD for an agent. Fires only when the CC addon is active and skill routing is used. The ACD-engine queue_* bridge intentionally does not emit skill_group_* (single source, no duplicates).

Typical event sequence for a skill-group-routed call: queue_joinedskill_group_candidates_foundskill_group_call_queued (only when no agent is immediately available) → skill_group_agent_assignedqueue_agent_offeredqueue_agent_connected

skill_group_call_abandoned fires when the caller hangs up while still queued; skill_group_service_unavailable fires on queue timeout or fallback. Both are reported by the Queue app through the AgentRegistry lifecycle hooks (notify_call_abandoned / notify_call_timeout / notify_call_fallback), which the CC adapter maps to the RWI events.

All queue events carry flat context fields.

queue_joined

Dispatch: call_owner / broadcast

FieldTypeDescription
call_idStringCall identifier
queue_idStringQueue ID
+ctxFlat context fields

queue_position_changed

FieldTypeDescription
call_idStringCall identifier
queue_idStringQueue ID
positionu32Current queue position
+ctxFlat context fields

queue_agent_offered / queue_agent_connected

FieldTypeDescription
call_idStringCall identifier
queue_idStringQueue ID
agent_idStringAgent ID
+ctxFlat context fields

queue_left

FieldTypeDescription
call_idStringCall identifier
queue_idStringQueue ID
reasonOption<String>Leave reason
+ctxFlat context fields

queue_wait_timeout

FieldTypeDescription
call_idStringCall identifier
queue_idStringQueue ID
+ctxFlat context fields

queue_overflowed

FieldTypeDescription
call_idStringCall identifier
original_queue_idStringOriginal queue ID
overflow_queue_idStringOverflow target queue ID
reasonStringOverflow reason
+ctxFlat context fields

queue_voicemail_redirected

FieldTypeDescription
call_idStringCall identifier
queue_idStringQueue ID
reasonStringReason
+ctxFlat context fields

queue_candidates_found

FieldTypeDescription
call_idStringCall identifier
queue_idStringQueue ID
candidatesVec<String>Candidate agent list
trace_idStringACD trace ID
+ctxFlat context fields

queue_agent_ringing / queue_agent_no_answer / queue_agent_rejected

FieldTypeDescription
call_idStringCall identifier
queue_idStringQueue ID
agent_idStringAgent ID
attemptu32no_answer/rejected only: attempt number
trace_idStringACD trace ID
+ctxFlat context fields

queue_fallback_executed

FieldTypeDescription
call_idStringCall identifier
queue_idStringQueue ID
actionStringFallback action executed
reasonStringReason
trace_idStringACD trace ID
+ctxFlat context fields

queue_alert

Dispatch: broadcast (no call_id)

FieldTypeDescription
queue_idStringQueue ID
alert_typeStringAlert type
messageStringAlert message

skill_group_candidates_found

Dispatch: broadcast

Emitted when the ACD scheduler finds candidate agents for a skill group.

FieldTypeDescription
call_idStringCall identifier
skill_group_idOption<String>Skill group ID (Some for the explicit skill-group:{id} path; None for autonomous skill routing)
candidatesVec<String>Candidate agent ID list
trace_idStringTrace ID
+ctxFlat context fields

skill_group_agent_assigned

Dispatch: broadcast

Emitted when the ACD scheduler decides to assign an agent to the call. This fires for an ACD Assign decision and for the strategy-picked first agent when no inline ACD policy is configured ("first agent selected by the strategy").

FieldTypeDescription
call_idStringCall identifier
skill_group_idOption<String>Skill group ID
agent_idStringAssigned agent ID
dispatch_reasonStringregular / forced_available / overflow
trace_idStringTrace ID
+ctxFlat context fields

skill_group_no_agent

Dispatch: broadcast

Emitted when the ACD scheduler cannot provide an agent for the skill group.

FieldTypeDescription
call_idStringCall identifier
skill_group_idOption<String>Skill group ID
reasonStringReason (no_candidates no matching agent / acd_blocked blocked by ACD policy / no_strategy_match strategy picked none)
+ctxFlat context fields

skill_group_call_queued

Dispatch: broadcast

Emitted when the call enters the skill-group queue because no agent was immediately available. Fires on an ACD Wait decision (with real position/ ewt_secs) or, when no ACD policy is configured, whenever routing finds no currently available agent (best-effort position/ewt_secs).

FieldTypeDescription
call_idStringCall identifier
skill_group_idStringSkill group ID
positionusizeQueue position
ewt_secsu32Estimated wait time (seconds)
reasonStringno_agent_available
trace_idStringTrace ID
+ctxFlat context fields

skill_group_call_abandoned

Dispatch: broadcast

Emitted when the caller hangs up while still waiting in the skill-group queue (before any agent answered).

FieldTypeDescription
call_idStringCall identifier
skill_group_idStringSkill group ID
waited_secsu64Time waited before abandoning
positionusizeQueue position at abandon
trace_idStringTrace ID
+ctxFlat context fields

skill_group_service_unavailable

Dispatch: broadcast

Emitted when a queued call could not be serviced (queue timeout or fallback).

FieldTypeDescription
call_idStringCall identifier
skill_group_idStringSkill group ID
reasonStringtimeout / fallback reason
attemptsu32Retry attempts
waited_secsu64Time waited
fallback_actionStringExecuted fallback action
trace_idStringTrace ID
+ctxFlat context fields

6.7 Agent State Events

agent_state_changed

Dispatch: broadcast

Agent state machine transition.

FieldTypeDescription
agent_idStringAgent ID
from_statusStringPrevious status
to_statusStringNew status
call_idOption<String>Associated call ID
agent_nameOption<String>Agent display name
agent_extensionOption<String>Agent extension number
callerOption<String>Caller / directory number
team_idOption<String>Team ID
duration_secsOption<u32>Duration in previous status
reason_codeOption<String>Reason code (e.g., CALL, BREAK, TRAINING)

Agent status values:

StatusDescriptionCan transition to
offlineDisconnectedidle, away, dnd
idleReady to accept callsringing, away, dnd, offline
awayOnline but not accepting (break)idle, dnd, offline
dndDo not disturb (meeting/training)idle, away, offline
ringingRinging (call_id present)busy (answer), idle (no answer)
busyOn a call (call_id present)wrapup
wrapupAfter-call workidle, away, dnd
custom:<name>Custom statusidle, away, dnd, offline
{
  "rwi": "1.0",
  "agent_state_changed": {
    "agent_id": "agent-001",
    "from_status": "idle",
    "to_status": "busy",
    "call_id": "call-abc",
    "agent_name": "Alice",
    "agent_extension": "8001",
    "caller": "8001",
    "team_id": "sales",
    "duration_secs": 300,
    "reason_code": "CALL"
  }
}

6.8 DN (Directory Number) Events

dn_state_changed

Dispatch: broadcast

Granular extension-level signaling events.

FieldTypeDescription
callerStringExtension / caller
event_nameStringEvent name (see table below)
system_timeStringSystem timestamp
call_idOption<String>Associated call ID
agent_idOption<String>Agent ID
caller_nameOption<String>Calling party name/number
callee_nameOption<String>Called party name/number
reason_codeOption<String>Reason code
agent_work_modeOption<String>Agent work mode
releasing_partyOption<String>Releasing party ("1 Local" / "2 Remote")
vq_nameOption<String>Virtual queue name
routing_targetOption<String>Routing target
skill_groupOption<String>Skill group
extraOption<Map<String, Value>>Extension fields (omitted when absent)

event_name values:

event_nameDescriptionTrigger
REGISTEREDExtension registeredSIP REGISTER success
DIALINGOutbound dialingAgent outbound or manual dial
RINGINGRingingAgent-side ringing
ESTABLISHEDCall establishedCall answered
RELEASEDReleasedHangup or transfer completed
ABANDONEDAbandonedCaller abandoned during ringing
HELDHeldAgent held, user hears music
RETRIEVEDRetrieved from holdHeld party retrieved
PARTYCHANGEDMulti-party state changedConference state changed
PARTYADDEDMulti-party addedParty added to conference
PARTYDELETEDMulti-party removedParty removed from conference
AGENTLOGINAgent loginAgent went from offline to online (CC addon)
AGENTLOGOUTAgent logoutAgent went from online to offline (CC addon)
AGENTREADYAgent readyAgent entered idle state (CC addon)
AGENTNOTREADYAgent not readyAgent entered busy/ringing/wrapup etc. (CC addon)
ONHOOKOn hookPhone on hook

Note: Use event_name for event routing and matching.

{
  "rwi": "1.0",
  "dn_state_changed": {
    "caller": "80001",
    "event_name": "ESTABLISHED",
    "system_time": "2026-05-14T17:54:49.003Z",
    "call_id": "call-abc",
    "agent_id": "10001",
    "caller_name": "19534519769",
    "callee_name": "39989",
    "extra": {
      "source": "KS",
      "kz_conn_id": "kc-12345",
      "user_data": { "kz_target": "39299", "kz_flowname": "CTC400Customer" }
    }
  }
}

dn_registered / dn_unregistered

Dispatch: broadcast

FieldTypeDescription
callerStringExtension number
agent_idOption<String>Agent ID
register_time / unregister_timeStringRegistration/unregistration timestamp

6.9 Call Metadata Events

call_metadata_updated

Triggered when call metadata is updated after initial call_incoming.

FieldTypeDescription
call_idStringCall identifier
metadataCallMetadataMetadata (see below)

CallMetadata fields:

FieldTypeDescription
root_call_idOption<String>Root call ID
caller_nameOption<String>Calling party number
callee_nameOption<String>Dialed number
called_phoneOption<String>Actual called number
dial_directionOption<String>Call direction
uuidOption<String>Global UUID
routing_pathOption<Vec<String>>Routing path
app_idOption<String>IVR application ID
routing_targetOption<String>Routing target
switch_nameOption<String>Switch name
{
  "rwi": "1.0",
  "call_metadata_updated": {
    "call_id": "call-abc",
    "metadata": {
      "root_call_id": "call-root-42",
      "caller_name": "330909",
      "callee_name": "9242000001",
      "called_phone": "018659727661",
      "dial_direction": "inbound",
      "uuid": "uuid-abc-123",
      "routing_path": ["menu:root", "queue:level1"],
      "app_id": "ivr-support",
      "routing_target": "queue:support",
      "switch_name": "SIP_Switch_KS"
    }
  }
}

6.10 Conference Events

conference_created / conference_destroyed

Dispatch: broadcast

FieldTypeDescription
conf_idStringConference room ID

conference_member_joined / conference_member_left / conference_member_muted / conference_member_unmuted

Dispatch: broadcast

FieldTypeDescription
conf_idStringConference ID
call_idStringMember call ID
+ctxFlat context fields

conference_ended_by_host

FieldTypeDescription
conf_idStringConference ID
host_call_idStringHost call ID
removed_call_idsVec<String>Removed member call IDs
+ctxFlat context fields

conference_auto_ended

FieldTypeDescription
conf_idStringConference ID
reasonStringEnd reason
+ctxFlat context fields

conference_error

FieldTypeDescription
conf_idStringConference ID
errorStringError message

conference_consult_dialing / conference_consult_connected

FieldTypeDescription
call_idStringConsultation call ID
targetStringConsultation target
+ctxFlat context fields

conference_merge_requested / conference_merged / conference_merge_failed

FieldTypeDescription
call_idStringCall ID (merge_requested includes consultation_call_id)
conf_idStringConference ID (merged/merge_failed)
consultation_call_idStringmerge_requested only: consultation call ID
reasonStringmerge_failed only: failure reason
+ctxFlat context fields

conference_seat_replace_started / ...succeeded / ...failed / ...rollback_failed

FieldTypeDescription
conf_idStringConference ID
old_call_idStringOld member call ID
new_call_idStringNew member call ID
reasonStringfailed/rollback_failed only: failure reason

Seat replacement event sequence (success path):

  1. conference_seat_replace_started
  2. conference_member_left (old member leaves)
  3. conference_member_joined (new member joins)
  4. conference_seat_replace_succeeded

6.11 Supervisor Events

supervisor_listen_started / supervisor_whisper_started / supervisor_barge_started / supervisor_takeover_started

FieldTypeDescription
supervisor_call_idStringSupervisor call ID
target_call_idStringTarget call ID

supervisor_mode_stopped

FieldTypeDescription
supervisor_call_idStringSupervisor call ID
target_call_idStringTarget call ID

6.12 Parallel Originate Events

parallel_originate_started

FieldTypeDescription
operation_idStringOperation ID
leg_countu32Number of parallel legs

parallel_originate_leg_ringing / parallel_originate_winner / parallel_originate_leg_cancelled

FieldTypeDescription
operation_idStringOperation ID
call_idStringLeg call ID
destinationStringDialed destination
reasonStringleg_cancelled only: cancellation reason
+ctxFlat context fields

parallel_originate_completed

FieldTypeDescription
operation_idStringOperation ID
winning_call_idStringWinning call ID

parallel_originate_failed

FieldTypeDescription
operation_idStringOperation ID
reasonStringFailure reason

6.13 SIP Signaling Events

sip_message_received / sip_notify_received

FieldTypeDescription
call_idStringCall identifier
content_typeStringContent type
bodyStringMessage body
eventStringsip_notify_received only: SIP Event header
+ctxFlat context fields

6.14 Session System Events

call_ownership_changed

FieldTypeDescription
call_idStringCall identifier
session_idStringTaking-over session ID
modeStringMode (control/listen/whisper/barge)
+ctxFlat context fields

session_resumed

FieldTypeDescription
session_idStringResumed session ID
last_sequenceu64Client-reported last sequence number

7. Event Quick Reference

Event TypeDispatchcall_idContext
call_incomingfan_outyesown fields
call_ringingowneryes+ctx
call_early_mediaowneryes+ctx
call_answeredowneryes+ctx
call_bridgedownerleg_a
call_unbridgedowneryes+ctx
call_transferredowneryes+ctx
call_transfer_acceptedowneryes+ctx
call_transfer_failedowneryes+ctx
call_hangupowneryes+ctx
call_no_answerowneryes+ctx
call_busyowneryes+ctx
media_hold_startedowneryes+ctx
media_hold_stoppedowneryes+ctx
media_ringback_passthrough_startedowneryes
media_ringback_passthrough_stoppedowneryes
media_play_startedowneryes+ctx
media_play_finishedowneryes+ctx
media_stream_startedowneryes+ctx
media_stream_stoppedowneryes+ctx
record_startedowneryes+ctx
record_pausedowneryes+ctx
record_resumedowneryes+ctx
record_stoppedowneryesown fields + enrich
record_failedowneryes+ctx
recording_metadata_availableowneryes
dtmffan_outyes+ctx
dtmf_collectedowneryes+ctx
dtmf_collection_timeoutowneryes+ctx
ivr_node_enteredfan_outyes+ctx
ivr_node_exitedfan_outyes+ctx
ivr_flow_transitionedfan_outyes+ctx
ivr_flow_completedfan_outyes+ctx
ivr_step_tracefan_outyes
queue_joinedowner/broadcastyes+ctx
queue_position_changedowneryes+ctx
queue_agent_offeredbroadcastyes+ctx
queue_agent_connectedowneryes+ctx
queue_leftbroadcastyes+ctx
queue_wait_timeoutowneryes+ctx
queue_overflowedowneryes+ctx
queue_voicemail_redirectedowneryes+ctx
queue_candidates_foundowneryes+ctx
queue_agent_ringingowneryes+ctx
queue_agent_no_answerowneryes+ctx
queue_agent_rejectedowneryes+ctx
queue_fallback_executedowneryes+ctx
queue_alertbroadcast
skill_group_candidates_foundbroadcastyes+ctx
skill_group_agent_assignedbroadcastyes+ctx
skill_group_no_agentbroadcastyes+ctx
skill_group_call_queuedbroadcastyes+ctx
skill_group_call_abandonedbroadcastyes+ctx
skill_group_service_unavailablebroadcastyes+ctx
agent_state_changedbroadcastoptional+ctx
cc_ringingbroadcastyes+ctx
cc_answeredbroadcastyes+ctx
cc_hangupbroadcastyes+ctx
cc_heldbroadcastyes+ctx
cc_unheldbroadcastyes+ctx
dn_state_changedbroadcastoptional
dn_registeredbroadcast
dn_unregisteredbroadcast
call_metadata_updatedowneryes
conference_createdbroadcast
conference_member_joinedbroadcastyes+ctx
conference_member_leftbroadcastyes+ctx
conference_member_mutedbroadcastyes+ctx
conference_member_unmutedbroadcastyes+ctx
conference_destroyedbroadcast
conference_ended_by_hostbroadcast+ctx
conference_auto_endedbroadcast+ctx
conference_errorbroadcast
conference_consult_dialingowneryes+ctx
conference_consult_connectedowneryes+ctx
conference_merge_requestedfan_outyes+ctx
conference_mergedfan_outyes+ctx
conference_merge_failedfan_outyes+ctx
conference_seat_replace_startedfan_outyes
conference_seat_replace_succeededfan_outyes
conference_seat_replace_failedfan_outyes
conference_seat_replace_rollback_failedfan_outyes
supervisor_listen_startedowner
supervisor_whisper_startedowner
supervisor_barge_startedowner
supervisor_takeover_startedowner
supervisor_mode_stoppedowner
parallel_originate_startedowner
parallel_originate_leg_ringingowneryes+ctx
parallel_originate_winnerowneryes+ctx
parallel_originate_leg_cancelledowneryes+ctx
parallel_originate_completedowneryes
parallel_originate_failedowner
sip_message_receivedowneryes+ctx
sip_notify_receivedowneryes+ctx
call_ownership_changedowneryes+ctx
session_resumedowner

8. Developer Examples

Python Webhook Receiver

from http.server import HTTPServer, BaseHTTPRequestHandler
import json

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        body = json.loads(self.rfile.read(length))

        event_type = body["event_type"]
        call_id = body["call_id"]

        print(f"[{event_type}] call_id={call_id}")

        if event_type == "recording_metadata_available":
            meta = body["event"]["recording_metadata_available"]["metadata"]
            print(f"  download: {meta['download_url']}")
            print(f"  file_size: {meta['file_size']}")

        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(b'{"status":"ok"}')

HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()

Python WebSocket Real-time Listener

import asyncio, json
from websockets import connect

async def main():
    async with connect(
        "ws://pbx.example.com/rwi/v1",
        additional_headers={"Authorization": "Bearer your-token"},
        subprotocols=["rwi-v1"],
    ) as ws:
        await ws.send(json.dumps({
            "rwi": "1.0",
            "action_id": "sub-001",
            "action": "session.subscribe",
            "params": {"contexts": ["*"]}
        }))

        async for msg in ws:
            payload = json.loads(msg)
            for key, data in payload.items():
                if key == "rwi":
                    continue
                print(f"[{key}] {json.dumps(data, ensure_ascii=False)}")

asyncio.run(main())

JavaScript / Node.js

const ws = new WebSocket("ws://pbx.example.com/rwi/v1", "rwi-v1", {
  headers: { Authorization: "Bearer your-token" }
});

ws.onopen = () => {
  ws.send(JSON.stringify({
    rwi: "1.0",
    action_id: "sub-001",
    action: "session.subscribe",
    params: { contexts: ["*"] }
  }));
};

ws.onmessage = (event) => {
  const payload = JSON.parse(event.data);
  for (const [eventType, eventData] of Object.entries(payload)) {
    if (eventType === "rwi") continue;
    console.log(`[${eventType}] call=${eventData.call_id}`, eventData);
  }
};

9. Auxiliary Structures

These structs are used as nested references and are not emitted as standalone events.

IvrNodeInfo

FieldTypeDescription
node_idStringNode ID
node_nameStringNode name
node_typeStringNode type
routing_targetOption<String>Routing target
previous_node_idOption<String>Previous node ID
next_node_idOption<String>Next node ID
duration_msOption<u32>Dwell time
result_valueOption<String>DTMF/result

IvrFlowContext

FieldTypeDescription
app_idStringIVR application ID
routing_pathVec<String>Routing path
service_typeOption<String>Service type
customer_typeOption<String>Customer type

Document version: v1.0
Last updated: 2026-06-23
Source code: src/rwi/proto.rs