Skip to main content

Browser Softphone — place and receive calls from the dashboard

The Browser Softphone is the dashboard’s built-in phone. It places and receives real calls from a browser tab, over WebRTC, without a desk phone, a SIP app, or a browser extension. It is the same underlying path the floating dialer and the SDK-demo test page ride: a short-lived LiveKit token joins a per-user voice room, then a dial request bridges the room to the PSTN through jambonz and the Devotel softswitch — the org’s wholesale outbound path. This page is not an external SIP client; it’s the in-dashboard surface.

1. What it is

The Browser Softphone page lives under Voice → Softphone. POST /voice/softphone/token issues a short-lived credential for the current user’s deterministic room (softphone:<tenant>:<user>), and POST /voice/softphone/dial starts the PSTN leg. Outbound termination exits through jambonz → the Devotel softswitch, exactly like every other production voice path. There is no SIP REGISTER step on this surface — the browser joins over the signed-in dashboard session and listens for inbound frames. That is also why the surface deliberately does not use the Web SDK Softphone class (the raw SIP-over-WSS register path) described on the Web SDK page: that class depends on a SIP WSS gateway that is not provisioned in most environments, so its register call is rejected. The dashboard uses the LiveKit path instead, which works out of the box. The floating softphone shell on every dashboard route, the global IncomingCallDialog, and the Developer → SDK Live Demo page all ride the same useSoftphone() state machine, so this page stays in lockstep with the softphone experience everywhere else.

2. Role gate

Placing a call spends wallet credit and registers a callable identity, so only owner, admin, and developer roles reach the surface. Viewers, billing, and agent roles are excluded — same gate as the Developer SDK-demo page. If a teammate can’t see the page, an org owner must change their role; it is the same gate the Developer SDK-demo page carries.

3. Inbound and outbound flows

At rest the page shows a Ready badge when the real-time event stream is connected (the LiveKit equivalent of “registered presence”), or a “Connecting to Orbit…” hint when it is not, since inbound rings arrive only when that stream is live. Outbound. Pick a caller ID from the organization’s active numbers (the picker lists every owned, active number), type the destination in E.164 form (for example +15559876543), and press Call. The dial runs through the same POST /voice/softphone/tokenPOST /voice/softphone/dial sequence the floating dialer uses, and the page watches the badge climb Connecting…Dialing…Ringing…On call. Inbound. A ringing Incoming call banner appears with Answer / Decline buttons, tracking the shared IncomingCallDialog so two dashboard tabs never double-answer. Accepting a call flips the badge to On call. Hang up. Hang up terminates the call; the badge falls back to Call ended. Every call placed or accepted through this page is a standard one — it lands in the Voice call logs with its caller/callee, duration, recording, and the disposition tag the responsible party set, so agent QA and analytics see the same trail the floating dialer leaves.

4. Caller ID and number sources

The picker fetches your organization’s active numbers and defaults to your saved preferred caller ID (the same persisted setting the floating dialer and incoming-call reply share). An extension that provisions a direct number can deep-link here with ?from=<E.164> to adopt that number as the shared caller ID when it’s owned and active. If no owned number can dial (all suspended, releasing, or still activating), the page says so with the specific reason, instead of a generic “No active numbers” state — pick a caller ID the ownership gate accepts, since the dial itself rejects a non-owned number.

5. Limits

  • Scope. Token and dial endpoints accept voice:read / voice:write scope; the page gates to owner/admin/developer.
  • Per-second dial caps. The voice surface is rate-limited like other read/write endpoints (read budget / write budget under RATE_LIMIT_AUTH_READ / RATE_LIMIT_AUTH_WRITE); an idle softphone never dials and so never exhausts the write budget.
  • Concurrent sessions. The softphone room is scoped to the current user, and two tabs opened on the same session share the same useSoftphone() state, not two parallel call sessions.

6. When to use this versus your own SIP client

Use the Browser Softphone when the work is yours — a sales call, an escalation, a supervisor barge, a demo — and when the call start or accept should leave the same dashboard that hosts the rest of the workflow. Use your own SIP client (a desk phone, a Zoiper-like app, the Web SDK Softphone embedded in your own app) when the agent works from a fixed PBX seat, the organization also needs registrations on desk phones, or you need classic SIP register semantics — the registering-your-pbx-on-orbit guide covers the PBX trunk side, and the Web SDK Softphone describes embedding the softphone class in your own application. The dashboard softphone does not use the SDK Softphone class — it rides the LiveKit room directly, and it never SIP-REGISTERs.

7. Escape hatch — token + dial from the API

POST /voice/softphone/token (any owner/admin/developer credential; an empty body is accepted):
Response (abridged):
Then POST /voice/softphone/dial with the destination and (optional) caller ID:
Both endpoints are the same pair the dashboard’s useSoftphone() context calls, so the live call flows under the same brand and billing account as the page. If something hangs in the page you can re-create the exact call sequence against the API and rule the surface in (or out).

See also