---
title: Browser / WebRTC SDK
description: >-
  Browser SDK for in-page calling. Mint a SIP token server-side, then place and
  receive calls with @sautikit/webrtc. Preview; pin your version.
summary: >-
  Official browser SDK for WebRTC calling. Mint a SIP token on your server, hand
  it to the browser, and place or receive calls from a web page. In preview; pin
  your version.
date: 2026-07-20T00:00:00.000Z
type: sdk
---


## Summary

`@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](/developers/sdks/node).

## Install

```bash
npm i @sautikit/webrtc
```

The package is ESM-only and ships its own types. It has **no runtime dependencies**.

> 
> Preview release (`0.1.2`). The API may change before `1.0` — pin an exact version rather than a caret range.
> 

## Mint a token on your server

The browser must never hold your API key. Mint a SIP token server-side and return it to the page:

<!-- diagram omitted -->

```ts
// 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.

## Place a call

```ts
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.

## Receive a call

```ts
client.on("incoming", async ({ from }) => {
  if (confirm(`Incoming call from ${from}. Answer?`)) {
    await client.accept();
  } else {
    client.reject("busy");
  }
});
```

## Client reference

### Configuration

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

### Methods

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

### Events

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`. |

## Token lifetime

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.

## See also

- [WebRTC concepts](/developers/concepts/webrtc) — how browser calling fits the platform
- [Mint a SIP token](/developers/api/mintSipToken) — the token endpoint reference
- [Node.js SDK](/developers/sdks/node) — server-side calling
