Websocket Event Subscriptions

WebSocket Event Subscriptions

The OneCloud UCaaS platform provides real-time call and presence events via a Socket.IO WebSocket server. Clients subscribe to events using a Bearer token obtained through the OAuth 2.0 Authorization Code Grant and receive push notifications for incoming calls, call state changes, and user presence updates.

Connection

Endpoint

Connect using a Socket.IO client to port 8001 on the same hostname as your API server:

wss://<your-api-hostname>:8001/socket.io

For example, if your API base URL is https://production-core.onecloud.com/ns-api, connect to:

wss://production-core.onecloud.com:8001/socket.io

Determining the Socket Hostname

Some deployments use a dedicated socket hostname. Query the PORTAL_SOCKET_HOSTNAME configuration to discover it:

GET /ns-api/v2/configurations/PORTAL_SOCKET_HOSTNAME?domain=~&user=~&user-scope=~&reseller=~ HTTP/1.1
Authorization: Bearer <access_token>
Accept: application/json

If a value is returned, use it as the socket hostname. Otherwise, use the hostname from your API base URL.

Socket.IO Client Options

OptionValueDescription
transports['websocket', 'polling']Prefer WebSocket, fall back to long-polling
path/socket.ioServer endpoint path
reconnectiontrueEnable automatic reconnection
reconnectionAttempts10Maximum retry attempts
reconnectionDelay1000Initial retry delay (ms)
reconnectionDelayMax5000Maximum retry delay (ms)
timeout20000Connection timeout (ms)

Example — Connect

import io from 'socket.io-client';

const socket = io('https://production-core.onecloud.com:8001', {
  transports: ['websocket', 'polling'],
  path: '/socket.io',
  reconnection: true,
  reconnectionAttempts: 10,
  timeout: 20000
});

socket.on('connect', () => {
  console.log('Connected:', socket.id);
});

Version Compatibility: The server supports Socket.IO v2, v3, and v4 clients. A v2 client is recommended for broadest compatibility.


Subscription

After connecting, subscribe to event streams by emitting a subscribe message with your credentials. Multiple subscription types are available:

TypeDescription
callCall events for a specific user/extension
agentAgent state events for a specific device
queuedQueue membership and state events
call + subtype: "queue"Call events scoped to a specific queue
voicemailVoicemail deposit and state change events
contactsUser presence and status updates

All subscriptions share a common set of fields:

FieldTypeRequiredDescription
applicationstringYesYour application identifier (e.g., "webphone", "myApp")
bearerstringYesOAuth 2.0 access token
domainstringYesOneCloud domain
typestringYesSubscription type (see above)
filterstringVariesFilter scope — extension, SIP URI, or queue ID depending on type
subtypestringNoSubscription subtype (e.g., "queue" for queue-scoped call events)

Call Subscription

Subscribes to call events for a specific user/extension on a domain.

socket.emit('subscribe', {
  application: 'myApp',
  bearer: '<access_token>',
  domain: 'example-domain',
  type: 'call',
  filter: '1001'
});

The filter value is the extension number. Events include incoming calls, outbound calls, call state changes, and transfers for that extension.

Agent Subscription

Subscribes to agent state events for a specific device. Useful for tracking agent login/logout, availability, and ACD state.

socket.emit('subscribe', {
  application: 'webphone',
  bearer: '<access_token>',
  domain: 'example-domain',
  type: 'agent',
  filter: 'sip:1001@example-domain'
});

The filter value is the full SIP URI of the agent device (e.g., sip:1001@example-domain).

Status confirmation format: Setup Complete (<domain>-<>-agent-<>-<filter>)

Queue Subscription

Two subscription types are available for call center queues:

Queued Events

Subscribes to queue membership and state changes (e.g., callers entering/leaving the queue).

socket.emit('subscribe', {
  application: 'webphone',
  bearer: '<access_token>',
  domain: 'example-domain',
  type: 'queued',
  filter: '4020'
});

Status confirmation format: Setup Complete (<domain>-<>-queued-<>-<filter>)

Queue Call Events

Subscribes to call events scoped to a specific queue. Uses type: "call" with subtype: "queue".

socket.emit('subscribe', {
  application: 'webphone',
  bearer: '<access_token>',
  domain: 'example-domain',
  type: 'call',
  subtype: 'queue',
  filter: '4020'
});

