Browser Calling with WebRTC
Browser calling with WebRTC on Sautikit: mint a short-lived token on your server, register the @sautikit/webrtc SDK and place calls at KES 3.00 a minute.
Browser calling with WebRTC on Sautikit: mint a short-lived token on your server, register the @sautikit/webrtc SDK and place calls at KES 3.00 a minute.
Browser calling with Sautikit uses a short-lived WebRTC token. Your backend mints it with POST /v1/webrtc/token using your API key and returns it to a signed-in browser session. The @sautikit/webrtc SDK registers with the token and can then place or receive calls. Calls are billed to your wallet like any other call, at KES 3.00 a minute.
Your API key must never reach the browser. Your server holds the key, mints a token, and returns only the token response.
curl -s -X POST "https://api.sautikit.com/v1/webrtc/token" \
-H "Authorization: Bearer $SAUTIKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone_number": "+254720000000"}' | jq .The body must be valid JSON; send {} at minimum. phone_number (or tenant_number_id) binds the session to one of your numbers, so calls from this browser go out through that number.
Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"endpoint": "wss://...",
"protocol": "...",
"clientName": "...",
"turnServer": { "iceServers": [] }
}Pass the whole object to the browser. endpoint, protocol and turnServer are supplied by the server so you never hard-code them. Tokens are short-lived (typically 60 to 300 seconds) and are only needed to register.
Put the mint call behind your own session check so only your users can request tokens.
import express from "express";
const app = express();
// requireAuth is your existing session middleware
app.post("/api/webrtc-token", requireAuth, async (req, res) => {
const resp = 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({ phone_number: "+254720000000" }),
});
if (!resp.ok) {
console.error("token mint failed:", await resp.text());
return res.status(502).json({ error: "token_mint_failed" });
}
// Each request must mint a fresh token
res.set("Cache-Control", "no-store");
res.json(await resp.json());
});npm i @sautikit/webrtcimport { Client } from "@sautikit/webrtc";
const session = await fetch("/api/webrtc-token", { method: "POST" }).then((r) => r.json());
const client = new Client({
token: session.token,
endpoint: session.endpoint,
protocol: session.protocol,
turnServer: session.turnServer,
});
client.on("registered", () => updateUI("Ready"));
client.on("error", (err) => console.error(err));Wait for registered before dialling.
call() asks for microphone permission, so run it from a click handler, not on page load.
client.on("ringing", () => updateUI("Ringing..."));
client.on("answered", () => updateUI("Connected"));
client.on("hangup", (e) => updateUI(`Call ended: ${e.reason}`));
document.getElementById("call-btn").addEventListener("click", () => {
client.call("+254720000001");
});
document.getElementById("mute-btn").addEventListener("click", () => client.mute());
document.getElementById("unmute-btn").addEventListener("click", () => client.unmute());
document.getElementById("hangup-btn").addEventListener("click", () => client.hangup());To ring a browser for inbound calls, see Route inbound calls to a browser WebRTC client. The SDK then fires incoming:
client.on("incoming", ({ from }) => {
showIncomingCallUI({
from,
onAnswer: () => client.accept(),
onReject: () => client.reject("busy"),
});
});The SDK retries a dropped connection by itself. When it gives up it fires closed: mint a fresh token and build a new Client.
client.on("closed", () => startSession()); // your function from Step 3
window.addEventListener("beforeunload", () => client.destroy());/api/webrtc-token without it.Place a test call from the browser, then fetch the call record:
curl -s "https://api.sautikit.com/v1/calls?direction=outbound&limit=1" \
-H "Authorization: Bearer $SAUTIKIT_API_KEY" | \
jq '.data[0] | {id, direction, status, local_e164, remote_e164, duration_seconds}'The record shows direction: "outbound". The call is charged to your wallet when it ends, at the same rate as a call placed through the API: KES 3.00 a minute, billed per second after it is answered.