For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
Log inSign up
Support
GuidesReferenceClick-to-Call
GuidesReferenceClick-to-Call
  • Getting Started
    • Overview
    • Authentication
    • RxJS Primer
    • Migrate from v3
  • Web Components
    • Overview
    • Click-to-Call
    • Theming
    • Customization
  • Build Voice & Video Apps
    • Overview
    • Outbound Calls
    • Inbound Calls
    • Device Management
    • Screen Sharing
    • Call Controls
    • Layouts
    • Messaging & Chat
  • Manage Resources
    • Overview
    • Users
    • Address Book
    • Client Preferences
    • Capabilities
  • Deploy
    • Overview
    • Framework Integration
    • SSR & Next.js
    • Troubleshooting
LogoLogoSignalWire Docs
Log inSign up
Support
On this page
  • Pick a destination
  • Choose audio, video, or both
  • Attach the streams
  • End the call
  • Try it: dial a destination
  • Next steps
Build Voice & Video Apps

Outbound Calls

|View as Markdown|Open in Claude|
Was this page helpful?
Edit this page
Previous

Inbound Calls

Next
Built with

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:

Browser
1import { SignalWire, StaticCredentialProvider } from "@signalwire/js";
2
3const client = new SignalWire(new StaticCredentialProvider({ token: SAT }));
4const call = await client.dial("/public/test-room", { audio: true, video: true });
5
6// Attach the media to the page.
7call.localStream$.subscribe((stream) => (localVideo.srcObject = stream));
8call.remoteStream$.subscribe((stream) => (remoteVideo.srcObject = stream));
9
10// End the call when the user clicks hang up.
11hangupButton.onclick = () => call.hangup();

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).

Pick a destination

The first argument to dial() is a URI string identifying what the call should reach. Four shapes cover the common cases:

Shared resource

/public/<resource> — a resource like a room, IVR, or app anyone in the project can dial.

Specific user

/private/<user> — a registered user (Subscriber) in your project that you can call directly.

Phone number

+15551234567 — a PSTN number. The token must be allowed to dial PSTN.

SIP endpoint

sip:alice@example.com — a SIP destination reachable from your space.

Browser
1const call = await client.dial("/public/test-room");

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.

Choose audio, video, or both

dial() takes a DialOptions object, which extends MediaOptions. The four common shapes:

Audio + video
Audio only
Video, mic muted
Receive only
1const call = await client.dial(destination, { audio: true, video: true });

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.

Attach the streams

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:

Browser
1// These subscriptions complete on their own when the call ends.
2call.localStream$.subscribe((stream) => (localVideo.srcObject = stream));
3call.remoteStream$.subscribe((stream) => (remoteVideo.srcObject = stream));
1<video id="localVideo" autoplay muted playsinline></video>
2<video id="remoteVideo" autoplay playsinline></video>

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.

End the call

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.

Browser
1hangupButton.onclick = () => call.hangup();

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.

Try it: dial a destination

Create a SAT against your project — the Authentication guide covers it end-to-end; the Create Subscriber Token reference sends the request for you.

POST
/api/fabric/subscribers/tokens
1curl -X POST https://{your_space_name}.signalwire.com/api/fabric/subscribers/tokens \
2 -H "Content-Type: application/json" \
3 -u "<project_id>:<api_token>" \
4 -d '{
5 "reference": "john.doe@example.com"
6}'
Try it

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.

