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
  • Authentication patterns
  • How the SDK gets its credential
  • Embed tokens (in-page)
  • Server-fetched SATs
  • Refreshing SATs
  • Server-side refresh
  • Client-side refresh
  • When both paths are configured
  • Connection lifecycle
  • Going online and offline
  • Try it: create a SAT and connect
  • Next steps
Getting Started

Authentication

Learn how to authenticate with the browser SDK client
|View as Markdown|Open in Claude|
Was this page helpful?
Edit this page
Previous

RxJS Primer

Next
Built with

The SDK acts on behalf of a user, whether that’s a full-access Subscriber or a guest with limited access. It authenticates with a Subscriber Access Token (SAT), a short-lived credential that identifies that user and carries the capabilities granted to them. Your backend creates the SAT using your Project API Token, then hands it to the browser, where the SDK uses it to open a WebSocket session with SignalWire.

Browser
1import { SignalWire, StaticCredentialProvider } from "@signalwire/js";
2
3const client = new SignalWire(
4 new StaticCredentialProvider({ token: "<your SAT>" })
5);

The sections below cover which kind of SAT to create, how to deliver it to the browser, and how to keep the session alive past the SAT’s expiry.

Before you start. You need a SignalWire space, a Project ID, and an API token with at least one of the Voice / Messaging / Fax / Video scopes. All three are in the API Credentials section of the dashboard. The Project API Token is what creates SATs. Keep it server-side only.

Authentication patterns

The SDK supports four authentication patterns, each shaped by who is holding the credential and what they need to do with it.

Authenticated users

For apps where users sign in with an account. Each user can place and receive calls.

Guest users

For users without an account who need limited calling, typically to a short list of destinations you allow.

Public usage

For embedding a “call us” button on a public webpage. Anyone visiting can dial one preset destination.

Call invite

For giving a specific recipient a way to connect to a call through a shareable invite.

Each pattern uses a different credential: three Subscriber Access Token (SAT) flavors issued for a user, guest, or invitee, and a separate Embed token for public widgets. The credential’s capabilities determine what the holder can do:

PatternCredentialInbound callsOutbound callsDestinationsAudience
Authenticated usersSubscriber Access Token✓✓Anywhere the user can reachOne signed-in user
Guest usersGuest SAT✗✓A list of allowed destinations you set (max 10)One guest user with scoped capabilities
Call inviteInvite SAT✗✓The inviting user’s addressOne invitee
Public usageEmbed token✗✓Tied to a single resourceAnyone visiting a public page

Match the credential’s reach to the trust level of whoever holds it. If a credential can dial anyone, then anyone who can read it can dial anyone, so use the delivery model below that keeps the credential out of untrusted hands.

How the SDK gets its credential

Credentials reach the SDK one of two ways. Embed tokens live in the page itself. Every other variant is created server-side and handed to the browser; the only thing that differs is whether the SDK should keep the session alive past the credential’s first expiry.

Embed tokens (in-page)

Embed tokens are the only credential designed to sit in a public page. They are pinned to one Click-to-Call resource: anyone who reads the page can only dial the resource the embed token was created for. That fixed scope is what makes them safe to expose to every visitor.

Getting an embed token is a two-step setup:

  1. Create a Click-to-Call resource in your SignalWire dashboard. The dashboard issues a Click-to-Call (C2C) token (with a c2c_ prefix) tied to that resource.
  2. Exchange the C2C token for an embed token by calling POST /api/embeds/tokens. The embed token is what the SDK is built to consume for public widgets.

The SDK also accepts a C2C token directly as a shortcut (convenient for testing), but production widgets should pass the exchanged embed token. The examples below use the shortcut form so they can run with only the dashboard value.

Shortcut for a single call. embeddableCall() handles credential exchange, client construction, and dial in one call:

Browser
1import { embeddableCall } from "@signalwire/js";
2
3const call = await embeddableCall({
4 host: "yourspace.signalwire.com",
5 embedToken: "c2c_7acc0e5e968706a032983cd80cdca219",
6 to: "/public/support",
7});

Full SDK setup for multiple calls or client-level subscriptions. Pass EmbedTokenCredentialProvider to the SDK directly. You keep a long-lived SignalWire client that can dial repeatedly and exposes observables you can subscribe to. The provider exchanges the embed token for a Guest SAT and refreshes automatically:

