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
  • Connection issues
  • InvalidCredentialsError on connect
  • Connection fails without an obvious error
  • NotConnectedError when calling dial()
  • WebSocket disconnects frequently
  • Video / Audio issues
  • Video is black
  • No remote audio
  • Remote can’t hear me
  • Echo or feedback
  • Permission denied for camera/microphone
  • Call issues
  • Call stays in trying state
  • Call connects but no media flows
  • Can’t receive inbound calls
  • DTMF tones not working
  • UI issues
  • Video element shows nothing
  • UI doesn’t update when state changes
  • Memory leak / page slows down
  • Browser-specific issues
  • Safari: video doesn’t play
  • Firefox: no audio output selection
  • Mobile: camera switches unexpectedly
  • Debugging
  • Verbose logging
  • Inspect WebSocket traffic
  • Get call statistics
  • Test without real media
  • FAQ
  • Do I need HTTPS?
  • What browsers are supported?
  • Can I use this in Node.js?
  • How do I implement a mute button?
  • How do I get call duration?
  • Can I record calls?
  • Why “Unimplemented” errors?
Deploy

Troubleshooting & FAQ

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

Common failure modes and how to diagnose them. For end-to-end patterns, see the guides; for full API contracts, see the reference.

Connection issues

InvalidCredentialsError on connect

Cause: Token is expired, malformed, or minted for a different SignalWire space.

Fix:

  1. Mint a fresh SAT from your backend — see Authentication.
  2. Confirm the token was issued by the space you’re connecting to.
  3. Check that the full token string was copied (SATs are long; truncation is common).
1import { jwtDecode } from "jwt-decode";
2
3const decoded = jwtDecode(token);
4const expiresAt = new Date(decoded.exp * 1000);
5console.log("Expired?", expiresAt < new Date());

Connection fails without an obvious error

Errors that happen outside of an await flow surface on errors$. If you never subscribe to it, those errors are silently dropped — subscribe during client construction so they always reach your logs.

1client.errors$.subscribe((error) => console.error("Client error:", error));

NotConnectedError when calling dial()

Cause: Calling dial() before the client finished connecting.

Wait for ready$ to emit true:

1import { filter, take } from "rxjs";
2
3client.ready$.pipe(filter(Boolean), take(1)).subscribe(async () => {
4 const call = await client.dial(destination);
5});

WebSocket disconnects frequently

Causes: Unstable network, corporate firewall/proxy blocking WebSocket, idle timeout.

The SDK reconnects automatically. Drive a “reconnecting” banner off isConnected$:

1client.isConnected$.subscribe((connected) => {
2 connected ? hideReconnectingBanner() : showReconnectingBanner();
3});

Video / Audio issues

Video is black

Causes: Camera permissions denied, camera in use by another app, wrong camera selected, hardware issue.

1const permission = await navigator.permissions.query({ name: "camera" });
2console.log("Camera permission:", permission.state);
3
4client.videoInputDevices$.subscribe((devices) => {
5 console.log("Available cameras:", devices);
6});

No remote audio

Causes: Output device not selected, remote participant muted, browser blocking unmuted autoplay, or the muted attribute left on the <video> element.

1<!-- Wrong — the `muted` attribute mutes playback regardless of the track -->
2<video id="remote" autoplay muted></video>
3
4<!-- Right -->
5<video id="remote" autoplay playsinline></video>

Browsers block autoplay with audio until the page has been interacted with — play() will reject. Catch that promise and surface a play-to-start button if it fails.

Remote can’t hear me

1call.self$.subscribe((self) => console.log("Audio muted:", self?.audioMuted));
2
3client.selectedAudioInputDevice$.subscribe((device) => {
4 console.log("Microphone:", device?.label);
5});

Echo or feedback

Use headphones, or enable echo cancellation:

1call.self$.subscribe(async (self) => {
2 if (self && !self.echoCancellation) await self.toggleEchoCancellation();
3});

Permission denied for camera/microphone

HTTPS is required. getUserMedia only works on secure contexts (HTTPS or localhost).

1try {
2 await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
3} catch (e) {
4 if (e.name === "NotAllowedError") {
5 alert("Click the lock icon in your address bar to reset permissions.");
6 }
7}

Call issues

Call stays in trying state

Causes: Invalid destination, destination not reachable, network/firewall blocking.

1call.errors$.subscribe((error) => console.error("Call error:", error));

