Embedded Softphone (WebRTC)
SIP.IO can put calling directly inside a web app, no desk phone and no separately installed softphone. A browser page registers over WebSocket (WSS) as a real SIP endpoint and can place or receive calls like any other device.
The piece that’s live today is the authentication foundation: a token endpoint that mints a short-lived, ephemeral SIP credential for the browser to register with, following the same model as Twilio Access Tokens. The embeddable client SDK itself (the JS library, floating widget, screen-pop helpers, CRM connectors) is not yet built; see What’s next.
Why a token, not a password
Section titled “Why a token, not a password”A desk phone stores a long-lived SIP password. A browser tab is a much worse place to keep one: it’s visible in devtools, gets baked into bundles, and can’t be revoked per-session. So the embedded softphone never sees a real account password. Instead, your backend (holding an API key with the softphone scope) asks the platform to mint a one-time, short-lived credential scoped to a single agent identity, and hands that to the browser.
curl -X POST https://api.sip.io/v1/softphone/token \ -H 'x-api-key: sk_…' -H 'content-type: application/json' \ -d '{ "identity": "us_dana", "ttlSec": 3600 }'{ "ok": true, "identity": "us_dana", "extension": "1002", "sipUri": "sip:1002@acme.sip.io", "authUsername": "wrtc_a1b2c3d4e5f6a7b8", "password": "wp_…", "realm": "acme.sip.io", "wssUrl": "wss://webrtc-eu.sip.io", "transport": "wss", "iceServers": [ { "urls": ["stun:webrtc-eu.sip.io:443"] }, { "urls": ["turn:webrtc-eu.sip.io:443"], "username": "1735689600:us_dana", "credential": "…" } ], "expiresAt": 1735689600, "ttl": 3600}identity is a sip_user id or extension already in your account; the token is minted for that user’s existing identity, not a new one. Under the hood, the platform creates an ephemeral sip_device (auth_username prefixed wrtc_, is_webrtc=1, transport=wss) linked to that user, computes its digest hash (ha1/ha1b), and returns the cleartext password once; it is never stored. Because it’s a real device on the user’s account, inbound calls ring it just like any other device in the user’s fork.
Expiry is real, not advisory
Section titled “Expiry is real, not advisory”ttlSec (default 3600, clamped to 300 seconds..24 hours) sets sip_device.expires_at = now + ttl. That’s enforced, not a hint: the platform’s /auth device lookup checks expires_at on every REGISTER/INVITE challenge and rejects an ephemeral credential past it with reason: "expired", the same digest-auth path every phone uses. A token that’s expired can no longer register or place calls; the client must re-mint before expiresAt.
Expired ephemeral credentials are cleaned up two ways:
- Lazily, on the account’s next mint: minting a new token also deletes that account’s expired
wrtc_-prefixed devices. - Globally, by a
*/5cron reaper (reapExpiredWebrtc), so an integration that stops minting doesn’t leak devices forever. A matching/admin/reap-webrtcendpoint runs the same sweep on demand.
Both are scoped to the wrtc_ username prefix, so permanent WebRTC devices (expires_at IS NULL, e.g. a desk-style WebRTC endpoint someone provisions directly) are never touched.
NAT traversal: STUN/TURN via the TURN relay
Section titled “NAT traversal: STUN/TURN via the TURN relay”iceServers in the token response is a ready-to-use WebRTC ICE configuration for the browser’s RTCPeerConnection: STUN entries plus, where configured, TURN entries for networks that block outbound UDP media.
TURN credentials use the TURN relay’s TURN REST API convention (the use-auth-secret mechanism), minted fresh per token:
username="<expiry-unix>:<identity>"credential=base64(HMAC-SHA1(TURN_SECRET, username))
Like the SIP password, there’s no static TURN secret shared with the browser, the credential is short-lived and tied to the same expiry as the SIP credential. If TURN_URLS isn’t configured for the environment, iceServers simply omits TURN entries and the SDK connects directly to the public the media relay media anchor, which is fine on networks that allow outbound UDP; TURN only matters where it’s blocked.
Transport: WSS on 443, one shared host per region
Section titled “Transport: WSS on 443, one shared host per region”The browser registers over WSS on port 443, not an alternate port like 8443. Corporate and hotel Wi-Fi commonly blocks non-standard ports but always allows 443, so this is the traversal-friendly default, the same reasoning as the TURN :443 listener above.
The WSS endpoint is a shared host per region (webrtc-<region>.sip.io), not a per-tenant or wildcard hostname. Tenant identity lives at the SIP layer (each account’s realm, e.g. acme.cust.sip.io, per multi-tenant SIP domains), while the WSS host is just the transport, so it can use an ordinary single-host browser-HTTPS certificate. This sidesteps the wildcard-certificate restriction that applies to SIP-domain certs (RFC 5922 §7.2 forbids wildcard match at the SIP layer) since that rule doesn’t govern the WebSocket transport layer at all.
What’s next
Section titled “What’s next”The pieces above (/v1/softphone/token, TURN credential minting, the WSS transport) are the live authentication and connectivity foundation. Built on top of it, still on the roadmap:
- A published
@sipio/softphoneclient SDK (SIP.js-based) that wraps token fetch, registration, and call control behind a small JS API. - A floating widget for drop-in embedding without building call UI from scratch.
- Screen-pop helpers tying an inbound/outbound call to CRM record context (the
screenPopfield already accepted by click-to-call is intended for this). - CRM connector packages for common platforms.
Until the SDK ships, the token endpoint can be used directly with any SIP.js/JsSIP-based WebRTC client that can register over WSS with a username/password and consume a standard iceServers array.
Related
Section titled “Related”- Users & Devices: how the ephemeral device rings alongside a user’s other devices.
- Authentication: API key scopes and SIP digest auth, including the
softphonescope. - Click to Call: agent-first dialing, the other half of an embedded calling experience.