Status confirmation format: Setup Complete (<domain>-<>-call-<>-<filter>-<T>-queue)

Tip: To fully monitor a queue, subscribe to both queued and call with subtype: "queue" for the same queue ID.

Voicemail Subscription

Subscribes to voicemail events for a specific extension — new voicemail deposits, message state changes (read/unread), and deletions.

socket.emit('subscribe', {
  application: 'webphone',
  bearer: '<access_token>',
  domain: 'example-domain',
  type: 'voicemail',
  filter: '1001'
});

The filter value is the extension number to monitor for voicemail activity.

Contacts Subscription

Subscribes to presence and status updates for users on a domain.

socket.emit('subscribe', {
  application: 'myApp',
  bearer: '<access_token>',
  domain: 'example-domain',
  type: 'contacts'
});

No filter is required — presence events are returned for all users on the domain.

Multiple Subscriptions

A single connection can hold multiple subscriptions. Subscribe to each independently after connecting:

socket.on('connect', () => {
  // User call events
  socket.emit('subscribe', {
    application: 'webphone', bearer: token, domain: 'example-domain',
    type: 'call', filter: '1001'
  });

  // Agent state
  socket.emit('subscribe', {
    application: 'webphone', bearer: token, domain: 'example-domain',
    type: 'agent', filter: 'sip:1001@example-domain'
  });

  // Queue monitoring (both types)
  socket.emit('subscribe', {
    application: 'webphone', bearer: token, domain: 'example-domain',
    type: 'queued', filter: '4020'
  });
  socket.emit('subscribe', {
    application: 'webphone', bearer: token, domain: 'example-domain',
    type: 'call', subtype: 'queue', filter: '4020'
  });

  // Voicemail
  socket.emit('subscribe', {
    application: 'webphone', bearer: token, domain: 'example-domain',
    type: 'voicemail', filter: '1001'
  });

  // Presence
  socket.emit('subscribe', {
    application: 'webphone', bearer: token, domain: 'example-domain',
    type: 'contacts'
  });
});

Each subscription receives its own status confirmation.

Subscription Confirmation

The server confirms each successful subscription with a status event containing "Setup Complete" and a descriptor identifying the subscription:

Setup Complete (<domain>-<>-<type>-<>-<filter>)
Setup Complete (<domain>-<>-call-<>-<filter>-<T>-queue)

Listen for confirmations:

socket.on('subscribed', (data) => {
  console.log('Subscription confirmed');
});

socket.on('status', (data) => {
  if (data?.status?.includes('Setup Complete')) {
    console.log('Subscription confirmed:', data.status);
  }
});

Alternatively, the server may emit ack instead of subscribed — listen for both.

If no confirmation is received within 10–15 seconds, the access token may have expired. Refresh the token and re-subscribe.


Call Events

Once subscribed with type: "call", the server emits call events as call state changes. Each event contains a flat object with the fields below.

Event Fields

Call Identification

FieldTypeDescription
orig_callidstringOriginator call ID
term_callidstringTerminator call ID
by_callidstringTransfer-related call ID

Caller / Callee

FieldTypeDescription
anistringCaller ID number (Automatic Number Identification). Set to "Call-To-Talk" for click-to-dial calls
dnisstringDialed number (Dialed Number Identification Service)
orig_from_userstringOriginator user/extension
orig_from_namestringOriginator display name
term_userstringTerminator user/extension

Call State

FieldTypeValuesDescription
orig_call_infostring"progressing", "active"State of the originating leg
term_call_infostring"alerting", "active"State of the terminating leg
time_answerstringISO datetime or "0000-00-00 00:00:00"When the call was answered. "0000-00-00 00:00:00" means unanswered
removestring"yes"Present when the call leg has ended

Device / Routing

FieldTypeDescription
orig_typestringOrigin type: "device" or "gateway"
term_typestringDestination type: "device" or "gateway"
orig_substringOriginator extension number
term_substringTerminator extension number
term_uristringTerminator SIP URI (e.g., sip:1001@onecloud)

Transfer

FieldTypeValuesDescription
by_actionstring"Transfer", "MoveTo", "XferBlind", "ForwardSRing"Transfer action type

Determining Call State

Use these field combinations to determine the current call state:

