WebRTC
Build softphone experiences for your users on web, mobile, and desktop using DialStack's WebRTC API.
This API is the softphone side of DialStack — it lets a signed-in user place and receive calls from their browser or native app. It is not a programmable-voice or A2P (application-to-person) API: every WebRTC session is bound to an authenticated DialStack user, and the call shows up in their call history, billing, and presence just like a call from their desk phone.
Overview
The WebRTC API enables your application to make and receive phone calls directly from a browser or native app on behalf of a signed-in user. It consists of three parts:
- User authentication — end users authenticate to get a token (Authentication guide)
- Signalling — a WebSocket connection at
wss://api.dialstack.ai/v1/webrtchandles call setup, control, and presence - Media — WebRTC peer connections carry the actual audio between the client and DialStack
A single WebSocket connection handles all signalling for one user, including multiple simultaneous calls.
Architecture
┌───────────────────────────────────────────────────────────┐
│ Your Application (browser / mobile / desktop) │
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ │
│ │ Your UI │ │ DialStack SDK │ │ WebRTC │ │
│ │ (dial pad, │──│ (signalling, │──│ Media │ │
│ │ contacts) │ │ call control) │ │ (audio) │ │
│ └─────────────┘ └────────┬─────────┘ └──────┬──────┘ │
│ │ │ │
└────────────────────────────┼───────────────────┼──────────┘
│ WSS │ SRTP
│ (signalling) │ (encrypted audio)
▼ ▼
┌────────────────────────────────────────┐
│ DialStack │
│ │
│ Signalling ──── Media ──── PSTN │
│ Server Gateway Gateway │
└────────────────────────────────────────┘
- Signalling travels over a single WebSocket (JSON messages)
- Media flows directly via WebRTC (ICE/DTLS-SRTP) — encrypted end-to-end between client and DialStack
- PSTN connectivity is handled server-side — your client just sends and receives audio
Quick Start
1. Get a user token
After your backend has authenticated the user with your own identity provider, mint a DialStack user session token for them. Your backend calls DialStack server-to-server — the user's IdP credentials never leave your infrastructure:
// Your backend
import { DialStack } from '@dialstack/sdk/server';
const dialstack = new DialStack(process.env.DIALSTACK_API_KEY);
const { client_secret: token } = await dialstack.userSessions.create({
user: 'user_01h2xcejqtf2nbrexx3vqjhp42',
// ttl_seconds: 3600, // optional; defaults to 1 hour, max 24 hours
});
The returned client_secret is a short-lived JWT scoped to the named user. Hand it to your frontend; never expose your DialStack API key to the client. The user must already be provisioned via POST /v1/users.
2. Connect the softphone
// Your frontend
import { DialStackPhone } from '@dialstack/sdk/webrtc';
const phone = new DialStackPhone({
token,
onTokenExpiring: async () => {
const { token } = await fetch('/api/dialstack/user-token', {
method: 'POST',
headers: { Authorization: `Bearer ${sessionToken}` },
}).then((r) => r.json());
return token;
},
});
await phone.connect();
console.log('Phone connected, ready for calls');
3. Make a call
const call = await phone.call('+14155551234');
call.on('ringing', () => console.log('Ringing...'));
call.on('answered', () => console.log('Connected!'));
call.on('ended', (reason) => console.log('Call ended:', reason));
4. Receive a call
phone.on('incoming', (call) => {
console.log('Incoming call from', call.from, call.fromName);
// Show UI, then:
call.answer();
// Or reject:
// call.reject();
});
Connecting
WebSocket Connection
The SDK manages the WebSocket connection automatically. Under the hood, it:
- Opens a WebSocket to
wss://api.dialstack.ai/v1/webrtc - Sends an
authenticatemessage with the user token - Receives
authenticatedconfirming the session - Sends
pingevery 30 seconds to keep the connection alive
const phone = new DialStackPhone({ token });
phone.on('connected', () => {
// WebSocket connected and authenticated
});
phone.on('disconnected', () => {
// WebSocket lost — SDK will reconnect automatically
});
phone.on('reconnected', () => {
// Reconnected — active calls are restored
});
await phone.connect();
Reconnection
If the WebSocket connection drops (network change, brief outage), the SDK reconnects automatically with exponential backoff. Active calls survive brief disconnections (up to 30 seconds) — the server preserves call state during the gap.
On reconnection, the SDK:
- Authenticates with the current token
- Receives
call.restoredfor each active call - Closes the old
RTCPeerConnectionand creates a new one - Completes a full SDP offer/answer exchange to re-establish media
This adds ~1 second to call recovery but is simpler and more reliable than attempting an ICE restart on the existing connection. The SDK handles this automatically — your code just sees the reconnected event.
Session Limits
A single user can have up to 3 concurrent WebRTC sessions (e.g., laptop, phone, tablet). All connected sessions ring on incoming calls, but only one session at a time can hold an active call — once a call is answered on one device, the others stop ringing and cannot place or answer another call until the active call ends. This avoids the "two calls on two devices" gaming behaviors that the model isn't built for.
If a user attempts a 4th connection, the oldest session is disconnected.
phone.on('error', (error) => {
if (error.code === 'session_limit') {
// User has too many active sessions
showMessage('You are connected on too many devices. Close another session and try again.');
}
});
Token Refresh
User session tokens default to a 1-hour lifetime (configurable up to 24 hours via ttl_seconds). The onTokenExpiring callback fires ~5 minutes before expiry so you can mint a fresh token without interrupting the session.
How often the user has to interactively sign in depends on your identity provider, not DialStack. The session token is minted by your backend on demand, so DialStack inherits whatever session lifetime your IdP enforces. In practice this means:
- While the IdP session is still valid,
onTokenExpiringsilently calls your backend, which mints a fresh DialStack session — the user notices nothing. - If the IdP session has expired, your backend's mint call won't happen; surface that to the user by triggering your normal sign-in flow before calling
phone.connect()again.
const phone = new DialStackPhone({
token: initialToken,
onTokenExpiring: async () => {
// Fetch a new token from your backend
const { token } = await fetch('/api/dialstack/user-token', {
method: 'POST',
}).then((r) => r.json());
return token;
},
});
Error Handling
phone.on('error', (error) => {
switch (error.code) {
case 'auth_failed':
// Token is invalid — get a new one
break;
case 'auth_expired':
// Token expired — should have been refreshed via onTokenExpiring
break;
case 'rate_limited':
// Too many requests — back off
break;
}
});
What's Next
- Calling & Call Control — Make and receive calls, hold, transfer, DTMF
- Presence — Real-time user status and BLF
- Mobile & Push Notifications — Push notifications and background audio
- Emergency Calling (E911) — Address registration and 911 routing for nomadic users
- Network & Troubleshooting — Codecs, firewall configuration, debugging
- Signalling Protocol — Low-level WebSocket message reference
- Client SDK Reference — Complete TypeScript API