Call connects but no media flows

Cause: ICE connection failed (firewall blocking UDP) or TURN server unreachable.

1const pc = call.rtcPeerConnection;
2console.log("ICE state:", pc?.iceConnectionState);
3pc?.addEventListener("iceconnectionstatechange", () => {
4 console.log("ICE state changed:", pc.iceConnectionState);
5});

Can’t receive inbound calls

Causes: register() was never called, or the token can’t register. Only full Subscriber Access Tokens can register; Guest SATs, Invite SATs, and embed tokens are outbound-only.

1client.ready$.subscribe(async (ready) => {
2 if (ready) await client.register();
3});

Inbound calls then surface on session.incomingCalls$.

DTMF tones not working

Send digits only after the call is connected:

1import { filter, take } from "rxjs";
2
3call.status$
4 .pipe(filter((s) => s === "connected"), take(1))
5 .subscribe(async () => await call.sendDigits("123#"));

UI issues

Video element shows nothing

1call.remoteStream$.subscribe((stream) => {
2 const video = document.getElementById("remoteVideo");
3 if (stream && video) {
4 video.srcObject = stream;
5 video.play().catch((e) => console.error("Play failed:", e));
6 }
7});

UI doesn’t update when state changes

Subscribe immediately after getting the object — BehaviorSubjects emit current state on subscribe.

Memory leak / page slows down

Almost always a missing unsubscribe on a long-lived subscription. See RxJS Primer → Cleanup and the framework patterns in Framework Integration.

Browser-specific issues

Safari: video doesn’t play

Safari has strict autoplay policies. Add playsinline and handle the play promise:

1<video id="remote" autoplay playsinline></video>
1call.remoteStream$.subscribe(async (stream) => {
2 const video = document.getElementById("remote");
3 video.srcObject = stream;
4 try {
5 await video.play();
6 } catch {
7 showPlayButton(() => video.play());
8 }
9});

Firefox: no audio output selection

Firefox doesn’t fully support setSinkId. Audio plays through default output.

Mobile: camera switches unexpectedly

Device rotation or app switching can reset the camera.

1client.videoInputDevices$.subscribe((devices) => {
2 const preferred = devices.find((d) => d.label.includes("front"));
3 if (preferred) call.self?.selectVideoInputDevice(preferred);
4});

Debugging

Verbose logging

The SDK emits debug-level logs to the browser console. Filter by signalwire in DevTools to isolate them.

Inspect WebSocket traffic

DevTools → Network → “WS” filter → click the connection → Messages tab.

Get call statistics

call.rtcPeerConnection is the underlying RTCPeerConnection — it’s undefined until media negotiation starts, so guard before reading it.

1const stats = await call.rtcPeerConnection?.getStats();
2stats?.forEach((report) => {
3 if (report.type === "inbound-rtp" && report.kind === "video") {
4 console.log("Packets received:", report.packetsReceived);
5 console.log("Packets lost:", report.packetsLost);
6 }
7});

Test without real media

Chrome flag: --use-fake-device-for-media-stream. Or generate a canvas stream:

1const canvas = document.createElement("canvas");
2canvas.getContext("2d").fillRect(0, 0, 640, 480);
3const fakeStream = canvas.captureStream(30);

FAQ

Do I need HTTPS?

Yes for production. WebRTC’s getUserMedia requires a secure context. Localhost is exempt for development.

What browsers are supported?

Modern Chrome, Firefox, Safari, and Edge. No IE11.

Can I use this in Node.js?

No — the SDK is browser-only. Use the SignalWire REST APIs or server-side SDKs.

How do I implement a mute button?

1muteButton.onclick = () => call.self?.toggleMute();

call.self is null until the local participant joins — always check.

How do I get call duration?

1let startTime;
2call.status$.subscribe((status) => {
3 if (status === "connected") startTime = Date.now();
4 if (status === "disconnected" && startTime) {
5 console.log("Lasted:", Math.round((Date.now() - startTime) / 1000), "s");
6 }
7});

Can I record calls?

Recording is controlled by the SignalWire platform, not the browser. startRecording() is on the API surface but not yet implemented in v4 and will throw when called. Recording state configured server-side still surfaces through recording$.

Why “Unimplemented” errors?

Some methods are on the API surface but pending implementation, and others depend on capabilities the server hasn’t granted to this call. Check capabilities$ to gate the UI on what’s actually available.