RWI Events & Webhooks
RWI (Real-time WebSocket Interface) is RustPBX’s real-time event bus. In addition to WebSocket real-time push, it also supports HTTP Webhooks to forward events to external systems.
1. RWI Architecture
RustPBX internal events
│
▼
RWI Gateway
│
├── WebSocket connection (real-time control)
│ ws://host:8080/rwi/ws
│ Sub-protocol: rwi-v1
│
└── HTTP Webhook (event forwarding)
POST https://your-server/webhook
2. Webhook Configuration
In config.toml:
[rwi_webhook]
url = "https://your-server.example.com/api/rwi/events"
timeout_ms = 5000
[rwi_webhook.headers]
Authorization = "Bearer my-webhook-secret"
X-Source = "rustpbx"
# Optional: event type filtering (leave empty or omit to forward all)
events = [
"call_hangup",
"call_answered",
"queue_joined",
"queue_agent_connected",
"agent_state_changed",
"dtmf",
"ivr_step_trace",
"record_stopped"
]
2.1 Configuration Parameters
| Parameter | Description | Default |
url | Webhook receiver endpoint (required) | - |
timeout_ms | HTTP request timeout | 5000 |
headers | Custom HTTP Headers | None |
events | Event type allowlist (empty = all) | All |
3. Webhook Payload Format
Each POST request body:
{
"rwi": "1.0",
"sequence": 12345,
"timestamp": "2026-05-26T10:30:00Z",
"call_id": "abc123@10.0.0.1",
"event_type": "call_hangup",
"event": {
"call_id": "abc123@10.0.0.1",
"reason": "normal",
"duration": 120,
"sip_code": 200
}
}
| Field | Description |
rwi | Protocol version |
sequence | Monotonically increasing sequence number |
timestamp | ISO 8601 timestamp |
call_id | Associated SIP Call-ID |
event_type | Event type (snake_case string) |
event | Event detail data (varies by type) |
4. Deduplication
Webhooks have built-in deduplication:
- Ring buffer of 4096
(call_id, sequence) records
- Events with the same
(call_id, sequence) are not sent twice
- Handles scenarios where the same event is forwarded from multiple call owners
5. Complete Event Type List
5.1 Call Lifecycle
| event_type | Description |
call_incoming | Incoming call arrived |
call_ringing | Ringing |
call_early_media | Early media |
call_answered | Answered |
call_bridged | Bridge established |
call_unbridged | Bridge disconnected |
call_hangup | Hung up |
call_no_answer | No answer |
call_busy | Busy |
call_transferred | Transfer completed |
call_transfer_accepted | Transfer accepted |
call_transfer_failed | Transfer failed |
call_ownership_changed | Call ownership changed |
call_metadata_updated | Call metadata updated |
5.2 Media
| event_type | Description |
media_hold_started | Hold started |
media_hold_stopped | Hold stopped |
media_play_started | Playback started |
media_play_finished | Playback finished |
media_stream_started | Media stream started |
media_stream_stopped | Media stream stopped |
dtmf | DTMF key press |
dtmf_collected | DTMF collection complete |
dtmf_collection_timeout | DTMF collection timeout |
5.3 Recording
| event_type | Description |
record_started | Recording started |
record_paused | Recording paused |
record_resumed | Recording resumed |
record_stopped | Recording stopped |
record_failed | Recording failed |
recording_metadata_available | Recording metadata ready |
5.4 Queue / ACD
| event_type | Description |
queue_joined | Joined queue |
queue_position_changed | Queue position changed |
queue_candidates_found | Candidate agents found |
queue_agent_offered | Offered to agent |
queue_agent_ringing | Agent ringing |
queue_agent_no_answer | Agent did not answer |
queue_agent_rejected | Agent rejected |
queue_agent_connected | Agent connected |
queue_left | Left queue |
queue_wait_timeout | Wait timeout |
queue_overflowed | Queue overflowed |
queue_voicemail_redirected | Redirected to voicemail |
queue_fallback_executed | Fallback executed |
queue_alert | Queue alert |
5.5 Agents
| event_type | Description |
agent_state_changed | Agent state changed |
5.6 Extensions
| event_type | Description |
dn_state_changed | Extension state changed |
dn_registered | Extension registered |
dn_unregistered | Extension unregistered |
5.7 IVR
| event_type | Description |
ivr_node_entered | Entered IVR node |
ivr_node_exited | Exited IVR node |
ivr_flow_transitioned | IVR flow transitioned |
ivr_flow_completed | IVR flow completed |
ivr_step_trace | Step IVR trace |
5.8 Conference
| event_type | Description |
conference_created | Conference created |
conference_member_joined | Member joined |
conference_member_left | Member left |
conference_member_muted | Member muted |
conference_member_unmuted | Member unmuted |
conference_destroyed | Conference destroyed |
conference_error | Conference error |
conference_consult_dialing | Consult dialing |
conference_consult_connected | Consult connected |
conference_merge_requested | Merge requested |
conference_merged | Merge completed |
conference_merge_failed | Merge failed |
conference_seat_replace_started | Seat replace started |
conference_seat_replace_succeeded | Seat replace succeeded |
conference_seat_replace_failed | Seat replace failed |
5.9 Supervisor
| event_type | Description |
supervisor_listen_started | Listen started |
supervisor_whisper_started | Whisper started |
supervisor_barge_started | Barge started |
supervisor_takeover_started | Takeover started |
supervisor_mode_stopped | Supervisor mode stopped |
5.10 Parallel Originate
| event_type | Description |
parallel_originate_started | Parallel originate started |
parallel_originate_leg_ringing | Originate leg ringing |
parallel_originate_winner | Winning leg |
parallel_originate_leg_cancelled | Leg cancelled |
parallel_originate_completed | Parallel originate completed |
parallel_originate_failed | Parallel originate failed |
5.11 SIP Signaling
| event_type | Description |
sip_message_received | SIP MESSAGE received |
sip_notify_received | SIP NOTIFY received |
5.12 Session Recovery
| event_type | Description |
session_resumed | Session resumed after WebSocket reconnect |
6. Webhook Receiver Examples
6.1 Python Flask
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/api/rwi/events", methods=["POST"])
def handle_rwi_event():
payload = request.json
event_type = payload.get("event_type")
call_id = payload.get("call_id")
event = payload.get("event", {})
if event_type == "call_hangup":
print(f"Call {call_id} ended, duration={event.get('duration')}s")
elif event_type == "queue_agent_connected":
print(f"Agent {event.get('agent_id')} connected to call {call_id}")
elif event_type == "agent_state_changed":
print(f"Agent {event.get('agent_id')} -> {event.get('state')}")
return jsonify({"status": "ok"})
6.2 Node.js Express
app.post("/api/rwi/events", express.json(), (req, res) => {
const { event_type, call_id, event, sequence } = req.body;
console.log(`[${sequence}] ${event_type} on ${call_id}`);
res.json({ status: "ok" });
});
7. WebSocket Real-time Connection (Supplement)
In addition to Webhooks, you can also subscribe to events in real-time via WebSocket:
const ws = new WebSocket("ws://rustpbx:8080/rwi/ws", "rwi-v1");
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
console.log(data.event_type, data.call_id);
};
7.1 Session Recovery
After a WebSocket disconnect and reconnect, sessions can be recovered via the last_sequence parameter:
- Gateway caches the latest 1000 events
- 60-second retention period
- Monotonically increasing sequence numbers
8. Typical Use Cases
| Scenario | Subscribed Events | Description |
| Real-time CDR push | call_hangup | Get call end events in real-time |
| Call center wallboard | queue_*, agent_state_changed | Real-time queue and agent status updates |
| IVR tracing | ivr_step_trace | Log each IVR decision step |
| Recording completion | record_stopped + recording_metadata_available | Process immediately after recording |
| Alert system | queue_alert, queue_overflowed | Queue anomaly alerts |
| CRM integration | call_answered | Screen pop with customer info |