Browser / WebRTC SDK
Browser SDK for in-page calling. Mint a SIP token server-side, then place and receive calls with @sautikit/webrtc. Preview; pin your version.
Browser SDK for in-page calling. Mint a SIP token server-side, then place and receive calls with @sautikit/webrtc. Preview; pin your version.
@sautikit/webrtc puts a phone in the browser. Your server mints a short-lived SIP token, the page hands it to a Client, and the SDK owns the WebSocket, the SIP registration, and the RTCPeerConnection — you call client.call("+2547…") and listen for events.
It is browser-only: it needs RTCPeerConnection and getUserMedia, so it does not run under Node. For server-side calling use the Node.js SDK.
npm i @sautikit/webrtcThe package is ESM-only and ships its own types. It has no runtime dependencies.
The browser must never hold your API key. Mint a SIP token server-side and return it to the page:
// Your backend — POST /v1/webrtc/token with your bearer API key.
const res = await fetch("https://api.sautikit.com/v1/webrtc/token", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SAUTIKIT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
const session = await res.json();
// { token, endpoint, protocol, turnServer, clientName, sipProfile, expiresIn }Pass the whole object through to the browser. endpoint, protocol and turnServer are all server-supplied — hand them to the Client rather than hard-coding them, so a change to the calling infrastructure does not require a front-end release.
import { Client, WebRTCError } from "@sautikit/webrtc";
const client = new Client({
token: session.token,
endpoint: session.endpoint,
protocol: session.protocol,
turnServer: session.turnServer,
});
client.on("registered", () => console.log("ready to dial"));
client.on("ringing", () => console.log("ringing…"));
client.on("answered", () => console.log("connected"));
client.on("hangup", (e) => console.log("call ended:", e.reason));
client.on("error", (err: WebRTCError) => console.error(err));
await client.call("+254712345678");call() prompts for microphone permission, so it must run from a user gesture — a click handler, not on page load.
client.on("incoming", async ({ from }) => {
if (confirm(`Incoming call from ${from}. Answer?`)) {
await client.accept();
} else {
client.reject("busy");
}
});| Option | Type | Notes |
|---|---|---|
token | string | Required. Short-lived JWT from POST /v1/webrtc/token. |
endpoint | string | WebSocket endpoint. Use the value from the token response. |
protocol | string | Sec-WebSocket-Protocol name. Use the value from the token response. |
turnServer | RTCIceServer | RTCIceServer[] | ICE/TURN servers. Falls back to public STUN, which is not enough on many mobile networks — pass the server-supplied value. |
audioElement | HTMLAudioElement | Audio sink. Defaults to audio#remoteAudio, else a hidden element appended to <body>. |
debug | DebugLevel | Console verbosity. Defaults to 'none'. |
supervision | SupervisionBackend | Your server's start / setMode / stop relay. Enables client.supervise(). |
supervisionAutoAnswer | boolean | Answer the supervisor leg this client asked for automatically. Defaults to true. |
| Method | Purpose |
|---|---|
call(to) | Place an outbound call to an E.164 number. |
accept() | Answer the current inbound call. |
reject(reason?) | Decline the current inbound call. |
hangup() | End the active call. |
mute() / unmute() | Toggle the local microphone track. |
dtmf(digits) | Send DTMF tones on the active call. |
reconnect() | Force a fresh handshake. |
destroy() | Tear down the socket, peer connection and media tracks. |
getAudioStreams() | { localStream, remoteStream }, for meters or visualisers. |
supervise(callId, opts) | Supervise a live call from this browser. Returns a Supervision with setMode(), stop() and an ended event. |
Subscribe with client.on(name, cb) and remove with client.off(name).
| Event | Fires when |
|---|---|
connected | WebSocket handshake completed. |
registered | Registered with the calling platform — safe to dial. |
ringing | Outbound leg is ringing. |
answered | Far end answered. |
accepted | Far end acknowledged post-answer. |
declined | Far end declined the invite. |
incoming | Inbound call offered — payload carries from and sessionId. |
hangup | Call ended, for any reason. |
reconnecting | About to retry the handshake. |
registration_failed | Registration rejected; the SDK retries up to 5 times. |
closed | Retry budget exhausted — mint a fresh token and build a new Client. |
error | Recoverable error, as a WebRTCError. |
supervision | Agent side: a supervisor joined, changed mode on, or left the call this tab answered. |
A supervisor can listen to, whisper into or barge on a live call from the browser. The page cannot call the Sautikit API itself, so pass a supervision backend that relays start, mode changes and stop through your server, then:
const sup = await client.supervise("HD_24d73b4518bc", { mode: "listen", label: "Mary Wanjiku" });
sup.on("ended", ({ reason }) => console.log("supervision ended:", reason));
// Later, once the supervisor leg has been answered:
await sup.setMode("whisper");The SDK answers the supervisor leg it asked for by itself. On the agent's side, client.on("supervision", …) tells the tab that answered the call when a supervisor joins, changes mode or leaves. The server routes, the backend adapter and the agent banner are covered step by step in Supervise live calls.
Tokens currently expire after the expiresIn seconds returned by the mint endpoint. Treat closed as the signal to re-mint: fetch a new token and construct a new Client rather than reusing the old one.