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.
@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'. |
| 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. |
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. |
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.