Browser
1import { SignalWire, EmbedTokenCredentialProvider } from "@signalwire/js";
2
3const client = new SignalWire(
4 new EmbedTokenCredentialProvider(
5 "yourspace.signalwire.com",
6 "c2c_7acc0e5e968706a032983cd80cdca219"
7 )
8);
9
10const call = await client.dial("/public/support");

Server-fetched SATs

The browser asks for a SAT, your backend creates one using the Project API Token, and the SDK uses it for the session.

The browser never talks to SignalWire directly here. Creating any SAT requires the Project API Token, which can issue a SAT for any user in your project. That is why it stays server-side. The hop through your backend is what enforces “this browser session can only get the SAT it’s authorized for.”

Three SAT variants come through this path:

  • Subscriber Access Token (POST /api/fabric/subscribers/tokens): full user identity for a signed-in user; can receive inbound calls. Also called a default-scope SAT when you need to distinguish it from the variants below.
  • Guest SAT (POST /api/fabric/guests/tokens): outbound-only, pinned to up to 10 allowed_addresses.
  • Invite SAT (POST /api/fabric/subscriber/invites): outbound-only, pinned to one address; created client-side by a signed-in user and delivered out-of-band (URL, email, QR code) to one recipient.

Once the SAT is in the browser, the next decision is whether the session needs to outlive a single SAT. For one-shot sessions (typical for Guest and Invite SATs), use StaticCredentialProvider. The SDK uses the fetched SAT until it expires, then the session ends. For sessions that must outlive a single SAT, pick a refresh strategy below.

Refreshing SATs

SATs are short-lived (two hours by default), which limits the damage if one ever leaks. When a SAT expires, the SDK’s WebSocket session ends with it unless a fresh SAT is supplied first. Refreshing is the process of swapping in a fresh SAT before the current one expires, so the session continues uninterrupted: the user stays connected, ongoing calls aren’t dropped, and they don’t need to re-authenticate.

There are two ways to refresh a SAT, depending on where the rotation logic should live.

Server-side refresh

The backend rotates the SAT. Your CredentialProvider exposes a refresh() method that fetches a fresh SAT from your backend; the SDK calls it shortly before the current SAT’s expiry_at. Every rotation roundtrips through your backend.

Browser
1import { SignalWire } from "@signalwire/js";
2import type { CredentialProvider } from "@signalwire/js";
3
4class BackendSAT implements CredentialProvider {
5 async authenticate() {
6 const r = await fetch("/api/signalwire-token", {
7 method: "POST",
8 // `credentials: "include"` tells fetch to send the browser's cookies with
9 // the request, so your backend reads its own session cookie and knows
10 // which signed-in user is asking for a token.
11 credentials: "include",
12 });
13 const { token, expiresAt } = await r.json();
14 // expiry_at is a Date.now()-style millisecond timestamp.
15 return { token, expiry_at: expiresAt };
16 }
17
18 refresh() {
19 return this.authenticate();
20 }
21}
22
23const client = new SignalWire(new BackendSAT());

Inside /api/signalwire-token, your backend produces the fresh SAT one of two ways:

Rotate the refresh token (recommended)
Re-issue against the user's session

Every SAT comes back with a companion refresh_token. Your backend stores it and swaps it for a new SAT/refresh-token pair via POST /api/fabric/subscribers/tokens/refresh. This keeps the session going without re-checking the user’s app session on every rollover.

Server (Node.js)
1app.post("/api/signalwire-token", async (req, res) => {
2 // Look up the refresh_token you stored when this user was created.
3 const stored = await getRefreshTokenForUser(req.user.id);
4
5 // Swap that refresh_token for a new SAT + new refresh_token pair.
6 const r = await fetch(`https://${SPACE}/api/fabric/subscribers/tokens/refresh`, {
7 method: "POST",
8 headers: { Authorization: BASIC_AUTH, "Content-Type": "application/json" },
9 body: JSON.stringify({ refresh_token: stored }),
10 });
11 const { token, refresh_token } = await r.json();
12
13 // Save the rotated refresh_token so the next call can swap it too.
14 await saveRefreshTokenForUser(req.user.id, refresh_token);
15
16 // expiresAt assumes the 2h default; if you set `expire_at` when you created the SAT, compute from that.
17 res.json({ token, expiresAt: Date.now() + 2 * 60 * 60 * 1000 });
18});