StateCondition
Ringingorig_call_info == "progressing" and term_call_info == "alerting"
Answeredterm_call_info == "active" and time_answer != "0000-00-00 00:00:00"
Answered elsewhereorig_call_info == "active" and term_call_info != "active"
Missedremove == "yes" and time_answer == "0000-00-00 00:00:00" and no transfer action
Endedremove == "yes"
Transferredby_action == "MoveTo" and remove == "yes"

Example — Handling Call Events

socket.on('call', (data) => {
  const isRinging = data.orig_call_info === 'progressing'
    && data.term_call_info === 'alerting';

  const isAnswered = data.term_call_info === 'active'
    && data.time_answer
    && data.time_answer !== '0000-00-00 00:00:00';

  const isMissed = data.remove === 'yes'
    && (!data.time_answer || data.time_answer === '0000-00-00 00:00:00')
    && !data.by_action;

  const isEnded = data.remove === 'yes';

  if (isRinging) {
    console.log(`Incoming call from ${data.ani} to ${data.dnis}`);
  } else if (isAnswered) {
    console.log(`Call answered at ${data.time_answer}`);
  } else if (isMissed) {
    console.log(`Missed call from ${data.ani}`);
  } else if (isEnded) {
    console.log(`Call ended`);
  }
});

Presence Events

Once subscribed with type: "contacts", the server emits contacts-domain events with user presence updates for the subscribed domain.

socket.on('contacts-domain', (data) => {
  console.log('Presence update:', data);
});

Health Monitoring

The Socket.IO server sends periodic ping frames. Clients should monitor pong responses to detect connection staleness.

MetricRecommended Threshold
Pong timeout90 seconds (3 missed ping cycles)
Health check interval30 seconds
Consecutive failures before reconnect2
let lastPongTime = Date.now();
let consecutiveFailures = 0;

socket.on('pong', () => {
  lastPongTime = Date.now();
  consecutiveFailures = 0;
});

setInterval(() => {
  const elapsed = Date.now() - lastPongTime;
  if (elapsed > 90000) {
    consecutiveFailures++;
    if (consecutiveFailures >= 2) {
      socket.disconnect();
      socket.connect();
    }
  }
}, 30000);

Complete Example

import io from 'socket.io-client';

// 1. Connect
const socket = io('https://production-core.onecloud.com:8001', {
  transports: ['websocket', 'polling'],
  path: '/socket.io',
  reconnection: true,
  reconnectionAttempts: 10,
  timeout: 20000
});

// 2. Subscribe on connect
socket.on('connect', () => {
  socket.emit('subscribe', {
    application: 'myApp',
    bearer: '<access_token>',
    domain: 'my-domain',
    filter: '1001',
    type: 'call'
  });
});

// 3. Confirm subscription
socket.on('subscribed', () => console.log('Subscribed'));
socket.on('status', (data) => {
  if (data?.status?.includes('Setup Complete')) {
    console.log('Subscribed');
  }
});

// 4. Handle call events
socket.on('call', (data) => {
  if (data.orig_call_info === 'progressing' && data.term_call_info === 'alerting') {
    console.log(`Ringing: ${data.ani} → ${data.dnis}`);
  }
  if (data.remove === 'yes') {
    console.log('Call ended');
  }
});

// 5. Handle errors and reconnection
socket.on('disconnect', (reason) => {
  console.log('Disconnected:', reason);
});

socket.on('reconnect', (attempts) => {
  console.log('Reconnected after', attempts, 'attempts');
  // Re-subscribe after reconnection
  socket.emit('subscribe', {
    application: 'myApp',
    bearer: '<access_token>',
    domain: 'my-domain',
    filter: '1001',
    type: 'call'
  });
});

Error Handling

ErrorCauseResolution
connect_errorServer unreachable or hostname incorrectVerify the socket hostname and port 8001 is accessible
subscription_errorInvalid or expired bearer tokenRefresh the access token and re-subscribe
No events after subscriptionToken lacks required scope, or filter does not match any extensionsVerify the domain and filter values match the authenticated user
Connection closes immediatelySocket.IO version mismatchUse a v2 client for broadest compatibility

Token Refresh on Subscription Failure

If the subscription callback does not fire within 10 seconds, or a subscription_error event is received, refresh the access token and retry:

socket.on('subscription_error', async (error) => {
  const newToken = await refreshAccessToken();
  socket.emit('subscribe', {
    application: 'myApp',
    bearer: newToken,
    domain: 'my-domain',
    filter: '1001',
    type: 'call'
  });
});