Skip to main content
@bolna/web-call · v3.0.0 · Source: github.com/bolna-ai/web-call
In beta and available for any account on request. Reach out on Slack or email support@bolna.dev to have it enabled.

Overview

The Web Call SDK connects a browser tab directly to a Bolna voice agent for a live, two-way conversation. You render your own UI, and the SDK handles everything else: asking for mic access, connecting the call, and cleaning up when it ends. You get a small state machine, six events, and a handful of methods. No telephony or real-time-audio background required.

Install

CDN / plain <script>. No bundler needed. This exposes window.BolnaWebCall:

Set up your backend

The browser side of this SDK never sees your Bolna API key (the bn-… key). Anyone reading your page source could lift a key that’s exposed client-side and place calls on your account, so the SDK is built so that key never has a reason to be there. Instead, the browser requests a short-lived, single-use call session from your own backend: a set of connection credentials that expire in roughly 120 seconds and are consumed the moment the first call uses them. Your backend route is a thin proxy. It calls Bolna’s session-mint endpoint with your key server-side, then returns the JSON unchanged:
Node/Express
In sessionUrl mode, the SDK POSTs { "user_data": {...} } to this route automatically whenever you set userData. Your route must forward it to Bolna’s mint endpoint, like above — if it doesn’t, userData is silently dropped and never reaches the agent.
Why there’s no apiKey option: the SDK deliberately can’t take a Bolna API key. A minted session expires in about 2 minutes, and its credential is consumed by the first call, so leaking one buys an attacker almost nothing.

Quickstart

Once your backend exposes a session-minting route, the browser side comes down to three steps: create the call, listen for the events you care about, and start it from a click handler.

Passing user data

Just like telephony’s /call endpoint, you can pass per-call userData that gets substituted into the agent’s prompt and welcome message — a {name} or {order_id} in your prompt becomes the caller’s actual name or order number for that call. Set it once in the constructor to apply it to every call from that instance, or pass it to start() to set or override it for a single call:
How userData reaches the agent depends on your session source:
  • sessionUrl — the SDK POSTs { "user_data": {...} } to your session endpoint automatically. Your backend route must forward that field to Bolna’s mint endpoint unchanged (see the note under Set up your backend) or the variables are silently dropped.
  • getSession / session — the SDK has no request of its own to inject userData into. You own the mint call, so you must include user_data in your own mint POST yourself:
There’s no error or warning if you skip this — the call just connects without the variables substituted.
userData is a JSON object, capped at 50 KB. A mint request over that limit is rejected with a 400. It’s stored on the call record and shown in call history, so don’t put secrets in it.

Starter templates

Two complete, copy-pasteable starting points. Pick whichever matches your stack. Next.js (App Router). Works out of the box on Vercel. Set BOLNA_API_KEY and BOLNA_AGENT_ID as environment variables and deploy.
app/api/bolna-session/route.js
app/components/BolnaCallButton.jsx
usage
Plain HTML + Node (no build step, no framework). Two files, no package.json needed. Run BOLNA_API_KEY=bn-… BOLNA_AGENT_ID=… node server.mjs, then open index.html.
server.mjs
index.html

API reference

new BolnaWebCall(options). Provide exactly one session source. The constructor throws if you pass zero or more than one. Optional, on top of one of the three above: Methods Call state A fresh start() is allowed again once state reaches ended. Each call gets its own freshly-minted session. Events. Subscribe with call.on(event, handler), and remove a handler with off or once for a one-time listener.
A handler that throws is caught internally and logged. It can’t take down call handling or other listeners.
cause holds the underlying error when one exists, for example a DOMException from a denied getUserMedia() call, or the raw error from a failed fetch(). It’s for logging and debugging: untyped (unknown), and not guaranteed to be present.
Error codes Session shape. If you use getSession instead of sessionUrl, resolve to this shape. It’s exactly what Bolna’s mint endpoint returns, so most integrations never construct it by hand: your backend proxies the mint response as-is.

Using it with React

The SDK has no framework binding. Wrap it in a hook that creates one instance per component and tears it down on unmount:
Call start() from the onClick of the button that triggered it, not from inside a useEffect, so the browser still counts it as a user gesture.

Behavior notes

  • One call at a time per instance. A second start() while a call is live rejects with already_active instead of double-dialing.
  • Sessions are fetched inside start(), per call, so the short credential TTL never has a chance to lapse. Nothing is written to localStorage.
  • Echo cancellation is on by default. Leave it on; headphones give the cleanest result.
  • Closing or navigating the tab hangs up automatically (via pagehide), so abandoned calls free their concurrency slot immediately instead of waiting for a server-side timeout.
  • All audio is encrypted automatically. Nothing to configure.
  • userData passed to start(opts) takes precedence over the constructor’s userData for that call only; the instance-level value is unaffected and still applies to the next start().

Migrating from v1

The v1 library (bolna-webcall-library.js, raw WebSocket + 16kHz PCM) still works, and existing jsDelivr pins keep resolving, so nothing breaks if you don’t migrate. New integrations should start on the current version:
  • Standard WebRTC (Opus + jitter buffer) instead of a raw audio WebSocket.
  • Ephemeral, single-use sessions instead of handling a long-lived key in the client.
  • A typed event emitter and explicit CallState machine instead of ad hoc callbacks.

FAQ

Check the error event first. start()’s promise rejects, so an unhandled rejection is a common reason it looks silent. Log e.code and e.message to see what actually failed.
The browser only allows audio playback that originates from a user gesture. Make sure the element handler that calls start() is the direct result of a click, not a promise callback, timer, or effect.
Connection setup can be slow on networks that force every candidate through TURN over TCP/TLS. If you’re testing on a restrictive network, set iceTransportPolicy: "relay" to confirm TURN itself works, then leave it unset ("all") in production so most calls use the faster path.
This is your account’s concurrent web-call limit, not a per-browser limit. Check error.scope: "customer" means your account’s cap, "global" means Bolna’s platform-wide cap.
It depends on your session source. In sessionUrl mode, check that your backend route actually forwards the user_data field from its request body to Bolna’s mint endpoint — the SDK sends it, but a proxy that ignores the body will drop it silently. In getSession/session mode, the SDK never sees your mint request at all, so you must add user_data to that POST yourself. Either way, there’s no error thrown — the call just connects without the variables substituted. See Passing user data.