The new access token carries the standard SAT lifetime; the new refresh token outlives it by five minutes so the swap has slack. Store refresh tokens encrypted, server-side only.

Client-side refresh

The SDK rotates the SAT directly with SignalWire after it is first issued. Your backend is involved only at startup.

This path binds the SAT to the browser session that requested it. The SDK provides a public fingerprint at authentication time; the backend includes that fingerprint plus scope: "sat:refresh" on the create request. Refresh calls are then signed against the matching private key the browser holds, so a SAT lifted off the wire can’t be rotated from anywhere else.

Browser
1import { SignalWire } from "@signalwire/js";
2import type { CredentialProvider, AuthenticateContext } from "@signalwire/js";
3
4class BackendSAT implements CredentialProvider {
5 async authenticate(context?: AuthenticateContext) {
6 const r = await fetch("/api/signalwire-token", {
7 method: "POST",
8 // `credentials: "include"` tells fetch to send the browser's cookies with
9 // the request, so your backend reads its own session cookie and knows
10 // which signed-in user is asking for a token.
11 credentials: "include",
12 headers: { "content-type": "application/json" },
13 // Forward the SDK's fingerprint so the backend can issue a SAT bound to this browser.
14 body: JSON.stringify({ fingerprint: context?.fingerprint }),
15 });
16 const { token, expiresAt } = await r.json();
17 return { token, expiry_at: expiresAt };
18 }
19
20 // No refresh() — rotation happens directly between the SDK and SignalWire after the SAT is first issued.
21}
22
23const client = new SignalWire(new BackendSAT());

On the backend side, forward the fingerprint and request the refresh scope when creating the SAT:

Server (Node.js)
1app.post("/api/signalwire-token", requireUserAuth, async (req, res) => {
2 // `requireUserAuth` reads the session cookie and populates `req.user` with
3 // the signed-in app user; `req.body.fingerprint` was forwarded by the SDK.
4 const r = await fetch(`https://${SPACE}/api/fabric/subscribers/tokens`, {
5 method: "POST",
6 headers: { Authorization: BASIC_AUTH, "Content-Type": "application/json" },
7 body: JSON.stringify({
8 reference: req.user.email, // identifies the SignalWire user
9 fingerprint: req.body.fingerprint, // binds the SAT to this browser
10 scope: "sat:refresh", // lets the SDK refresh without your backend
11 }),
12 });
13 const { token } = await r.json();
14 res.json({ token, expiresAt: Date.now() + 2 * 60 * 60 * 1000 });
15});

For the rotation endpoints, refresh events you can subscribe to, and failure modes, see CredentialProvider.

When both paths are configured

You can provide a refresh() method and request sat:refresh scope. The SDK picks one mechanism per session: if the SAT carries sat:refresh scope, the client-side (Client Bound SAT) path wins and your refresh() is never called; otherwise it falls back to your refresh(). This makes refresh() a safe backstop — if the backend ever drops the scope, the session keeps rotating instead of dying at expiry.

That fallback is silent by default. To observe it — for example, to alert when a deployment expected to use bound tokens has downgraded to a developer-managed refresh — subscribe to client.warnings$:

Browser
1client.warnings$.subscribe((warning) => {
2 if (warning.code === "credential_refresh_fallback") {
3 // e.g. reason: "no-scope" — the SAT was minted without sat:refresh
4 console.warn("Refresh fell back to developer refresh():", warning.reason);
5 }
6 if (warning.code === "credential_no_refresh_handler") {
7 // Token has an expiry but no refresh path — session ends at expiry.
8 console.warn("Session will end at", new Date(warning.expiresAt));
9 }
10});

Connection lifecycle

Constructing SignalWire runs three steps in sequence: authenticate the SAT, open the WebSocket, and register the user as online. Each step runs by default, and each can be deferred with a constructor option in SignalWireOptions so your UI can drive it later.

StepDefaultDefer withRun later with
Open the WebSocketrunsskipConnection: trueclient.connect()
Register as onlinerunsskipRegister: trueclient.register()
Persist across reloadsoffpersist: true(constructor only)
Browser
1const client = new SignalWire(credentialProvider, {
2 skipConnection: true,
3 skipRegister: true,
4});
5
6await client.connect(); // open the WebSocket when the user opts in
7await client.register(); // come online for inbound calls

