> For complete lifecycle-event payload schemas and examples, use the canonical overview: Session /api/sessions; Room lifecycle, participants, and room playback /api/rooms; Session playback /api/playback; Recording /api/recording; DTMF /api/keypad-input; Messages /api/messages; WebSocket Session /api/web-socket. Use /api/event-delivery for transport behavior. Endpoint pages name relevant events, but these overviews are the canonical references.

# Browser calling

> Browser calling guide covering client tokens, SIP-over-WebSocket registration, token rotation, dialing an identity, and Room-based call control.

Browser calling lets an agent or user talk from a web page. Your server creates
a **client token** for an `identity`, such as an agent's ID. The browser uses the
token to connect over SIP over WebSocket ([RFC 7118](https://www.rfc-editor.org/rfc/rfc7118)).
Each call the browser makes or receives is an ordinary [Session](/api/sessions)
with `type: "webrtc"`, so Rooms, recording, playback, and events work as they do
for any other Session.

## Flow

1. Have the application's `webrtc_allowed_cidrs` setting configured with the
   networks your users connect from. Browsers can connect only from these
   networks, and
   [`POST /v1/client_tokens`](/api/browser-calling/create) returns `403` until
   the list is set.
2. From your server, create a token for the identity. Send the response to the
   signed-in user's browser.
3. The browser connects with any SIP-over-WebSocket library that supports
   standard WebRTC media.
4. The browser places calls, or your server dials the identity with
   [`POST /v1/sessions:dial`](/api/sessions/dial).
5. Your server handles the Session events and controls the call.

Create tokens only from your server. The `password` is returned only by the
create response, so never log it or store it longer than the client needs it.

## Create a token

**`POST /v1/client_tokens`**

```json title="POST /v1/client_tokens"
{
  "identity": "agent-1",
  "ttl_seconds": 3600
}
```

`identity` is 1 to 64 lowercase letters, digits, dots, underscores, or hyphens.
`ttl_seconds` defaults to 3600 and ranges from 60 to 31536000. The response
contains everything the client needs:

```json
{
  "client_token_id": "ct_0123456789abcdef0123456789abcdef",
  "identity": "agent-1",
  "protocol": "sip_wss",
  "ws_url": "wss://…",
  "sip_uri": "sip:agent-1@…",
  "username": "ct_0123456789abcdef0123456789abcdef",
  "password": "…",
  "ice_servers": [],
  "created_at": "2026-11-05T09:00:00Z",
  "expires_at": "2026-11-05T10:00:00Z"
}
```

[Get a client token](/api/browser-calling/get) returns `registered` (whether a
client is connected with it) and `last_used_at`, and never the password.
[List client tokens](/api/browser-calling/list) accepts an optional `identity`
filter. [Revoke a client token](/api/browser-calling/revoke) stops it from
authenticating new connections.

## Connect with JsSIP

Any compliant library works. This example uses
[JsSIP](https://jssip.net):

```js
const ua = new JsSIP.UA({
  sockets: [new JsSIP.WebSocketInterface(token.ws_url)],
  uri: token.sip_uri,
  authorization_user: token.username,
  password: token.password,
  register: true,
});

// Inbound calls (dialed from your server) arrive here. Answer with the
// microphone stream.
ua.on("newRTCSession", ({ originator, session }) => {
  if (originator === "remote") {
    session.answer({ mediaStream: micStream, pcConfig: { iceServers: token.ice_servers } });
  }
});

ua.on("registered", () => {
  // Any destination in your domain places the call; your server learns about
  // it from the Session events.
  const session = ua.call(`sip:call@${token.sip_uri.split("@")[1]}`, {
    mediaStream: micStream,
    pcConfig: { iceServers: token.ice_servers },
  });

  // Send a keypad digit as an RFC 4733 telephone event.
  session.sendDTMF("5", { transportType: "RFC2833" });
});

ua.start();
```

Media is standard WebRTC. Audio uses Opus with in-band forward error
correction. Send keypad digits as RFC 4733 telephone events, as above; SIP INFO
DTMF is not supported.

The platform answers the browser's own outbound call automatically. TURN is not
provided, so `ice_servers` is empty and the client must be able to reach the
platform over UDP.

## Rules

* **One device per identity.** While one token of an identity is registered or in
  a call, the identity's other token cannot register or place calls. After the
  first device disconnects, it can take a few seconds before the other token is
  accepted.
* **One Session per identity.** A call that arrives while the identity already
  has a Session is rejected as busy, whether the browser placed it or you
  dialed it.
* **Two live tokens per identity.** Creating a third returns `409`.
* **Session type.** Every browser call is a Session with `type: "webrtc"`.
  [`session.created`](/api/sessions) carries the `identity`.
* **Unsupported from the browser.** In-call SIP hold, re-INVITE, transfer, and
  messaging are not supported. Use the API and Rooms for call control.

## Rotate a token

A token only gates new connections, so you can replace one without dropping the
user. Use the second token slot:

1. Create a new token for the same identity while the old one is still live.
2. Switch the client to the new token's credentials.
3. Delete the old token.

While the old token is registered or in a call, the new token cannot register
or place calls until that device disconnects. Reconnect the client with the new token after it
has stopped using the old one.

## Revocation, replacement, and expiry

A call already in progress on a token that was revoked, replaced, or has expired
continues until it hangs up. To end it immediately, call
[`DELETE /v1/sessions/{uuid}`](/api/sessions/delete) after you revoke the token.

## Call an identity

Dial the identity from your server:

**`POST /v1/sessions:dial`**

```json title="POST /v1/sessions:dial"
{
  "to": { "identity": "agent-1" },
  "ring_timeout_sec": 30,
  "max_duration_sec": 3600
}
```

The response returns the new Session. The call is answered when the browser
answers. It returns `409` if no device is connected as that identity or the
identity already has a Session. The `from`, `caller_id_session_uuid`, and
`trunk_id` fields are not allowed when dialing an identity.

## Control calls with Rooms

Control the call from your server instead of the browser. A common contact
center pattern:

* Put the customer's Session in a [Room](/api/rooms) and leave it there.
* Move the agent between customers by removing the agent's Session from one Room
  and [adding it to another](/api/room-participants/add). The customer stays in
  their Room, so the customer's audio, recording, and playback are not
  interrupted.
* To put the customer on hold, play audio to the Room with
  [Room playback](/api/room-playback), or place the customer alone in a Room.

To whisper to an agent or keep a participant from being heard, set the
participant's [`audience`](/api/room-participants/audience) so only the intended
listeners hear them. See the Room participant APIs for the full set of options.