WebSocket Reference
This page is the message-level reference for the Vodia PBX WebSocket protocol. This page assumes you have already established a session and opened the socket.
The reference is organised as follows:
- Message envelope conventions — how messages are framed and discriminated
- Quick reference — every message type at a glance
- Bootstrap sequence — what the client must send on connect
- Outbound call timeline — annotated frame-by-frame
- Inbound call timeline — annotated frame-by-frame
- Message details — schema and real example for every message
- Field reference — deep-dive on the largest payloads (
invitesdp,call-state.calls[i]) - Reconnection — handling disconnects
Message envelope conventions
Every WebSocket frame is a UTF-8 JSON string. There are no binary frames. Two discriminator styles coexist:
Action-discriminated messages have a top-level action field naming the operation:
{"action": "blf", "add": ["500", "502"]}
This is the dominant style. Both directions use it, and the action name selects the rest of the schema. All client→server messages use this style; most server→client messages do too.
Flat-field messages have no action field. Instead, one of a small set of reserved keys is present at the top level, and the key itself names the message type. They are only sent by the server, and they all relate to WebRTC/SIP signalling:
{"invitesdp": "v=0\r\no=- ...", "callid": "abc@pbx", "from": "...", ...}
The reserved flat-field keys are: sdp, invitesdp, candidate, bye, ringtones, response, cancel, cancelresponse, info, change. A client implementation typically checks for these keys first, then falls through to switching on action.
The official websocket.js module follows this exact precedence order:
if (msg.sdp) { /* handle SDP */ }
else if (msg.invitesdp) { /* handle inbound INVITE */ }
else if (msg.candidate) { /* handle ICE candidate */ }
else if (msg.bye) { /* handle hangup */ }
// ...
else switch (msg.action) { /* handle action messages */ }
Quick reference
Client → server
| Action | Purpose | Group |
|---|---|---|
own-calls | Subscribe to own call-state stream | Bootstrap |
orbit-calls | Subscribe to call-park orbit updates | Bootstrap |
domain-calls | Subscribe to tenant-wide call state | Bootstrap |
ringtones | Request the user's ringtone set | Bootstrap |
sip-register | Register browser as a SIP endpoint | Bootstrap |
blf | Subscribe / unsubscribe BLF for extensions | Monitoring |
sdp-packet | Send WebRTC SDP offer (outbound call) | Outbound call |
ice-candidate | Send ICE candidate | Both directions |
sip-ack | Send SIP ACK | Outbound call |
sip-ringing | Send SIP 180 Ringing | Inbound call |
sdp-200ok | Send SIP 200 OK with SDP answer (accept inbound) | Inbound call |
sip-bye | Hang up call | In-call control |
bye-response | Acknowledge a remote BYE | In-call control |
mute | Mute / unmute call | In-call control |
wrtc-hold | Hold / resume call | In-call control |
rec-call | Start / stop call recording | In-call control |
Server → client (action-discriminated)
| Action | Purpose | Group |
|---|---|---|
sip-register | Confirms SIP registration | Bootstrap ack |
new-vm | New voicemail count | Counter |
missed-calls | Missed call counter update | Counter |
chats | Unread chat counter update | Counter |
blf | BLF state update for a subscribed extension | Monitoring |
call-state | Full call object for all visible calls | Monitoring |
user-change | User settings or state changed (refresh hint) | Monitoring |
stats-change | Statistics changed (refresh hint) | Monitoring |
activity | Activity-feed update | Monitoring |
chat | New chat / SMS thread notification | Messaging |
callerid | Caller ID for an active call | Per-call |
rec-start | Recording started | Per-call |
rec-stop | Recording stopped | Per-call |
alert | System alert | System |
Server → client (flat-field)
| Top-level key | Purpose | Group |
|---|---|---|
sdp | WebRTC SDP from server (e.g. 183, 200 OK) | Outbound call |
invitesdp | Inbound SIP INVITE with SDP | Inbound call |
candidate | Server ICE candidate | Both directions |
bye | Remote hangup | In-call control |
ringtones | Initial ringtone list | Bootstrap ack |
Bootstrap sequence
Immediately after onopen fires, the official web client sends six messages in this exact order. Most of them are subscriptions that prime the server to push state for this session. A custom client should send the same sequence — or at least the subset relevant to its use case — before the server starts pushing useful state.
↑ {"action":"own-calls","subscribe":true}
↑ {"action":"orbit-calls","subscribe":true}
↑ {"action":"blf","add":["<own-username>"]}
↑ {"action":"ringtones"}
↑ {"action":"domain-calls","subscribe":true}
↑ {"action":"sip-register","useragent":"<UA string>"}
Within ~300ms the server replies with the initial state for each subscription:
↓ {"action":"blf","user":"<own-username>","calls":[]}
↓ {"action":"blf","user":"<own-username>","dnd":false}
↓ {"action":"blf","user":"<own-username>","chat":true}
↓ {"action":"blf","user":"<own-username>","mwi":false}
↓ {"ringtones":["/img/ringer1.wav","/img/ringer2.wav", ...]}
↓ {"action":"call-state","time":...,"calls":[]}
↓ {"action":"sip-register","add":"wrtc-52950","expires":3600}
↓ {"action":"new-vm","count":0}
↓ {"action":"missed-calls","count":0,"reload":false}
↓ {"action":"chats","count":0}
The sip-register server reply contains the add field (registration ID, e.g. "wrtc-52950") and expires in seconds. The client should refresh the registration before expires elapses by sending sip-register again. The official client refreshes at expires/2 or expires - 15, whichever is larger.
Following bootstrap, the client typically issues blf add:[<extension>,...] for any other extensions it wants to monitor.
Outbound call timeline
The frames below are taken from a real outbound call from extension 501 to +15555550199. Times are relative to the first frame, in seconds. Phone numbers and SDP material have been sanitised.
0.000 ↑ {"action":"sdp-packet","callid":"6f273940@app","to":"+15555550199",
"sdp":{"sdp":"v=0\r\n...","type":"offer"}}
0.060 ↑ {"action":"ice-candidate","callid":"6f273940@app",
"candidate":{"candidate":"candidate:1566297096 1 udp 2122260223 192.168.1.10 54377 typ host ...",
"sdpMid":"0","sdpMLineIndex":0,"usernameFragment":"dLkT"}}
0.062 ↑ {"action":"ice-candidate","callid":"6f273940@app","candidate":{...}}
0.150 ↓ {"sdp":"v=0\r\n...","code":183,"cseq":"1","callid":"6f273940@app",
"from":"<sip:501@pbx.example.com>;tag=xznau4","from-user":"501","from-display":"",
"to":"<sip:+15555550199@pbx.example.com>;tag=d7aff443db","to-user":"+15555550199","to-display":"",
"video":"false"}
0.180 ↓ {"candidate":"candidate:401712 1 udp 394836 203.0.113.10 62650 typ host ...",
"callid":"6f273940@app",
"adr":[["10.0.0.6",62650],["203.0.113.10",62650]]}
0.200 ↓ {"action":"callerid","callid":"6f273940@app",
"assertedid":"\"Bob Smith\" <sip:5550199@pbx.example.com>",
"name":"Bob Smith","number":"5550199"}
1.000 ↓ {"action":"call-state","time":...,"calls":[
{"id":10,"type":"extcall","from-number":"501","to-number":"+15555550199",
"account":"501","state":"alerting", ...}]}
5.500 ↓ {"action":"call-state","time":...,"calls":[
{"id":10,...,"state":"connected","connect":"...","rec":true}]}
5.510 ↑ {"action":"sip-ack","callid":"6f273940@app"}
7.200 ↑ {"action":"rec-call","id":"6f273940@app","startstop":"on"}
7.250 ↓ {"action":"rec-start","callid":"6f273940@app"}
12.000 ↑ {"action":"mute","callid":"6f273940@app","muted":true}
14.500 ↑ {"action":"mute","callid":"6f273940@app","muted":false}
20.000 ↑ {"action":"wrtc-hold","callid":"6f273940@app","holdcmd":"sendonly"}
20.150 ↓ {"action":"call-state","time":...,"calls":[{"id":10,...,"state":"connected"}]}
35.000 ↑ {"action":"rec-call","id":"6f273940@app","startstop":"off"}
35.150 ↓ {"action":"rec-stop","callid":"6f273940@app"}
42.000 ↑ {"action":"sip-bye","callid":"6f273940@app"}
42.150 ↓ {"action":"call-state","time":...,"calls":[]}
Notes:
- The client creates the
callidlocally (e.g. random hex +@app). All call-scoped messages reference the same callid for the lifetime of the call. sdp-packetcarries an SDP offer. The server's flat-fieldsdpreply is the SDP answer wrapped in a SIP envelope (thecodefield is the SIP status code:183for Session Progress,200for OK).ice-candidateis trickled — the client sends as many as it gathers; the server replies with its own candidates as separate flat-fieldcandidatemessages.call-statetransitions throughalerting→connected→ (other transitional states likeconfirmed,early) → empty array on hangup.- The recording flag (
recin the call object) is independent of explicitrec-callrequests — the PBX may auto-record per policy.rec-start/rec-stopserver pushes signal the actual state change.
Inbound call timeline
A real inbound queue call from +15555550199 into queue 400, which rang extensions 501 and 502 simultaneously. Extension 501's client picked up.
0.000 ↓ {"action":"call-state","time":...,"calls":[
{"id":13,"type":"acd","from-number":"+15555550199","to-number":"+15555550100",
"account":"400","state":"alerting","extension":["501","502"],
"trunk":["Main Trunk*"], ...}]}
0.007 ↓ {"invitesdp":"v=0\r\n...","callid":"7swcwut@pbx","cseq":"5454",
"from":"\"5550199 (0050)\" <sip:0050@pbx.example.com>;tag=584406335",
"from-user":"0050","from-display":"5550199 (0050)",
"to":"\"Main Trunk\" <sip:+15555550100@pbx.example.com>",
"to-user":"+15555550100","to-display":"Main Trunk",
"assertedid":"","asserted-user":"","asserted-display":"",
"spamscore":"adrbook","alertinfo":"/img/ringer2.wav",
"diversion":"","group":"400","video":"false"}
0.023 ↑ {"action":"sip-ringing","callid":"7swcwut@pbx","cseq":"5454"}
0.162 ↓ {"candidate":"candidate:... 203.0.113.10 62930 ...","callid":"7swcwut@pbx"}
0.162 ↓ {"action":"user-change","extensions":"501"}
0.162 ↓ {"action":"blf","user":"501","calls":[{"state":"early"}]}
# User clicks Accept (12s of ringback shown to caller)
12.085 ↑ {"action":"sdp-200ok","callid":"7swcwut@pbx","cseq":"5454",
"sdp":{"sdp":"v=0\r\n...","type":"answer"}}
12.086 ↑ {"action":"ice-candidate","callid":"7swcwut@pbx","candidate":{...}}
12.205 ↑ {"action":"ice-candidate","callid":"7swcwut@pbx","candidate":{...}}
14.250 ↓ {"action":"rec-start","callid":"7swcwut@pbx"}
14.420 ↓ {"action":"blf","user":"501","calls":[{"state":"confirmed"}]}
# Call is now connected; conversation continues...
19.200 ↑ {"action":"sip-bye","callid":"7swcwut@pbx"}
19.370 ↓ {"action":"call-state","time":...,"calls":[]}
19.370 ↓ {"action":"missed-calls","count":...,"reload":false}
Notes:
- The full SIP INVITE arrives in a single
invitesdpmessage with rich routing metadata (group,diversion,alertinfo,spamscore). See Field reference: invitesdp. - The
call-stateentry hastype: "acd"because this was a queue call. Both ringing endpoints appear inextension: ["501", "502"]. A direct extension-to-extension call would havetype: "extcall". sip-ringingis sent immediately on receiving theinvitesdpto trigger ringback on the caller side. Delay it and the caller hears silence.sdp-200okaccepts the call. The innersdpfield is an object with{type: "answer", sdp: "..."}— not a bare string. This matches whatRTCPeerConnection.createAnswer()produces.
Message details
Each entry below documents one message type. Schemas describe the JSON payload; examples are real frames from test captures, sanitised.
Client → server messages
own-calls
Subscribe to call-state events for the current user. Sent during bootstrap.
{
"action": "own-calls",
"subscribe": true
}
Fields:
action(string, required):"own-calls".subscribe(boolean, required):trueto subscribe.
orbit-calls
Subscribe to call-park orbit updates. Sent during bootstrap.
{
"action": "orbit-calls",
"subscribe": true
}
Fields:
action(string, required):"orbit-calls".subscribe(boolean, required):trueto subscribe.
domain-calls
Subscribe to tenant-wide call state. This is what gives a monitoring or wallboard client visibility into other extensions' calls. Without this subscription, call-state only carries the current user's own calls.
{
"action": "domain-calls",
"subscribe": true
}
Fields:
action(string, required):"domain-calls".subscribe(boolean, required):trueto subscribe.
ringtones (send)
Request the list of ringtone URLs the user has configured. The server replies with a flat-field ringtones message.
{
"action": "ringtones"
}
Fields:
action(string, required):"ringtones".
sip-register (send)
Register the browser as a SIP endpoint (i.e. start the web softphone). After registration the user can place outbound WebRTC calls and receive inbound calls via the same WebSocket.
{
"action": "sip-register",
"useragent": "Chrome/537.36 Macintosh/70.3.beta"
}
Fields:
action(string, required):"sip-register".useragent(string, required): User-Agent string the PBX should record for this registration.
The server replies with {"action": "sip-register", "add": "wrtc-NNNNN", "expires": 3600}. The client must re-send sip-register before expires seconds elapse to keep the registration alive.
blf (send)
Subscribe or unsubscribe to BLF (busy-lamp-field) updates for one or more extensions. Used to drive BLF lamps, presence indicators, console monitoring grids, and any UI that shows the state of other extensions.
Subscribe form:
{
"action": "blf",
"add": ["500", "502", "503", "504"]
}
Unsubscribe form:
{
"action": "blf",
"sub": ["503", "504"]
}
Fields:
action(string, required):"blf".add(array of strings, optional): Extension usernames to start watching.sub(array of strings, optional): Extension usernames to stop watching.
Exactly one of add or sub should be present per message. After subscribing, the server pushes a blf message for each watched extension immediately (initial state) and again whenever that extension's state changes.
sdp-packet
Send an SDP offer to initiate an outbound WebRTC call. This is the first message in the outbound call flow.
{
"action": "sdp-packet",
"callid": "6f273940@app",
"to": "+15555550199",
"sdp": {
"sdp": "v=0\r\no=- ... ",
"type": "offer"
}
}
Fields:
action(string, required):"sdp-packet".callid(string, required): Client-generated call ID. Convention: 8 hex chars +@app.to(string, required): Destination extension or E.164 phone number.sdp(object, required): SDP offer object as produced byRTCPeerConnection.createOffer()—{type: "offer", sdp: "<sdp-string>"}.
ice-candidate
Send a WebRTC ICE candidate to the server. Trickled — send each candidate as soon as RTCPeerConnection.onicecandidate fires.
{
"action": "ice-candidate",
"callid": "6f273940@app",
"candidate": {
"candidate": "candidate:1566297096 1 udp 2122260223 192.168.1.10 54377 typ host generation 0 ufrag dLkT network-id 1 network-cost 10",
"sdpMid": "0",
"sdpMLineIndex": 0,
"usernameFragment": "dLkT"
}
}
Fields:
action(string, required):"ice-candidate".callid(string, required): Call ID this candidate belongs to.candidate(object, required): StandardRTCIceCandidateInitobject.
sip-ack
Send a SIP ACK to confirm receipt of a 200 OK during outbound call setup. Sent after the server's call-state flips to connected.
{
"action": "sip-ack",
"callid": "6f273940@app"
}
Fields:
action(string, required):"sip-ack".callid(string, required): Call ID.
sip-ringing
Send a SIP 180 Ringing to indicate the inbound call is being processed. Sent immediately on receiving an invitesdp message, before showing the accept/reject UI to the user.
{
"action": "sip-ringing",
"callid": "7swcwut@pbx",
"cseq": "5454"
}
Fields:
action(string, required):"sip-ringing".callid(string, required): Call ID from the inboundinvitesdp.cseq(string, required): CSeq from the inboundinvitesdp.
sdp-200ok
Accept an inbound call by sending a SIP 200 OK with SDP answer. Sent after the user clicks Accept on the inbound-call UI.
{
"action": "sdp-200ok",
"callid": "7swcwut@pbx",
"cseq": "5454",
"sdp": {
"sdp": "v=0\r\no=- ...",
"type": "answer"
}
}
Fields:
action(string, required):"sdp-200ok".callid(string, required): Call ID from the inboundinvitesdp.cseq(string, required): CSeq from the inboundinvitesdp.sdp(object, required): SDP answer object as produced byRTCPeerConnection.createAnswer()—{type: "answer", sdp: "<sdp-string>"}.
sip-bye
Hang up an active call (either direction). Sent when the user clicks Hang Up.
{
"action": "sip-bye",
"callid": "6f273940@app"
}
Fields:
action(string, required):"sip-bye".callid(string, required): Call ID to terminate.
bye-response
Acknowledge a remote BYE (i.e. the other side hung up). Sent in response to a flat-field bye push from the server.
{
"action": "bye-response",
"callid": "bfa38460@app",
"cseq": "8201"
}
Fields:
action(string, required):"bye-response".callid(string, required): Call ID from the inboundbye.cseq(string, required): CSeq from the inboundbye.
mute
Mute or unmute an active call. The PBX records the mute state for the call; the local browser is still responsible for actually stopping its audio track.
{
"action": "mute",
"callid": "6f273940@app",
"muted": true
}
Fields:
action(string, required):"mute".callid(string, required): Call ID.muted(boolean, required):trueto mute,falseto unmute.
wrtc-hold
Hold or resume a WebRTC call by sending a renegotiated SDP with the appropriate direction attribute.
{
"action": "wrtc-hold",
"callid": "6f273940@app",
"holdcmd": "sendonly"
}
Fields:
action(string, required):"wrtc-hold".callid(string, required): Call ID.holdcmd(string, required):"sendonly"to hold,"sendrecv"to resume.
rec-call
Start or stop recording the call.
{
"action": "rec-call",
"id": "6f273940@app",
"startstop": "on"
}
Fields:
action(string, required):"rec-call".id(string, required): Call ID. Note: this field is namedid, notcallid, in contrast to other call-scoped messages.startstop(string, required):"on"to start,"off"to stop.
The server confirms with a rec-start or rec-stop push.
Server → client messages (action-discriminated)
sip-register (recv)
Confirms a SIP registration. Sent in reply to the client's sip-register and re-sent on registration refresh.
{
"action": "sip-register",
"add": "wrtc-52950",
"expires": 3600
}
Fields:
action(string):"sip-register".add(string): Registration ID assigned by the PBX. Used internally; clients usually do not need to inspect it.expires(integer): Registration lifetime in seconds. Refresh before this elapses.
new-vm
New voicemail count. Sent during bootstrap and whenever the count changes.
{
"action": "new-vm",
"count": 3,
"mailbox": "501"
}
Fields:
action(string):"new-vm".count(integer): Total new voicemail messages.mailbox(string, optional): Mailbox identifier. Omitted when only the current user's mailbox is involved.
If the user has access to multiple mailboxes (shared mailbox, supervisor, etc.), a separate new-vm is pushed per mailbox and the client should sum the counts.
missed-calls
Missed-call counter update.
{
"action": "missed-calls",
"count": 0,
"reload": false
}
Fields:
action(string):"missed-calls".count(integer): Missed-call count.reload(boolean):trueif the client should re-fetch its call history (i.e. the change is non-trivial);falseif just updating the counter suffices.
chats
Unread-chat counter update.
{
"action": "chats",
"count": 0
}
Fields:
action(string):"chats".count(integer): Unread-chat count.
blf (recv)
BLF state update for a watched extension. Pushed initially after a blf add:[...] subscription and again whenever that extension's state changes. Each message contains one or more of the state fields (calls, dnd, mwi, chat) — not all at once.
{
"action": "blf",
"user": "500",
"calls": [{"state": "confirmed"}]
}
{
"action": "blf",
"user": "500",
"dnd": true
}
{
"action": "blf",
"user": "500",
"chat": false
}
Fields:
action(string):"blf".user(string): Extension username this update is for.calls(array, optional): Array of call summaries, each{state: "alerting"|"connected"|"early"|"confirmed"|...}. Empty array means no active calls.dnd(boolean, optional): Do-Not-Disturb state.mwi(boolean, optional): Message-Waiting-Indicator (voicemail).chat(boolean, optional): Chat-presence state (online / offline).
A BLF lamp UI should maintain a per-extension state object and merge these messages into it, since each message updates only one or two facets at a time.
call-state
Full call-state snapshot for all calls visible to this session. Visibility is determined by which subscribes were sent during bootstrap — own-calls shows the current user's calls, domain-calls shows all calls in the tenant.
Each call in calls[] is a rich object with SIP routing details, timing, and current state. See Field reference: call-state.calls[i] for the full field list.
{
"action": "call-state",
"time": 1779191325.629734,
"calls": [
{
"id": 10,
"type": "extcall",
"from": "\"Desk Phone\" <sip:501@pbx.example.com>",
"from-name": "Desk Phone",
"from-number": "501",
"to": "\"Bob Smith\" <sip:+15555550199@pbx.example.com>",
"to-name": "Bob Smith",
"to-number": "+15555550199",
"account": "501",
"start": "1779191324.630383",
"connect": "",
"domain": "pbx.example.com",
"rec": false,
"cmc": "",
"priority": "",
"trunk": ["Main Trunk"],
"extension": ["501*"],
"state": "alerting"
}
]
}
An empty calls: [] array means no active calls in the visibility scope. The server pushes a fresh snapshot on every state transition.
user-change
Notification that a user's settings or state have changed. The client should re-fetch the relevant data; the message itself only identifies which user(s) changed.
{
"action": "user-change",
"extensions": "501"
}
Fields:
action(string):"user-change".extensions(string): Space-separated list of affected extension usernames.
stats-change
Notification that statistics have changed for one or more extensions. Used by dashboards to know when to re-fetch counters.
{
"action": "stats-change",
"extensions": "501"
}
Fields:
action(string):"stats-change".extensions(string): Space-separated list of affected extension usernames.
activity
Activity-feed update. Used to invalidate or remove items from the activity feed.
{
"action": "activity",
"remove": 12135
}
Fields:
action(string):"activity".remove(integer, optional): ID of an activity-feed item to remove from the client's local cache.
chat (recv)
New chat/SMS thread notification. Pushed when a new message arrives in a thread — the client should re-fetch the thread via GET /rest/user/{account}/chat?number=... to retrieve the actual message content.
{
"action": "chat",
"dest": "+15555550199",
"add": "true"
}
Fields:
action(string):"chat".dest(string): Peer extension or E.164 number identifying the affected thread.add(string):"true"— present when a new message was added to the thread.
callerid
Caller ID for an active call. Sent when the PBX has fresh caller-ID information (e.g. after a contact-book lookup completes).
{
"action": "callerid",
"callid": "bfa38460@app",
"assertedid": "\"Bob Smith\" <sip:5550199@pbx.example.com>",
"name": "Bob Smith",
"number": "5550199"
}
Fields:
action(string):"callerid".callid(string): Call ID.assertedid(string): Full SIP P-Asserted-Identity header value.name(string): Display name extracted from the asserted identity.number(string): Number extracted from the asserted identity.
rec-start
Recording has started for a call. May be triggered by an explicit rec-call request or by automatic-recording policy.
{
"action": "rec-start",
"callid": "bfa38460@app"
}
Fields:
action(string):"rec-start".callid(string): Call ID.
rec-stop
Recording has stopped for a call.
{
"action": "rec-stop",
"callid": "6f273940@app"
}
Fields:
action(string):"rec-stop".callid(string): Call ID.
alert
System alert. The official client forwards the entire message to a pbx.alert custom event, so any additional top-level fields beyond action should be treated as opaque alert metadata.
{
"action": "alert"
}
Fields:
action(string):"alert".
Server → client messages (flat-field)
Field: sdp
WebRTC SDP from the server, wrapped in a SIP envelope. Sent in response to the client's sdp-packet outbound offer — typically twice per call: once as a 183 Session Progress (early media) and once as a 200 OK (call connected).
{
"sdp": "v=0\r\no=- ...",
"code": 183,
"cseq": "1",
"callid": "bfa38460@app",
"from": "<sip:501@pbx.example.com>;tag=xznau4",
"from-user": "501",
"from-display": "",
"to": "<sip:+15555550199@pbx.example.com>;tag=d7aff443db",
"to-user": "+15555550199",
"to-display": "",
"video": "false"
}
Fields:
sdp(string): SDP answer body.code(integer): SIP status code (183,200).cseq(string): SIP CSeq.callid(string): Call ID.from,from-user,from-display(string): SIP From header and parsed components.to,to-user,to-display(string): SIP To header and parsed components.video(string):"false"for audio-only,"true"if SDP includes video.
Apply the SDP via RTCPeerConnection.setRemoteDescription({type: "answer", sdp: msg.sdp}).
Field: invitesdp
Inbound SIP INVITE with SDP. This is the message that drives the inbound-call UI. See Field reference: invitesdp for the full field list including spam scoring, alert-info, diversion, and routing group.
{
"invitesdp": "v=0\r\no=- ...",
"callid": "7swcwut@pbx",
"cseq": "5454",
"from": "\"5550199 (0050)\" <sip:0050@pbx.example.com>;tag=584406335",
"from-user": "0050",
"from-display": "5550199 (0050)",
"to": "\"Main Trunk\" <sip:+15555550100@pbx.example.com>",
"to-user": "+15555550100",
"to-display": "Main Trunk",
"assertedid": "",
"asserted-user": "",
"asserted-display": "",
"spamscore": "adrbook",
"alertinfo": "/img/ringer2.wav",
"diversion": "",
"group": "400",
"video": "false"
}
Apply the SDP via RTCPeerConnection.setRemoteDescription({type: "offer", sdp: msg.invitesdp}). Reply with sip-ringing immediately, then sdp-200ok to accept the call.
Field: candidate
Server ICE candidate. Sent during call setup (both inbound and outbound).
{
"candidate": "candidate:401712 1 udp 394836 203.0.113.10 62650 typ host generation 0 ufrag 25iq\r\n",
"callid": "bfa38460@app",
"adr": [
["10.0.0.6", 62650],
["203.0.113.10", 62650],
["2001:db8::1", 62650]
]
}
Fields:
candidate(string): Standard ICE candidate line.callid(string): Call ID.adr(array of[ip, port]tuples): Additional reachable addresses the server is announcing. Convenience field — the same information may also appear inline incandidate.
Apply via RTCPeerConnection.addIceCandidate(new RTCIceCandidate({candidate: msg.candidate, sdpMid: "", sdpMLineIndex: 0})).
Field: bye
Remote hangup. The other side has terminated the call.
{
"bye": "true",
"callid": "bfa38460@app",
"cseq": "8201"
}
Fields:
bye(string):"true".callid(string): Call ID.cseq(string): SIP CSeq.
Reply with bye-response carrying the same callid and cseq. Tear down the local RTCPeerConnection.
Field: ringtones
Initial ringtone list. Sent in reply to the client's ringtones request during bootstrap.
{
"ringtones": [
"/img/ringer1.wav",
"/img/ringer2.wav",
"/img/ringer3.wav"
]
}
Fields:
ringtones(array of strings): Ringtone URLs relative to the PBX origin. The client should preload these for low-latency playback.
The alertinfo field in invitesdp references one of these paths to specify which ringtone to play for a given inbound call.
Field reference
call-state.calls[i]
Each call object in a call-state message has the following fields:
| Field | Type | Description |
|---|---|---|
id | integer | PBX-internal call ID. Distinct from the WebRTC callid used in signalling messages. |
type | string | Call category: "extcall" (extension-to-extension or outbound), "acd" (call queue), "attendant" (auto-attendant routed), and other internal types. |
from | string | Full SIP From URI with display name. |
from-name | string | Display-name portion of from. |
from-number | string | User-part of from. |
to | string | Full SIP To URI with display name. |
to-name | string | Display-name portion of to. |
to-number | string | User-part of to. |
account | string | Account this call is associated with (extension number, queue number, auto-attendant number, etc.). May be empty for some call types. |
start | string | Unix timestamp (seconds, with fractional part) when the call started. |
connect | string | Unix timestamp when the call connected. Empty string before connection. |
domain | string | Tenant hostname. |
rec | boolean | true if the call is currently being recorded. |
cmc | string | CMC (account code) entered by the caller. Empty if none. |
priority | string | Queue priority (for ACD calls). |
trunk | array of strings | Trunks involved. Names ending with * indicate the trunk that originated the call. |
extension | array of strings | Extensions ringing or connected. Names ending with * indicate the originating extension. Empty array possible during call transitions. |
state | string | Current state: "alerting", "connected", "early", "confirmed", and other transitional values. |
invitesdp
The inbound invitesdp message carries a full SIP INVITE worth of metadata in addition to the SDP body:
| Field | Type | Description |
|---|---|---|
invitesdp | string | SDP offer body. |
callid | string | Call ID. |
cseq | string | SIP CSeq. |
from | string | Full SIP From URI including display name and tag. |
from-user | string | User-part of from. |
from-display | string | Display-name portion of from. |
to | string | Full SIP To URI. |
to-user | string | User-part of to. |
to-display | string | Display-name portion of to. |
assertedid | string | Full P-Asserted-Identity header value. Empty if not present. |
asserted-user | string | User-part of assertedid. |
asserted-display | string | Display-name portion of assertedid. |
spamscore | string | PBX spam classification. Observed values: "adrbook" (caller is in user's address book — likely legitimate). |
alertinfo | string | Ringtone URL to play, matching one of the paths in the bootstrap ringtones list. |
diversion | string | SIP Diversion header value. Empty if not present. |
group | string | Routing group (queue number, auto-attendant number, etc.) that delivered this call. |
video | string | "true" if the offer includes video, "false" otherwise. |
Reconnection
When the WebSocket closes unexpectedly, the client should:
- Wait ~10 seconds before reconnecting (the official client's default).
- Verify the session is still valid via
GET /rest/system/session— if the response indicates no session, the session has expired and the third-party login flow must be redone. - If still authenticated, reopen the WebSocket and replay the bootstrap sequence.
The server does not require the client to resume by call ID — any in-progress calls are terminated when the WebSocket closes. The browser's RTCPeerConnection objects should be torn down at the same time.
A 10-second heartbeat timer in the client (independent of the socket) can detect silent disconnects (e.g. network failures that don't trigger onclose) by checking socket.readyState and forcing a reconnect when it isn't OPEN.