Source code:
src/rwi/proto.rs| Protocol version:1.0
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:
| Channel | Protocol | Use Case |
|---|---|---|
| WebSocket subscription | ws(s)://<host>/rwi/v1 |
Real-time bidirectional interaction (bots, softphones, dashboards) |
| Webhook callback | HTTP POST | Async notifications (CRM, recording systems, analytics) |
| Method | Recipient | Meaning |
|---|---|---|
call_owner |
WS session owning the call_id | Per-call fine-grained events |
fan_out |
All WS sessions subscribed to the context | Incoming call notifications, IVR events |
broadcast |
All online WS sessions | Global events (agent state, DN registration, etc.) |
webhook |
Configured HTTP endpoint | All events forwarded (filterable) |
GET /rwi/v1 HTTP/1.1
Upgrade: websocket
Authorization: Bearer <token>
Or via query parameter: GET /rwi/v1?token=<token>
[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 = []| Field | Type | Default | Description |
|---|---|---|---|
url |
String | (required) | HTTP endpoint receiving POST requests |
timeout_ms |
u64 | 5000 | HTTP request timeout in milliseconds |
headers |
HashMap | (optional) | Custom HTTP headers sent with every request |
events |
Vec<String> | [] (all) | Event type whitelist; empty forwards all events |
{
/* 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.
{
"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 |
| `agent_id` | Option\<String\> | Agent ID |
| `agent_name` | Option\<String\> | Agent display name |
**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 Format | Description |
|---|---|
queue:<queue_id> |
Subscribe to queue events |
agent:<agent_id> |
Subscribe to agent events |
* |
Wildcard — receive all broadcast events |
{
"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.
The webhook handler deduplicates using (call_id, sequence) tuples in a 4096-entry ring buffer. Duplicate events are silently dropped.
In the tables below,
+ctxmeans the event carries flat context fields.?indicates anOption<T>field — omitted from JSON whennull.
Dispatch: fan_out_to_context
New call enters the system. First event in any call flow.
| Field | Type | Description |
|---|---|---|
call_id |
String | Unique call identifier |
context |
String | Dialplan context |
caller |
String | Caller SIP URI |
callee |
String | Callee SIP URI |
dial_direction |
String | inbound / outbound / internal |
trunk |
Option<String> | SIP trunk name |
sip_headers |
Map<String, String> | Whitelisted SIP headers |
root_call_id |
Option<String> | Root call ID (constant across transfers) |
caller_name |
Option<String> | Calling party number |
callee_name |
Option<String> | Dialed number / DNIS |
called_phone |
Option<String> | Actual called number (outbound scenario) |
app_id |
Option<String> | IVR application ID |
routing_target |
Option<String> | Routing target |
uuid |
Option<String> | Global UUID (for recording linkage) |
routing_path |
Option<Vec<String>> | Routing path sequence |
Note:
call_incomingusesdial_direction; other events' context usesdirection.
{
"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"]
}
}Dispatch: call_owner
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
| +ctx | Flat 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"
}
}Dispatch: call_owner (both legs receive it)
| Field | Type | Description |
|---|---|---|
leg_a |
String | A-leg call_id |
leg_b |
String | B-leg call_id |
Dispatch: call_owner
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
reason |
Option<String> | Hangup reason (see table below) |
sip_status |
Option<u16> | SIP response code |
| +ctx | Flat context fields |
reason values:
| Value | Description |
|---|---|
caller |
Caller hung up |
callee |
Callee hung up |
refer |
REFER transfer hangup |
system |
System hangup |
autohangup |
Auto hangup (timeout) |
noAnswer |
No answer (408/480/487) |
rejected |
Rejected/busy (486/600/603) |
canceled |
Canceled (487) |
failed |
Generic failure (other 4xx) |
serverUnavailable |
Server unavailable (5xx) |
rtpTimeout |
RTP timeout |
{
"rwi": "1.0",
"call_hangup": {
"call_id": "call-abc",
"reason": "caller",
"sip_status": null,
"caller": "sip:13800138000@pbx.local",
"callee": "sip:4000@pbx.local",
"caller_name": "13800138000",
"callee_name": "4000",
"direction": "inbound"
}
}Dispatch: call_owner
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
transfer_target |
Option<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). |
| +ctx | Flat context fields |
Dispatch: call_owner
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
sip_status |
Option<u16> | SIP status code |
reason |
Option<String> | Failure reason |
transfer_target |
Option<String> | Original transfer target string (see above) |
| +ctx | Flat context fields |
Dispatch: call_owner
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
| +ctx | Flat context fields |
Dispatch: call_owner
| Field | Type | Description |
|---|---|---|
source |
String | Source leg call_id |
target |
String | Target leg call_id |
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
leg_id |
Option<String> | Target leg |
track_id |
String | Playback track ID |
interrupted |
bool | media_play_finished only: whether interrupted by DTMF |
| +ctx | Flat context fields |
Dispatch: fan_out_to_context
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
digit |
String | DTMF digit (0-9, *, #) |
leg_id |
Option<String> | Leg that generated the DTMF |
extra |
Option<Object> | Extra data (extension field, defaults to null) |
| +ctx | Flat context fields |
Dispatch: call_owner
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
leg_id |
String | Leg that provided the digits |
digits |
String | dtmf_collected only: collected digit string |
| +ctx | Flat context fields |
Dispatch: call_owner
Trigger: Via
RecordStart/RecordPause/RecordResume/RecordStopRWI commands. Not automatic — recording does not start automatically when a call is answered.
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
error |
String | record_failed only: error message |
| +ctx | Flat context fields |
Dispatch: call_owner
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
duration_secs |
Option<u64> | Recording duration in seconds |
filename |
Option<String> | Recording filename |
unique_id |
Option<String> | Recording UUID |
file_size |
Option<u64> | File size in bytes |
download_url |
Option<String> | Download URL |
caller_name |
Option<String> | Calling party number |
callee_name |
Option<String> | Dialed number |
called_phone |
Option<String> | Actual called number |
call_type |
Option<String> | inbound/outbound/internal/consult |
agent_id |
Option<String> | Agent ID |
agent_name |
Option<String> | Agent name |
call_start_time |
Option<String> | Call start timestamp (ISO 8601) |
call_end_time |
Option<String> | Call end timestamp |
upload_time |
Option<String> | Upload completion timestamp |
switch_flag |
Option<String> | Site identifier (e.g., ks, bj) |
root_call_id |
Option<String> | Root call ID |
Note:
record_stoppeddoes not carry flat context, but includes its ownani/dnisfields.enrich()backfillsNonefields 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"
}
}Dispatch: call_owner
Triggered when the recording file upload completes, containing full metadata.
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
metadata |
RecordingMetadata | Recording metadata (see below) |
RecordingMetadata fields:
| Field | Type | Description |
|---|---|---|
filename |
String | Recording filename (required) |
unique_id |
String | Recording UUID (required) |
file_size |
u64 | File size in bytes (required) |
download_url |
Option<String> | Download URL |
caller_name |
Option<String> | Calling party number |
callee_name |
Option<String> | Dialed number |
called_phone |
Option<String> | Actual called number |
call_type |
String | Call type (required) |
agent_id |
Option<String> | Agent ID |
agent_name |
Option<String> | Agent name |
call_start_time |
Option<String> | Call start timestamp |
call_end_time |
Option<String> | Call end timestamp |
upload_time |
Option<String> | Upload completion timestamp |
switch_flag |
Option<String> | Site identifier |
process_flag |
Option<String> | Process identifier (e.g., ks_22_normal) |
root_call_id |
Option<String> | Root call ID |
{
"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"
}
}
}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
RecordingUploadHookafterCallRecordManagerprocesses the record- SipFlow recording: emitted after SipFlow media file upload to S3/HTTP completes
- Not triggered by the
RecordStopcommand — unlikerecord_started/record_stoppedwhich require an explicit command
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
url |
Option<String> | Upload URL (if uploaded), local file path (no upload), or SipFlow media file URL |
duration_secs |
u64 | Recording duration (seconds) |
file_size |
u64 | File size (bytes) |
All IVR events carry flat context fields.
Dispatch: fan_out_to_context
Call enters an IVR node (menu, prompt, etc.).
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
node_id |
String | Node ID |
node_name |
String | Node name |
node_type |
String | Node type (menu, prompt, transfer, etc.) |
app_id |
String | IVR application ID |
entry_time |
String | Entry timestamp (ISO 8601) |
caller_name |
Option<String> | Calling party number |
callee_name |
Option<String> | Dialed number |
routing_target |
Option<String> | Routing target |
previous_node_id |
Option<String> | Previous node ID |
| +ctx | Flat context fields |
Dispatch: fan_out_to_context
Call exits an IVR node.
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
node_id |
String | Node ID |
node_name |
String | Node name |
result_value |
Option<String> | User DTMF or branch result |
duration_ms |
u32 | Node dwell time in milliseconds |
exit_time |
String | Exit timestamp |
next_node_id |
Option<String> | Next node ID |
hangup_reason |
Option<String> | Hangup reason |
call_result |
Option<String> | Call result |
| +ctx | Flat context fields |
Dispatch: fan_out_to_context
Call transitions between IVR applications.
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
from_app_id |
String | Source application ID |
to_app_id |
String | Target application ID |
from_node_id |
String | Source node ID |
to_node_id |
String | Target node ID |
transition_reason |
String | Transition reason (menu_choice, transfer, overflow, etc.) |
transition_time |
String | Transition timestamp |
next_routing_target |
Option<String> | Next routing target |
| +ctx | Flat context fields |
Dispatch: fan_out_to_context
IVR flow completes (terminal action executed: Transfer, Queue, Voicemail, Hangup).
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
app_id |
String | IVR application ID |
total_nodes_traversed |
u32 | Total nodes traversed |
total_duration_ms |
u32 | Total IVR duration in milliseconds |
final_result |
String | Final result (transferred, voicemail, abandoned, etc.) |
completion_time |
String | Completion timestamp |
final_routing_target |
Option<String> | Final routing target |
| +ctx | Flat 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"
}
}Dispatch: fan_out_to_context
Step-mode IVR trace event. Emitted on each provider round-trip or action execution completion.
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
session_id |
String | Session ID |
caller |
String | Caller |
callee |
String | Callee |
step_index |
u32 | Step index |
trigger |
Object | Structured trigger info for this step, see below |
action_type |
String | Action type (e.g., Transfer, Prompt, DtmfMenu) |
action_json |
Option<String> | Action details JSON |
result_kind |
String | Result type (terminal, continue, error) |
duration_ms |
u64 | Step execution duration (ms), always present |
error |
Option<String> | Error message |
step_id |
Option<String> | Current node ID, returned by provider via ActionNode.step_id |
step_name |
Option<String> | Current node name, returned by provider via ActionNode.step_name |
step_start_time |
Option<String> | Current step start time (ISO UTC) |
step_end_time |
Option<String> | Current step end time (ISO UTC). Only present when step execution completes (terminal/error); null during WaitFor (waiting for user input) |
extra |
Option<JSON Object> | Transparent passthrough data from provider. Provider returns the complete object in ActionNode.extra each time; RustPBX stores and outputs it as-is |
triggerfield:Describes what caused the current step to execute, as an object:
{ "type": "dtmf", "detail": { "digit": "2" } }
Sub-field Type Description typeString Trigger source type: session_start,dtmf,dtmf_menu,dtmf_menu_timeout,audio_complete,action_execute,chained,api_response,phone_collected,recording_complete,input_voice,error,dtmf_menu_invalid,unknowndetailOption<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
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), fires regardless of whether the CC addon is enabled. Covers the generic lifecycle: join, ringing, connected, abandon, timeout, fallback.skill_group_*(skill-group scheduling decisions): produced by the CC addon's ACD adapter (src/addons/cc/agent_registry_adapter.rs) when the queue asks the ACD for an agent and the ACD produces a scheduling result. Fires only when the CC addon is active and skill routing is used.Typical event sequence for a skill-group-routed call:
queue_joined→skill_group_candidates_found→skill_group_agent_assigned→queue_agent_offered→queue_agent_connected
All queue events carry flat context fields.
Dispatch: call_owner / broadcast
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
queue_id |
String | Queue ID |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
queue_id |
String | Queue ID |
position |
u32 | Current queue position |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
queue_id |
String | Queue ID |
agent_id |
String | Agent ID |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
queue_id |
String | Queue ID |
reason |
Option<String> | Leave reason |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
queue_id |
String | Queue ID |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
original_queue_id |
String | Original queue ID |
overflow_queue_id |
String | Overflow target queue ID |
reason |
String | Overflow reason |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
queue_id |
String | Queue ID |
reason |
String | Reason |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
queue_id |
String | Queue ID |
candidates |
Vec<String> | Candidate agent list |
trace_id |
String | ACD trace ID |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
queue_id |
String | Queue ID |
agent_id |
String | Agent ID |
attempt |
u32 | no_answer/rejected only: attempt number |
trace_id |
String | ACD trace ID |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
queue_id |
String | Queue ID |
action |
String | Fallback action executed |
reason |
String | Reason |
trace_id |
String | ACD trace ID |
| +ctx | Flat context fields |
Dispatch: broadcast (no call_id)
| Field | Type | Description |
|---|---|---|
queue_id |
String | Queue ID |
alert_type |
String | Alert type |
message |
String | Alert message |
Dispatch: broadcast
Emitted when the ACD scheduler finds candidate agents for a skill group.
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
skill_group_id |
Option<String> | Skill group ID (Some for the explicit skill-group:{id} path; None for autonomous skill routing) |
candidates |
Vec<String> | Candidate agent ID list |
trace_id |
String | Trace ID |
Dispatch: broadcast
Emitted when the ACD scheduler decides to assign an agent to the call (an ACD Assign decision or the first agent selected by the strategy).
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
skill_group_id |
Option<String> | Skill group ID |
agent_id |
String | Assigned agent ID |
trace_id |
String | Trace ID |
Dispatch: broadcast
Emitted when the ACD scheduler cannot provide an agent for the skill group.
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
skill_group_id |
Option<String> | Skill group ID |
reason |
String | Reason (no_candidates no matching agent / acd_blocked blocked by ACD policy / no_strategy_match strategy picked none) |
Dispatch: broadcast
Agent state machine transition.
| Field | Type | Description |
|---|---|---|
agent_id |
String | Agent ID |
from_status |
String | Previous status |
to_status |
String | New status |
call_id |
Option<String> | Associated call ID |
agent_name |
Option<String> | Agent display name |
agent_extension |
Option<String> | Agent extension number |
caller |
Option<String> | Caller / directory number |
team_id |
Option<String> | Team ID |
duration_secs |
Option<u32> | Duration in previous status |
reason_code |
Option<String> | Reason code (e.g., CALL, BREAK, TRAINING) |
Agent status values:
| Status | Description | Can transition to |
|---|---|---|
offline |
Disconnected | idle, away, dnd |
idle |
Ready to accept calls | ringing, away, dnd, offline |
away |
Online but not accepting (break) | idle, dnd, offline |
dnd |
Do not disturb (meeting/training) | idle, away, offline |
ringing |
Ringing (call_id present) | busy (answer), idle (no answer) |
busy |
On a call (call_id present) | wrapup |
wrapup |
After-call work | idle, away, dnd |
custom:<name> |
Custom status | idle, 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"
}
}Dispatch: broadcast
Granular extension-level signaling events.
| Field | Type | Description |
|---|---|---|
caller |
String | Extension / caller |
event_name |
String | Event name (see table below) |
system_time |
String | System timestamp |
call_id |
Option<String> | Associated call ID |
agent_id |
Option<String> | Agent ID |
caller_name |
Option<String> | Calling party name/number |
callee_name |
Option<String> | Called party name/number |
reason_code |
Option<String> | Reason code |
agent_work_mode |
Option<String> | Agent work mode |
releasing_party |
Option<String> | Releasing party ("1 Local" / "2 Remote") |
vq_name |
Option<String> | Virtual queue name |
routing_target |
Option<String> | Routing target |
skill_group |
Option<String> | Skill group |
extra |
Option<Map<String, Value>> | Extension fields (omitted when absent) |
event_name values:
| event_name | Description | Trigger |
|---|---|---|
REGISTERED |
Extension registered | SIP REGISTER success |
DIALING |
Outbound dialing | Agent outbound or manual dial |
RINGING |
Ringing | Agent-side ringing |
ESTABLISHED |
Call established | Call answered |
RELEASED |
Released | Hangup or transfer completed |
ABANDONED |
Abandoned | Caller abandoned during ringing |
HELD |
Held | Agent held, user hears music |
RETRIEVED |
Retrieved from hold | Held party retrieved |
PARTYCHANGED |
Multi-party state changed | Conference state changed |
PARTYADDED |
Multi-party added | Party added to conference |
PARTYDELETED |
Multi-party removed | Party removed from conference |
AGENTLOGIN |
Agent login | Agent went from offline to online (CC addon) |
AGENTLOGOUT |
Agent logout | Agent went from online to offline (CC addon) |
AGENTREADY |
Agent ready | Agent entered idle state (CC addon) |
AGENTNOTREADY |
Agent not ready | Agent entered busy/ringing/wrapup etc. (CC addon) |
ONHOOK |
On hook | Phone on hook |
Note: Use
event_namefor 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" }
}
}
}Dispatch: broadcast
| Field | Type | Description |
|---|---|---|
caller |
String | Extension number |
agent_id |
Option<String> | Agent ID |
register_time / unregister_time |
String | Registration/unregistration timestamp |
Triggered when call metadata is updated after initial call_incoming.
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
metadata |
CallMetadata | Metadata (see below) |
CallMetadata fields:
| Field | Type | Description |
|---|---|---|
root_call_id |
Option<String> | Root call ID |
caller_name |
Option<String> | Calling party number |
callee_name |
Option<String> | Dialed number |
called_phone |
Option<String> | Actual called number |
dial_direction |
Option<String> | Call direction |
uuid |
Option<String> | Global UUID |
routing_path |
Option<Vec<String>> | Routing path |
app_id |
Option<String> | IVR application ID |
routing_target |
Option<String> | Routing target |
switch_name |
Option<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"
}
}
}Dispatch: broadcast
| Field | Type | Description |
|---|---|---|
conf_id |
String | Conference room ID |
conference_member_joined / conference_member_left / conference_member_muted / conference_member_unmuted
Dispatch: broadcast
| Field | Type | Description |
|---|---|---|
conf_id |
String | Conference ID |
call_id |
String | Member call ID |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
conf_id |
String | Conference ID |
host_call_id |
String | Host call ID |
removed_call_ids |
Vec<String> | Removed member call IDs |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
conf_id |
String | Conference ID |
reason |
String | End reason |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
conf_id |
String | Conference ID |
error |
String | Error message |
| Field | Type | Description |
|---|---|---|
call_id |
String | Consultation call ID |
target |
String | Consultation target |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
call_id |
String | Call ID (merge_requested includes consultation_call_id) |
conf_id |
String | Conference ID (merged/merge_failed) |
consultation_call_id |
String | merge_requested only: consultation call ID |
reason |
String | merge_failed only: failure reason |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
conf_id |
String | Conference ID |
old_call_id |
String | Old member call ID |
new_call_id |
String | New member call ID |
reason |
String | failed/rollback_failed only: failure reason |
Seat replacement event sequence (success path):
conference_seat_replace_startedconference_member_left(old member leaves)conference_member_joined(new member joins)conference_seat_replace_succeeded
supervisor_listen_started / supervisor_whisper_started / supervisor_barge_started / supervisor_takeover_started
| Field | Type | Description |
|---|---|---|
supervisor_call_id |
String | Supervisor call ID |
target_call_id |
String | Target call ID |
| Field | Type | Description |
|---|---|---|
supervisor_call_id |
String | Supervisor call ID |
target_call_id |
String | Target call ID |
| Field | Type | Description |
|---|---|---|
operation_id |
String | Operation ID |
leg_count |
u32 | Number of parallel legs |
| Field | Type | Description |
|---|---|---|
operation_id |
String | Operation ID |
call_id |
String | Leg call ID |
destination |
String | Dialed destination |
reason |
String | leg_cancelled only: cancellation reason |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
operation_id |
String | Operation ID |
winning_call_id |
String | Winning call ID |
| Field | Type | Description |
|---|---|---|
operation_id |
String | Operation ID |
reason |
String | Failure reason |
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
content_type |
String | Content type |
body |
String | Message body |
event |
String | sip_notify_received only: SIP Event header |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
call_id |
String | Call identifier |
session_id |
String | Taking-over session ID |
mode |
String | Mode (control/listen/whisper/barge) |
| +ctx | Flat context fields |
| Field | Type | Description |
|---|---|---|
session_id |
String | Resumed session ID |
last_sequence |
u64 | Client-reported last sequence number |
| Event Type | Dispatch | call_id | Context |
|---|---|---|---|
call_incoming |
fan_out | yes | own fields |
call_ringing |
owner | yes | +ctx |
call_early_media |
owner | yes | +ctx |
call_answered |
owner | yes | +ctx |
call_bridged |
owner | leg_a | — |
call_unbridged |
owner | yes | +ctx |
call_transferred |
owner | yes | +ctx |
call_transfer_accepted |
owner | yes | +ctx |
call_transfer_failed |
owner | yes | +ctx |
call_hangup |
owner | yes | +ctx |
call_no_answer |
owner | yes | +ctx |
call_busy |
owner | yes | +ctx |
media_hold_started |
owner | yes | +ctx |
media_hold_stopped |
owner | yes | +ctx |
media_ringback_passthrough_started |
owner | yes | — |
media_ringback_passthrough_stopped |
owner | yes | — |
media_play_started |
owner | yes | +ctx |
media_play_finished |
owner | yes | +ctx |
media_stream_started |
owner | yes | +ctx |
media_stream_stopped |
owner | yes | +ctx |
record_started |
owner | yes | +ctx |
record_paused |
owner | yes | +ctx |
record_resumed |
owner | yes | +ctx |
record_stopped |
owner | yes | own fields + enrich |
record_failed |
owner | yes | +ctx |
recording_metadata_available |
owner | yes | — |
dtmf |
fan_out | yes | +ctx |
dtmf_collected |
owner | yes | +ctx |
dtmf_collection_timeout |
owner | yes | +ctx |
ivr_node_entered |
fan_out | yes | +ctx |
ivr_node_exited |
fan_out | yes | +ctx |
ivr_flow_transitioned |
fan_out | yes | +ctx |
ivr_flow_completed |
fan_out | yes | +ctx |
ivr_step_trace |
fan_out | yes | — |
queue_joined |
owner/broadcast | yes | +ctx |
queue_position_changed |
owner | yes | +ctx |
queue_agent_offered |
broadcast | yes | +ctx |
queue_agent_connected |
owner | yes | +ctx |
queue_left |
broadcast | yes | +ctx |
queue_wait_timeout |
owner | yes | +ctx |
queue_overflowed |
owner | yes | +ctx |
queue_voicemail_redirected |
owner | yes | +ctx |
queue_candidates_found |
owner | yes | +ctx |
queue_agent_ringing |
owner | yes | +ctx |
queue_agent_no_answer |
owner | yes | +ctx |
queue_agent_rejected |
owner | yes | +ctx |
queue_fallback_executed |
owner | yes | +ctx |
queue_alert |
broadcast | — | — |
skill_group_candidates_found |
broadcast | yes | — |
skill_group_agent_assigned |
broadcast | yes | — |
skill_group_no_agent |
broadcast | yes | — |
agent_state_changed |
broadcast | optional | — |
dn_state_changed |
broadcast | optional | — |
dn_registered |
broadcast | — | — |
dn_unregistered |
broadcast | — | — |
call_metadata_updated |
owner | yes | — |
conference_created |
broadcast | — | — |
conference_member_joined |
broadcast | yes | +ctx |
conference_member_left |
broadcast | yes | +ctx |
conference_member_muted |
broadcast | yes | +ctx |
conference_member_unmuted |
broadcast | yes | +ctx |
conference_destroyed |
broadcast | — | — |
conference_ended_by_host |
broadcast | — | +ctx |
conference_auto_ended |
broadcast | — | +ctx |
conference_error |
broadcast | — | — |
conference_consult_dialing |
owner | yes | +ctx |
conference_consult_connected |
owner | yes | +ctx |
conference_merge_requested |
fan_out | yes | +ctx |
conference_merged |
fan_out | yes | +ctx |
conference_merge_failed |
fan_out | yes | +ctx |
conference_seat_replace_started |
fan_out | yes | — |
conference_seat_replace_succeeded |
fan_out | yes | — |
conference_seat_replace_failed |
fan_out | yes | — |
conference_seat_replace_rollback_failed |
fan_out | yes | — |
supervisor_listen_started |
owner | — | — |
supervisor_whisper_started |
owner | — | — |
supervisor_barge_started |
owner | — | — |
supervisor_takeover_started |
owner | — | — |
supervisor_mode_stopped |
owner | — | — |
parallel_originate_started |
owner | — | — |
parallel_originate_leg_ringing |
owner | yes | +ctx |
parallel_originate_winner |
owner | yes | +ctx |
parallel_originate_leg_cancelled |
owner | yes | +ctx |
parallel_originate_completed |
owner | yes | — |
parallel_originate_failed |
owner | — | — |
sip_message_received |
owner | yes | +ctx |
sip_notify_received |
owner | yes | +ctx |
call_ownership_changed |
owner | yes | +ctx |
session_resumed |
owner | — | — |
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()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())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);
}
};These structs are used as nested references and are not emitted as standalone events.
| Field | Type | Description |
|---|---|---|
node_id |
String | Node ID |
node_name |
String | Node name |
node_type |
String | Node type |
routing_target |
Option<String> | Routing target |
previous_node_id |
Option<String> | Previous node ID |
next_node_id |
Option<String> | Next node ID |
duration_ms |
Option<u32> | Dwell time |
result_value |
Option<String> | DTMF/result |
| Field | Type | Description |
|---|---|---|
app_id |
String | IVR application ID |
routing_path |
Vec<String> | Routing path |
service_type |
Option<String> | Service type |
customer_type |
Option<String> | Customer type |
Document version: v1.0
Last updated: 2026-06-23
Source code: src/rwi/proto.rs