To place a call from a web app, hand the SDK a destination, choose whether to send audio, video, or both, and attach the resulting media streams to the page. The same client.dial() call works for joining a room, calling another user, or reaching a phone number — only the destination string changes.
Here’s the shape of an outbound call end-to-end:
Before you start. You’ll need a Subscriber Access Token your backend issued for the user, a destination the token is allowed to reach, and an HTTPS page (browsers only grant mic and camera access over a secure origin — localhost is the development exception).
The first argument to dial() is a URI string identifying what the call should reach. Four shapes cover the common cases:
/public/<resource> — a resource like a room, IVR, or app anyone in the project can dial.
/private/<user> — a registered user (Subscriber) in your project that you can call directly.
+15551234567 — a PSTN number. The token must be allowed to dial PSTN.
sip:alice@example.com — a SIP destination reachable from your space.
If you’re iterating addresses from the directory, an Address object exposes defaultChannel — a ready-to-dial URI for that address — so you don’t have to assemble the string yourself.
dial() takes a DialOptions object, which extends MediaOptions. The four common shapes:
Standard video call. Both tracks captured from the selected mic and camera.
The destination URI can also carry a ?channel=audio or ?channel=video hint that sets the matching media defaults — useful when the destination URI is what determines the call shape. Explicit options passed to dial() always win.
For pinned device constraints, codec preference, your own MediaStream, or custom invite metadata, see DialOptions. For mic, camera, or speaker selection that persists across every call, use the device management APIs instead of constraining each dial() individually.
A Call exposes two media observables: localStream$ (what the user is sending) and remoteStream$ (what the user receives). Both emit a MediaStream once the track is ready — bind each one to a <video> element’s srcObject:
The local element needs muted so the user doesn’t echo their own voice; the remote element must not be muted or no one is heard. Both need playsinline for mobile Safari. Audio-only calls use the same remoteStream$ — keep the <video> element and the browser plays the audio track through it.
Call hangup() when the user clicks the hang-up button or navigates away. The call transitions through disconnecting → disconnected → destroyed; any subscriptions on the call complete naturally.
If the user closes the tab without calling hangup(), the SDK still tears the call down when the page unloads. Calling hangup() explicitly gives you a clean point to dismiss the in-call UI before the connection drops.
To leave the page but keep the call alive on the platform — a transfer-and-disappear flow — use transfer() instead.
Create a SAT against your project — the Authentication guide covers it end-to-end; the Create Subscriber Token reference sends the request for you.
Copy the returned token, save the page below as outbound-demo.html, and open it over HTTPS (or localhost). Paste the SAT and a destination, toggle the Send audio / Send video checkboxes to match the call shape you want, click Dial, and watch the log — it records every status the call moves through. The checkboxes map directly onto the audio and video keys of dial()’s DialOptions.
You should see Status: connected once the destination picks up. If dial() rejects with CallCreateError, the token’s scope doesn’t reach the destination — re-check allowed_addresses or the token’s project.