> 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. > Build reliable calling experiences with Vorbal APIs for calls, media, messaging, and webhooks. ## API Docs - Browser calling [Create a client token](https://docs.vorbal.dev/api/browser-calling/create.md) - Browser calling [List client tokens](https://docs.vorbal.dev/api/browser-calling/list.md) - Browser calling [Get a client token](https://docs.vorbal.dev/api/browser-calling/get.md) - Browser calling [Revoke a client token](https://docs.vorbal.dev/api/browser-calling/revoke.md) ## OpenAPI Specification The raw OpenAPI 3.1 specification for this API is available at: - [OpenAPI JSON](https://docs.vorbal.dev/openapi.json) - [OpenAPI YAML](https://docs.vorbal.dev/openapi.yaml)