outbound-demo.html — full source
1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="utf-8" />
5 <title>SignalWire SDK outbound call demo</title>
6 <style>
7 /* Shared demo shell — identical across the inbound, outbound, and
8 device-management guides. Per-demo extras go below this block. */
9 body { font: 14px/1.5 system-ui, sans-serif; max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
10 label { display: block; margin: 0.75rem 0 0.25rem; font-weight: 600; }
11 input, select { width: 100%; padding: 0.5rem; font: 13px ui-monospace, monospace; box-sizing: border-box; }
12 button { margin: 0.5rem 0.5rem 0 0; padding: 0.5rem 1rem; font: 14px system-ui; cursor: pointer; }
13 button[disabled] { opacity: 0.5; cursor: not-allowed; }
14 .videos { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-top: 1rem; }
15 video { width: 100%; background: #000; border-radius: 4px; aspect-ratio: 4/3; }
16 #log { margin-top: 1rem; padding: 1rem; background: #111; color: #0f0; font: 13px ui-monospace, monospace; min-height: 6rem; white-space: pre-wrap; border-radius: 4px; }
17 /* Outbound-specific */
18 .media-options { margin: 0.75rem 0 0; padding: 0.5rem 0.75rem; border: 1px solid #ccc; border-radius: 4px; }
19 .media-options legend { padding: 0 0.25rem; font-weight: 600; }
20 .media-options label { display: inline-flex; align-items: center; gap: 0.35rem; margin: 0 1rem 0 0; font-weight: 400; }
21 .media-options input[type="checkbox"] { width: auto; padding: 0; }
22 </style>
23 </head>
24 <body>
25 <h1>SignalWire SDK outbound call demo</h1>
26
27 <label for="token">Subscriber Access Token</label>
28 <input id="token" type="password" placeholder="Paste your SAT here" />
29
30 <label for="destination">Destination</label>
31 <input id="destination" type="text" placeholder="/public/test-room" />
32
33 <fieldset class="media-options">
34 <legend>Media</legend>
35 <label><input type="checkbox" id="audio" checked /> Send audio</label>
36 <label><input type="checkbox" id="video" checked /> Send video</label>
37 </fieldset>
38
39 <button id="dial">Dial</button>
40 <button id="hangup" disabled>Hang up</button>
41
42 <div class="videos">
43 <video id="local" autoplay muted playsinline></video>
44 <video id="remote" autoplay playsinline></video>
45 </div>
46
47 <pre id="log"></pre>
48
49 <script type="module">
50 import { SignalWire, StaticCredentialProvider } from "https://esm.sh/@signalwire/js@dev";
51
52 const log = (msg) =>
53 (document.getElementById("log").textContent += msg + "\n");
54
55 let activeCall = null;
56
57 document.getElementById("dial").addEventListener("click", async () => {
58 const token = document.getElementById("token").value.trim();
59 const destination = document.getElementById("destination").value.trim();
60 if (!token || !destination) return log("Need a token and a destination.");
61
62 const audio = document.getElementById("audio").checked;
63 const video = document.getElementById("video").checked;
64
65 const dialBtn = document.getElementById("dial");
66 const hangupBtn = document.getElementById("hangup");
67 dialBtn.disabled = true;
68 log("Dialing " + destination + "...");
69 log("Options: audio=" + audio + ", video=" + video);
70
71 const provider = new StaticCredentialProvider({ token });
72 const client = new SignalWire(provider);
73
74 try {
75 const call = await client.dial(destination, { audio, video });
76 activeCall = call;
77 hangupBtn.disabled = false;
78
79 call.localStream$.subscribe((stream) => {
80 document.getElementById("local").srcObject = stream;
81 });
82 call.remoteStream$.subscribe((stream) => {
83 document.getElementById("remote").srcObject = stream;
84 });
85 call.status$.subscribe((status) => {
86 log("Status: " + status);
87 if (status === "destroyed") {
88 dialBtn.disabled = false;
89 hangupBtn.disabled = true;
90 activeCall = null;
91 }
92 });
93 call.errors$.subscribe((err) => {
94 log("Error: " + (err.name || "Error") + " — " + err.message);
95 });
96 } catch (err) {
97 log("dial() rejected: " + (err.name || "Error") + " — " + err.message);
98 dialBtn.disabled = false;
99 }
100 });
101
102 document.getElementById("hangup").addEventListener("click", () => {
103 if (activeCall) activeCall.hangup();
104 });
105 </script>
106 </body>
107</html>

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.

Next steps

Inbound Calls

Receive calls in a signed-in user session.

Device Management

Choose the mic, camera, and speaker the call should use.

DialOptions reference

Every option you can pass to client.dial().