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_HOSTNAMEconfiguration 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/jsonIf a value is returned, use it as the socket hostname. Otherwise, use the hostname from your API base URL.
Socket.IO Client Options
| Option | Value | Description |
|---|---|---|
transports | ['websocket', 'polling'] | Prefer WebSocket, fall back to long-polling |
path | /socket.io | Server endpoint path |
reconnection | true | Enable automatic reconnection |
reconnectionAttempts | 10 | Maximum retry attempts |
reconnectionDelay | 1000 | Initial retry delay (ms) |
reconnectionDelayMax | 5000 | Maximum retry delay (ms) |
timeout | 20000 | Connection 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:
| Type | Description |
|---|---|
call | Call events for a specific user/extension |
agent | Agent state events for a specific device |
queued | Queue membership and state events |
call + subtype: "queue" | Call events scoped to a specific queue |
voicemail | Voicemail deposit and state change events |
contacts | User presence and status updates |
All subscriptions share a common set of fields:
| Field | Type | Required | Description |
|---|---|---|---|
application | string | Yes | Your application identifier (e.g., "webphone", "myApp") |
bearer | string | Yes | OAuth 2.0 access token |
domain | string | Yes | OneCloud domain |
type | string | Yes | Subscription type (see above) |
filter | string | Varies | Filter scope — extension, SIP URI, or queue ID depending on type |
subtype | string | No | Subscription 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
queuedandcallwithsubtype: "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
| Field | Type | Description |
|---|---|---|
orig_callid | string | Originator call ID |
term_callid | string | Terminator call ID |
by_callid | string | Transfer-related call ID |
Caller / Callee
| Field | Type | Description |
|---|---|---|
ani | string | Caller ID number (Automatic Number Identification). Set to "Call-To-Talk" for click-to-dial calls |
dnis | string | Dialed number (Dialed Number Identification Service) |
orig_from_user | string | Originator user/extension |
orig_from_name | string | Originator display name |
term_user | string | Terminator user/extension |
Call State
| Field | Type | Values | Description |
|---|---|---|---|
orig_call_info | string | "progressing", "active" | State of the originating leg |
term_call_info | string | "alerting", "active" | State of the terminating leg |
time_answer | string | ISO datetime or "0000-00-00 00:00:00" | When the call was answered. "0000-00-00 00:00:00" means unanswered |
remove | string | "yes" | Present when the call leg has ended |
Device / Routing
| Field | Type | Description |
|---|---|---|
orig_type | string | Origin type: "device" or "gateway" |
term_type | string | Destination type: "device" or "gateway" |
orig_sub | string | Originator extension number |
term_sub | string | Terminator extension number |
term_uri | string | Terminator SIP URI (e.g., sip:1001@onecloud) |
Transfer
| Field | Type | Values | Description |
|---|---|---|---|
by_action | string | "Transfer", "MoveTo", "XferBlind", "ForwardSRing" | Transfer action type |
Determining Call State
Use these field combinations to determine the current call state:
| State | Condition |
|---|---|
| Ringing | orig_call_info == "progressing" and term_call_info == "alerting" |
| Answered | term_call_info == "active" and time_answer != "0000-00-00 00:00:00" |
| Answered elsewhere | orig_call_info == "active" and term_call_info != "active" |
| Missed | remove == "yes" and time_answer == "0000-00-00 00:00:00" and no transfer action |
| Ended | remove == "yes" |
| Transferred | by_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.
| Metric | Recommended Threshold |
|---|---|
| Pong timeout | 90 seconds (3 missed ping cycles) |
| Health check interval | 30 seconds |
| Consecutive failures before reconnect | 2 |
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
| Error | Cause | Resolution |
|---|---|---|
connect_error | Server unreachable or hostname incorrect | Verify the socket hostname and port 8001 is accessible |
subscription_error | Invalid or expired bearer token | Refresh the access token and re-subscribe |
| No events after subscription | Token lacks required scope, or filter does not match any extensions | Verify the domain and filter values match the authenticated user |
| Connection closes immediately | Socket.IO version mismatch | Use 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'
});
});Updated about 7 hours ago