Going online and offline

register() tells SignalWire the user is online on this session, so inbound calls and presence events route here. It runs automatically when the client constructs unless skipRegister: true is set — defer it when the user needs to opt in (microphone prompt, “Go online” toggle, permissions step) before they start receiving calls.

unregister() is the opposite: the user goes offline for inbound calls, but the WebSocket stays open so outbound calls and observable subscriptions keep working. Use it for Do Not Disturb, app-background, or “available / away” toggles.

Browser
1await client.unregister(); // go offline; socket stays open
2await client.register(); // come back online later

Closing the session entirely is a separate step. disconnect() closes the WebSocket, and destroy() wipes persisted state on explicit logout.

Only credentials issued with full user (Subscriber) identity can register. Guest SATs, Invite SATs, and embed-derived Guest SATs are outbound-only, so register() is a no-op on those clients — inbound calls require a full Subscriber Access Token.

Try it: create a SAT and connect

Create a Subscriber Access Token (SAT) for your project using the request snippet below. Have your space name and an API token ready, with at least one of the Voice / Messaging / Fax / Video scopes. Both come from the API Credentials section of the SignalWire dashboard. Open the Create Subscriber Token reference to send the request with your space and credentials filled in.

The Project API Token can issue a SAT for any user in your project. Use a development project, or rotate the API token afterward.

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 auth-demo.html, and open it in a browser. Paste the SAT into the input, click Authenticate, and watch the log. It reports whether the SDK was able to open a session with the SAT.

auth-demo.html — full source
1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="utf-8" />
5 <title>SignalWire SDK auth demo</title>
6 <style>
7 body { font: 14px/1.5 system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
8 label { display: block; margin: 0.75rem 0 0.25rem; font-weight: 600; }
9 input { width: 100%; padding: 0.5rem; font: 13px ui-monospace, monospace; box-sizing: border-box; }
10 button { margin-top: 0.75rem; padding: 0.5rem 1rem; font: 14px system-ui; cursor: pointer; }
11 button[disabled] { opacity: 0.5; cursor: wait; }
12 #log { margin-top: 1rem; padding: 1rem; background: #111; color: #0f0; font: 13px ui-monospace, monospace; min-height: 5rem; white-space: pre-wrap; border-radius: 4px; }
13 </style>
14 </head>
15 <body>
16 <h1>SignalWire SDK auth demo</h1>
17
18 <label for="token">Subscriber Access Token</label>
19 <input id="token" type="password" placeholder="Paste your SAT here" />
20 <button id="connect">Authenticate</button>
21
22 <pre id="log"></pre>
23
24 <script type="module">
25 import { SignalWire, StaticCredentialProvider } from "https://esm.sh/@signalwire/js@dev";
26
27 const log = (msg) =>
28 (document.getElementById("log").textContent += msg + "\n");
29
30 document.getElementById("connect").addEventListener("click", () => {
31 const token = document.getElementById("token").value.trim();
32 if (!token) return log("Paste a token first.");
33
34 const button = document.getElementById("connect");
35 button.disabled = true;
36 log("Connecting...");
37
38 const provider = new StaticCredentialProvider({ token });
39 const client = new SignalWire(provider);
40
41 const readySub = client.ready$.subscribe((ready) => {
42 if (ready) {
43 log("Authenticated — WebSocket open.");
44 readySub.unsubscribe(); // one-shot: stop after the first ready signal
45 }
46 });
47
48 const errorsSub = client.errors$.subscribe((err) => {
49 log("Failed: " + (err.name || "Error") + " — " + err.message);
50 button.disabled = false;
51 errorsSub.unsubscribe(); // when you're done
52 });
53 });
54 </script>
55 </body>
56</html>

You should see Authenticated — WebSocket open. in the log. If you see Failed: InvalidCredentialsError, the SAT is expired, malformed, or issued for a different SignalWire space than the SDK is connecting to. Create a fresh one and try again.

Next steps

Inbound Calls

Receive incoming calls in a signed-in user session.

Outbound Calls

Dial users, rooms, or PSTN destinations.

Subscribers

Platform concept: who a credential represents and what addresses they can reach.