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
| Property | Type | Required | Description |
|---|---|---|---|
token | string | Yes | User token from POST /v1/user_sessions |
onTokenExpiring | () => Promise<string> | No | Called ~5 minutes before token expiry. Return a fresh token. |
autoReconnect | boolean | No | Reconnect automatically on disconnect (default: true) |
iceServers | RTCIceServer[] | No | Override 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
| Property | Type | Description |
|---|---|---|
callerId | string | Outbound 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
| Property | Type | Required | Description |
|---|---|---|---|
street | string | Yes | Street address |
street2 | string | null | No | Suite, floor, apartment, room number |
city | string | Yes | City |
state | string | Yes | Two-letter state/province code |
zip | string | Yes | ZIP/postal code |
country | string | Yes | Two-letter country code (ISO 3166-1) |
Events
| Event | Payload | Description |
|---|---|---|
connected | — | WebSocket connected and authenticated |
disconnected | — | WebSocket disconnected |
reconnected | — | Reconnected after a drop |
incoming | Call | Incoming call |
presenceList | PresenceEntry[] | Initial presence snapshot |
presenceUpdate | PresenceUpdate | User presence changed |
network.changed | — | Client IP address changed (prompt for E911) |
error | PhoneError | Connection 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
| Property | Type | Description |
|---|---|---|
id | string | Call identifier |
direction | 'inbound' | 'outbound' | Call direction |
state | 'trying' | 'ringing' | 'active' | 'held' | 'ended' | Current call state |
from | string | Caller's number (E.164) |
fromName | string | null | Caller's display name |
to | string | Called number or extension |
isMuted | boolean | Whether the microphone is muted |
isHeld | boolean | Whether the call is on hold |
duration | number | Call 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
| Event | Payload | Description |
|---|---|---|
trying | — | Server is processing the call |
ringing | — | Remote party is ringing |
answered | — | Call connected |
held | 'local' | 'remote' | Call placed on hold |
resumed | — | Call resumed from hold |
ended | string | Call 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,
});
Related Resources
- WebRTC Overview — Integration guide with complete code examples
- Signalling Protocol — Low-level WebSocket message reference
- Server SDK — Server-side SDK for account and user management