Browser calling lets a user place or receive a phone call through their web browser using WebRTC, without needing a phone handset or a separate VoIP app. Sautikit implements this via a short-lived SIP token that your server mints per session and hands to the browser. The browser SDK uses the token to authenticate against Sautikit's SIP gateway, which bridges the WebRTC audio to the public telephone network. All billing and call records are attributed to your workspace.
The API key never leaves your server. Only the short-lived SIP token (5-minute TTL) is handed to the browser. Even if intercepted, an expired token grants no access. Each browser session should receive its own token; do not share one token across multiple users or tabs.
Your token endpoint should be behind your own authentication layer so that only logged-in users receive tokens. A user who obtains a valid token can place calls that are billed to your workspace, so treat the token endpoint with the same care as any billing-sensitive route.
The browser SDK can also receive inbound calls. When a call arrives at a Sautikit number whose routing webhook returns a Dial action targeting a SIP URI associated with the browser session, the SDK fires a client.on("incoming", (call) => ...) event. The user can accept or reject the call from the UI.
Endpoints you call (server-side):
POST /v1/sip/token: mint a short-lived SIP token for the browser session.GET /v1/calls: list calls placed by browser sessions for CDR and billing review.GET /v1/calls/{call_sid}: retrieve individual call detail records.Browser SDK (client-side):
new SautikitClient({ tokenUrl }): initialise with your token endpoint URL.client.connect(): establish the WebSocket connection to the SIP gateway.client.call(destination, { from }): place an outbound call.client.on("incoming", handler): receive inbound calls.call.hangup(): end the call.call.on("answered" | "ended" | "ringing"): call state events.Related concept:
import express from "express";
import { requireAuth } from "./auth"; // your own auth middleware
const app = express();
// Mint a SIP token for an authenticated user's browser session
app.post("/api/sip-token", requireAuth, async (req, res) => {
const response = await fetch("https://api.sautikit.com/v1/sip/token", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`,
},
});
if (!response.ok) {
return res.status(502).json({ error: "Failed to mint SIP token" });
}
const { token, expires_at } = await response.json();
// Return token to the authenticated browser session only
res.json({ token, expires_at });
});
app.listen(3000);import { SautikitClient } from "@sautikit/browser";
const client = new SautikitClient({
// Your server endpoint; must require user authentication
tokenUrl: "/api/sip-token",
});
// Connect to the SIP gateway on page load
await client.connect();
console.log("WebRTC ready");
// ── Outbound call ────────────────────────────────────────────────
async function startCall(destinationNumber) {
const call = await client.call(destinationNumber, {
from: "+254700000001", // one of your claimed Sautikit numbers
});
call.on("ringing", () => updateUI("Ringing..."));
call.on("answered", () => updateUI("Connected"));
call.on("ended", () => updateUI("Call ended"));
return call;
}
// ── Inbound call ─────────────────────────────────────────────────
client.on("incoming", (inboundCall) => {
showIncomingCallBanner(inboundCall.from);
document.getElementById("accept-btn").onclick = () => inboundCall.accept();
document.getElementById("reject-btn").onclick = () => inboundCall.reject();
inboundCall.on("ended", () => hideIncomingCallBanner());
});
// ── Hang up ───────────────────────────────────────────────────────
function endCall(call) {
call.hangup();
}<!doctype html>
<html lang="en">
<head><title>Click to Call</title></head>
<body>
<button id="call-btn">Call Support</button>
<p id="status">Ready</p>
<script type="module">
import { SautikitClient } from "https://cdn.jsdelivr.net/npm/@sautikit/browser/dist/index.esm.js";
const client = new SautikitClient({ tokenUrl: "/api/sip-token" });
await client.connect();
let activeCall = null;
document.getElementById("call-btn").addEventListener("click", async () => {
if (activeCall) {
await activeCall.hangup();
activeCall = null;
document.getElementById("call-btn").textContent = "Call Support";
document.getElementById("status").textContent = "Call ended";
return;
}
activeCall = await client.call("+254700000001", { from: "+254700000001" });
document.getElementById("call-btn").textContent = "Hang Up";
activeCall.on("answered", () => {
document.getElementById("status").textContent = "Connected";
});
activeCall.on("ended", () => {
document.getElementById("status").textContent = "Call ended";
document.getElementById("call-btn").textContent = "Call Support";
activeCall = null;
});
});
</script>
</body>
</html>Browser-originated calls are billed identically to API-originated calls at the standard outbound per-minute rate for the destination. There is no additional charge for the WebRTC or SIP gateway layer; the gateway cost is included in the per-minute rate.
Token minting (POST /v1/sip/token) does not incur a fee per request. A connected browser that places no calls costs nothing beyond the workspace subscription.
Key cost drivers:
timeout on your Dial actions short if the user experience allows.For call center agents handling many short calls, model cost per agent hour rather than per call. An agent handling 10 calls of 4 minutes each in an hour at the standard rate gives a predictable hourly cost that you can compare against traditional telephony line rental.
POST /v1/sip/token request and response schema.from identifier for outbound calls.