Skip to main content

WebRTC Client SDK

The WebRTC Client SDK provides a high-level interface for building softphone applications. It handles WebSocket signalling, WebRTC media, call management, and presence — so you can focus on your UI.

Installation

npm install @dialstack/sdk

The WebRTC client is available at @dialstack/sdk/webrtc.

DialStackPhone

The main entry point. Manages the WebSocket connection, calls, and presence for a single user.

Constructor

import { DialStackPhone } from '@dialstack/sdk/webrtc';

const phone = new DialStackPhone(options: PhoneOptions);

PhoneOptions

PropertyTypeRequiredDescription
tokenstringYesUser token from POST /v1/user_sessions
onTokenExpiring() => Promise<string>NoCalled ~5 minutes before token expiry. Return a fresh token.
autoReconnectbooleanNoReconnect automatically on disconnect (default: true)
iceServersRTCIceServer[]NoOverride ICE server configuration (default: fetched from API)

Connection

// Connect to the signalling server
await phone.connect(): Promise<void>;

// Disconnect (ends all active calls)
phone.disconnect(): void;

// Connection state
phone.isConnected: boolean;

Making Calls

// Dial a phone number or extension
const call = await phone.call(
destination: string,
options?: CallOptions
): Promise<Call>;

CallOptions

PropertyTypeDescription
callerIdstringOutbound caller ID (E.164). Must be assigned to the account.

Active Calls

// All active calls (ringing, active, held)
phone.activeCalls: Call[];

// Find a call by ID
phone.getCall(callId: string): Call | undefined;

Presence

// Subscribe to presence updates
phone.subscribePresence(userIds?: string[]): void;

// Set your own presence
await phone.setPresence(
status: 'available' | 'dnd' | 'away',
statusText?: string
): Promise<void>;

Emergency Address

// Set or update emergency address (E911)
await phone.setEmergencyAddress(address: EmergencyAddressRequest): Promise<EmergencyAddress>;

// Get current emergency address
await phone.getEmergencyAddress(): Promise<EmergencyAddress | null>;

EmergencyAddressRequest

PropertyTypeRequiredDescription
streetstringYesStreet address
street2string | nullNoSuite, floor, apartment, room number
citystringYesCity
statestringYesTwo-letter state/province code
zipstringYesZIP/postal code
countrystringYesTwo-letter country code (ISO 3166-1)

Events

EventPayloadDescription
connectedWebSocket connected and authenticated
disconnectedWebSocket disconnected
reconnectedReconnected after a drop
incomingCallIncoming call
presenceListPresenceEntry[]Initial presence snapshot
presenceUpdatePresenceUpdateUser presence changed
network.changedClient IP address changed (prompt for E911)
errorPhoneErrorConnection or protocol error
phone.on(event: string, handler: Function): void;
phone.off(event: string, handler?: Function): void;

Call

Represents a single phone call. Obtained from phone.call() or the incoming event.

Properties

PropertyTypeDescription
idstringCall identifier
direction'inbound' | 'outbound'Call direction
state'trying' | 'ringing' | 'active' | 'held' | 'ended'Current call state
fromstringCaller's number (E.164)
fromNamestring | nullCaller's display name
tostringCalled number or extension
isMutedbooleanWhether the microphone is muted
isHeldbooleanWhether the call is on hold
durationnumberCall duration in seconds (updates in real-time)

Methods

// Answer an incoming call
call.answer(): void;

// Reject an incoming call
call.reject(reason?: 'busy' | 'decline'): void;

// Hang up
call.hangup(): void;

// Hold / resume
call.hold(): void;
call.resume(): void;

// Mute / unmute
call.mute(): void;
call.unmute(): void;

// DTMF is sent inline with the audio stream via the browser's
// RTCDTMFSender API (RFC 4733). See Calling & Call Control → DTMF.

// Blind transfer
call.transfer(destination: string): void;

// Attended transfer (returns the consultation call)
call.attendedTransfer(destination: string): Promise<Call>;

// Complete an attended transfer
call.completeTransfer(): void;

// Access the underlying RTCPeerConnection (for stats, custom media handling)
call.peerConnection: RTCPeerConnection;

Events

EventPayloadDescription
tryingServer is processing the call
ringingRemote party is ringing
answeredCall connected
held'local' | 'remote'Call placed on hold
resumedCall resumed from hold
endedstringCall ended (reason: hangup, no-answer, busy, failed, transferred, rejected)
call.on(event: string, handler: Function): void;
call.off(event: string, handler?: Function): void;

Types

PresenceEntry

interface PresenceEntry {
userId: string;
name: string;
status: 'available' | 'on_call' | 'dnd' | 'away' | 'offline';
statusText: string | null;
updatedAt: string;
}

PresenceUpdate

interface PresenceUpdate {
userId: string;
status: 'available' | 'on_call' | 'dnd' | 'away' | 'offline';
statusText: string | null;
updatedAt: string;
}

PhoneError

interface PhoneError {
code:
| 'auth_failed'
| 'auth_expired'
| 'invalid_message'
| 'call_failed'
| 'call_not_found'
| 'emergency_address_required'
| 'session_limit'
| 'session_revoked'
| 'rate_limited'
| 'internal_error';
message: string;
callId?: string;
fatal: boolean;
}

Server SDK Additions

The following resources are added to the server SDK for WebRTC-related backend operations:

userSessions

Mints a short-lived JWT session token for a single DialStack user. The returned client_secret is the Bearer token your frontend uses on the signalling WebSocket and /v1/me/* REST routes.

// Create a user session token
const { client_secret, expires_at } = await dialstack.userSessions.create({
user: 'user_01h2xcejqtf2nbrexx3vqjhp42',
// ttl_seconds: 3600, // optional; defaults to 24 hours, max 7 days
});

The user must already be provisioned via POST /v1/users and belong to an account owned by the calling platform. To extend a session, simply call userSessions.create again — there is no refresh endpoint; expiry is enforced by the token's own exp claim.

To cut a user off before their tokens expire (offboarding, a compromised device), revoke all of their sessions at once:

// Invalidate every outstanding session token for a user
const { sessions_revoked_at } = await dialstack.users.revokeSessions(
'user_01h2xcejqtf2nbrexx3vqjhp42'
);

Revoked tokens stop working immediately on REST routes and at connection time; an already-connected phone is disconnected with a fatal session_revoked error the next time it places or receives a call, and the SDK will not auto-reconnect. Minting a new session afterwards restores service.

webrtc

// Get ICE server configuration
const { ice_servers, expires_at } = await dialstack.webrtc.iceServers({
token: userToken,
});