Socket.IO
What the client emits. What the server emits. Who listens.
Agent panel and dashboard share one Socket.IO server on /socket.io/. This is the wire protocol only. Calling is in the Progressive and Predictive guides. Lead ingest is the HTTP API.
Connect
const socket = io('https://YOUR_HOST', {
transports: ['websocket'],
auth: { token: 'YOUR_WS_SECRET' }
});
socket.on('connect', () => {
socket.emit('join', { role: 'agent', extension: '1007' });
});
WS_SECRET is required. Send it as handshake.auth.token, or query token / key. Wrong key → connect_error unauthorized. After connect, emit join or you are in no room.
Rooms
| Room | Join as | Receives |
|---|---|---|
dashboard | { role: 'dashboard' } | Snapshot, patch, every dialer event, inbound waiting/offers, health, panel |
ext:{extension} | { role: 'agent', extension } | That agent’s status, dialer, call_connected, inbound ringing/answered/ended |
One socket is one role. Join again leaves the previous room. Several tabs on the same extension share the room. Disconnect waits 3 seconds before the agent is treated as gone (refresh-safe).
Client emits — server listens
The browser emits. Hub in src/ws/hub.js listens.
| Emit | Payload | Server does |
|---|---|---|
join | { role: 'dashboard' } or { role: 'agent', extension: '1007' } | Join room, then emit joined plus initial data |
leave | — | Leave room. Agent: decrement panel count |
ready | — | Presence ready. Writes dialer_employee.panel_status = active |
pause | { reason, note? } required | Presence paused. Writes dialer_agent_pauses.reason. note required for other (12 words) |
presence | { presence: 'ready' | 'paused', reason?, note? } | Same as ready / pause. reason required when pausing |
ping | — | Reply pong |
redial | { oli_id, ondemand? } | Re-originate that lead on this extension (skip wrap / queue). Skill must match. Attempt call_type = redial |
Socket.IO disconnect is treated as leave. Missing extension on agent join → server_error extension is required to join as agent. Extension not in this module’s dialer_employee roster → extension is not assigned to this dialer. Ready/pause before join also emits server_error. Agent pause (and presence paused) requires a picker code from public GET /api/pause-reasons. Missing, unknown, or system-only codes emit server_error { "code": "invalid_pause_reason" } and do not pause. other also needs { note } (max 12 words). Pick ondemand before the on-demand OLI dialog. ready needs no reason. redial before join, or without oli_id, returns { ok: false } (and server_error if you did not pass an ack callback). A successful agent join reloads that employee row and syncs the AMI roster.
Server emits — client listens
The hub emits. The browser listens.
| Listen | Room | When |
|---|---|---|
joined | That socket | After a successful join |
snapshot | dashboard | Dashboard join, and full AMI snapshot |
patch | dashboard | AMI incremental device-state changes |
status | ext:{n} | AMI + panel/presence for that extension |
panel | dashboard | Agent join, leave, ready, pause |
presence | dashboard | Last tab for an extension left (reason: 'left') |
dialer | Both | Engine phase: claim, buffer, connect, hangup |
call_connected | Both | Both parties talking (once per call) |
inbound | Dashboard; desk except waiting | PSTN park, offer, talk, hangup |
health | dashboard | AMI/DB health |
pong | That socket | Reply to ping |
server_error | That socket | Join / presence failures, and redial failures when no ack callback is used |
Payloads
joined
{ "role": "agent", "extension": "1007", "rooms": ["ext:1007"] }
Dashboard join is { "role": "dashboard", "rooms": ["dashboard"] }.
dialer
Always has extension and phase: idle, paused, buffer, waiting, dialing, inbound, on_call. Also buffer_ms / buffer_ends_at (wrap after a connected call only), connect_ms, on_call_at, talk_ms, client_name, oli_id, form_type, call_type (outgoing / incoming / redial while live; null when waiting / buffer / paused — this is the attempt column, not engine mode), outbound_blocked (pending remarks — dashboard Dialer column Remarks). Predictive adds outbound (other ringing/hold/offer legs, phase ringing, waiting, or connecting) and a floor-wide on_hold count of answered customers waiting for an agent (outbound MOH + mid agent-offer + inbound waiting).
call_connected
{
"extension": "1007",
"oli_id": "DUMMY-001",
"form_type": "GST",
"priority_id": 6,
"call_type": "outgoing"
}
Once per call when the conversation is actually up. call_type is outgoing / incoming / redial (same as dialer_call_attempts).
redial
After a drop, the CRM or agent page emits redial from an agent socket. The engine skips wrap time and the queue, claims the lead if oli_id exists and form_type matches this agent’s skills (no priority_ids gate), and originates again (AMI). ondemand: true allows redial from pause; on success the hub also sets presence ready. Auto-dial attempts stay outgoing; this attempt (and a decline on that predictive offer) is stored with call_type = redial.
socket.emit('redial', { oli_id: 'OLI124' }, (result) => {
// { ok: true, oli_id: 'OLI124' }
// { ok: false, error: 'busy'|'not_ready'|'unknown_oli'|'not_owned'|'originate_failed'|'maintenance', message: '…' }
});
Reopens any active master row, including Received, Rejected, Not Connected, Abandoned, and Disposed. Rejected if the agent is already on another call, SIP is not idle, (unless ondemand) paused, or the floor is in maintenance. Skill mismatch returns not_owned. Pending remarks do not block redial. Then listen for dialer phase: dialing and call_connected.
status / panel
One extension: AMI category, panel active/inactive, presence ready/paused, pause_reason (open pause code while paused), pause_reason_note when the reason is Other, paused_at (epoch ms when the current pause started; dashboard Work column counts from this), name from dialer_employee.display_name, assigned_roles from dialer_employee.skills (JSON), panel_status, buffer_time, decline_cooldown_seconds, is_maintenance, outbound_blocked (pending remarks), microsip_alert when the panel is up but AMI is unavailable. Socket call_type here is engine mode (Progressive / Predictive), not the attempt column. CRM profile fields such as email/mobile are not on dialer_employee and may be null. Pause is refused while outbound_blocked is true.
snapshot
updated_at, ami, summary (AMI on_call / dialing / idle / unavailable / total plus on_hold; dialing is desks still ringing a customer — progressive phase dialing, or predictive outbound legs with phase ringing; agent offer after answer is on_hold, not dialing), dialer (active config for this module’s module_id, including decline_cooldown_seconds and is_maintenance; hold_timeout_seconds is not sent), extensions[] (same shape as status, only endpoints present in this module’s dialer_employee roster — not every PJSIP contact).
inbound
PSTN caller. kind is waiting (dashboard only), ringing, answered, offer_failed, or ended. Fields include extension, caller_number, oli_id, client_name, form_type. Waiting callers stay on hold music until a skilled agent is offered. Decline uses decline_cooldown_seconds. Hold is unlimited until hangup or connect.
Built-in pages
| Page | Emits | Listens |
|---|---|---|
Dashboard / | join dashboard | snapshot, patch, dialer, panel, presence, health |
Agent /agent.html?ext=1007 | join agent, ready / pause, redial (on-demand) | status, dialer, call_connected, inbound, joined, server_error |
Socket does not push leads. Use POST /api/leads, PUT /api/escalated-tickets, and POST /api/unholdDialerData. Normal outbound is still engine-driven; redial is the client asking the engine to originate one specific lead now. Hub onAgentState is an in-process callback into the dialer, not a Socket.IO event.