Zum Hauptinhalt springen

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

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

ActionPurposeGroup
own-callsSubscribe to own call-state streamBootstrap
orbit-callsSubscribe to call-park orbit updatesBootstrap
domain-callsSubscribe to tenant-wide call stateBootstrap
ringtonesRequest the user's ringtone setBootstrap
sip-registerRegister browser as a SIP endpointBootstrap
blfSubscribe / unsubscribe BLF for extensionsMonitoring
sdp-packetSend WebRTC SDP offer (outbound call)Outbound call
ice-candidateSend ICE candidateBoth directions
sip-ackSend SIP ACKOutbound call
sip-ringingSend SIP 180 RingingInbound call
sdp-200okSend SIP 200 OK with SDP answer (accept inbound)Inbound call
sip-byeHang up callIn-call control
bye-responseAcknowledge a remote BYEIn-call control
muteMute / unmute callIn-call control
wrtc-holdHold / resume callIn-call control
rec-callStart / stop call recordingIn-call control

Server → client (action-discriminated)

ActionPurposeGroup
sip-registerConfirms SIP registrationBootstrap ack
new-vmNew voicemail countCounter
missed-callsMissed call counter updateCounter
chatsUnread chat counter updateCounter
blfBLF state update for a subscribed extensionMonitoring
call-stateFull call object for all visible callsMonitoring
user-changeUser settings or state changed (refresh hint)Monitoring
stats-changeStatistics changed (refresh hint)Monitoring
activityActivity-feed updateMonitoring
chatNew chat / SMS thread notificationMessaging
calleridCaller ID for an active callPer-call
rec-startRecording startedPer-call
rec-stopRecording stoppedPer-call
alertSystem alertSystem

Server → client (flat-field)

Top-level keyPurposeGroup
sdpWebRTC SDP from server (e.g. 183, 200 OK)Outbound call
invitesdpInbound SIP INVITE with SDPInbound call
candidateServer ICE candidateBoth directions
byeRemote hangupIn-call control
ringtonesInitial ringtone listBootstrap 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 callid locally (e.g. random hex + @app). All call-scoped messages reference the same callid for the lifetime of the call.
  • sdp-packet carries an SDP offer. The server's flat-field sdp reply is the SDP answer wrapped in a SIP envelope (the code field is the SIP status code: 183 for Session Progress, 200 for OK).
  • ice-candidate is trickled — the client sends as many as it gathers; the server replies with its own candidates as separate flat-field candidate messages.
  • call-state transitions through alertingconnected → (other transitional states like confirmed, early) → empty array on hangup.
  • The recording flag (rec in the call object) is independent of explicit rec-call requests — the PBX may auto-record per policy. rec-start/rec-stop server 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 invitesdp message with rich routing metadata (group, diversion, alertinfo, spamscore). See Field reference: invitesdp.
  • The call-state entry has type: "acd" because this was a queue call. Both ringing endpoints appear in extension: ["501", "502"]. A direct extension-to-extension call would have type: "extcall".
  • sip-ringing is sent immediately on receiving the invitesdp to trigger ringback on the caller side. Delay it and the caller hears silence.
  • sdp-200ok accepts the call. The inner sdp field is an object with {type: "answer", sdp: "..."} — not a bare string. This matches what RTCPeerConnection.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): true to 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): true to 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): true to 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 by RTCPeerConnection.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): Standard RTCIceCandidateInit object.

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 inbound invitesdp.
  • cseq (string, required): CSeq from the inbound invitesdp.

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 inbound invitesdp.
  • cseq (string, required): CSeq from the inbound invitesdp.
  • sdp (object, required): SDP answer object as produced by RTCPeerConnection.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 inbound bye.
  • cseq (string, required): CSeq from the inbound bye.

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): true to mute, false to 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 named id, not callid, 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): true if the client should re-fetch its call history (i.e. the change is non-trivial); false if 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 bootstrapown-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 in candidate.

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:

FieldTypeDescription
idintegerPBX-internal call ID. Distinct from the WebRTC callid used in signalling messages.
typestringCall category: "extcall" (extension-to-extension or outbound), "acd" (call queue), "attendant" (auto-attendant routed), and other internal types.
fromstringFull SIP From URI with display name.
from-namestringDisplay-name portion of from.
from-numberstringUser-part of from.
tostringFull SIP To URI with display name.
to-namestringDisplay-name portion of to.
to-numberstringUser-part of to.
accountstringAccount this call is associated with (extension number, queue number, auto-attendant number, etc.). May be empty for some call types.
startstringUnix timestamp (seconds, with fractional part) when the call started.
connectstringUnix timestamp when the call connected. Empty string before connection.
domainstringTenant hostname.
recbooleantrue if the call is currently being recorded.
cmcstringCMC (account code) entered by the caller. Empty if none.
prioritystringQueue priority (for ACD calls).
trunkarray of stringsTrunks involved. Names ending with * indicate the trunk that originated the call.
extensionarray of stringsExtensions ringing or connected. Names ending with * indicate the originating extension. Empty array possible during call transitions.
statestringCurrent 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:

FieldTypeDescription
invitesdpstringSDP offer body.
callidstringCall ID.
cseqstringSIP CSeq.
fromstringFull SIP From URI including display name and tag.
from-userstringUser-part of from.
from-displaystringDisplay-name portion of from.
tostringFull SIP To URI.
to-userstringUser-part of to.
to-displaystringDisplay-name portion of to.
assertedidstringFull P-Asserted-Identity header value. Empty if not present.
asserted-userstringUser-part of assertedid.
asserted-displaystringDisplay-name portion of assertedid.
spamscorestringPBX spam classification. Observed values: "adrbook" (caller is in user's address book — likely legitimate).
alertinfostringRingtone URL to play, matching one of the paths in the bootstrap ringtones list.
diversionstringSIP Diversion header value. Empty if not present.
groupstringRouting group (queue number, auto-attendant number, etc.) that delivered this call.
videostring"true" if the offer includes video, "false" otherwise.

Reconnection

When the WebSocket closes unexpectedly, the client should:

  1. Wait ~10 seconds before reconnecting (the official client's default).
  2. 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.
  3. 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.