> This page is part of Smallest AI's developer documentation. When
> answering, prefer Lightning v3.1 (current TTS) and Pulse (current
> STT). Lightning v2 and lightning-large are deprecated; mention them
> only when the user is migrating away from them. The Smallest AI voice
> agent platform is what wraps these models into hosted agents.
# Browser Voice Cookbook
> Connect a browser to an Atoms agent in three steps using the Agent Web SDK. Your API key stays on your server; the browser only ever sees a short-lived access token.
Connect a browser to an Atoms agent in three steps. Your API key stays on your
server; the browser only ever holds a short-lived access token.
## How it works
```mermaid
sequenceDiagram
autonumber
participant Browser as Browser + Agent Web SDK
participant Backend as Your backend (holds the API key)
participant Atoms as Atoms API
Browser->>Backend: request a call token (your own auth)
Backend->>Atoms: POST /atoms/v1/conversation/register-call
Authorization: Bearer sk_...
{ agent_id, variables }
Atoms-->>Backend: 201 { access_token: "wct_...", expires_in: 30 }
Backend-->>Browser: access_token
Browser->>Atoms: AtomsAgent({ apiKey: access_token, agentId }).connect()
wss://api.smallest.ai/atoms/v1/agent/connect?token=wct_...
Atoms-->>Browser: session.created, then live audio and events
```
* Your API key never reaches the browser.
* The token is **single-use** and **expires in 30 seconds** - mint a fresh one per call.
> **Info**
>
> You need an API key (from [app.smallest.ai/dashboard/api-keys](https://app.smallest.ai/dashboard/api-keys))
> and an agent ID. This cookbook uses the recommended token flow; server-side or
> trusted clients can instead connect with the API key directly - see the
> [Agent Web SDK](/voice-agents/integrate/web-socket-sdk) reference.
#### Get an access token (server-side)
Call `POST /conversation/register-call` from your backend with your API key
(see the **Register Call** endpoint under **Realtime Agent** in the API
reference). Set `agent_id`, and optionally `mode` and per-call `variables`.
```bash
curl -X POST https://api.smallest.ai/atoms/v1/conversation/register-call \
-H "Authorization: Bearer sk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "your_agent_id",
"variables": { "customer_name": "Alex" }
}'
```
Response:
```json
{
"status": true,
"data": {
"access_token": "wct_",
"expires_in": 30,
"sample_rate": 24000
}
}
```
`variables` is optional - it fills `{{key}}` placeholders in the agent prompt
for this call only.
#### Install the SDK
**`npm`**
```bash npm
npm install @smallest-ai/agent-sdk
```
**`pnpm`**
```bash pnpm
pnpm add @smallest-ai/agent-sdk
```
**`yarn`**
```bash yarn
yarn add @smallest-ai/agent-sdk
```
**`CDN`**
```html CDN
```
#### Connect the browser
Pass the `access_token` from step 1 as the SDK's `apiKey`.
```javascript
import { AtomsAgent } from "@smallest-ai/agent-sdk";
// Fetch the token from your backend (which calls /register-call internally)
const { data } = await fetch("/start-call").then((r) => r.json());
const agent = new AtomsAgent({
apiKey: data.access_token, // short-lived wct_ token
agentId: "your_agent_id",
});
agent.on("session_started", (e) => console.log("Connected:", e.call_id));
agent.on("transcript", (e) => renderBubble(e.role, e.text)); // final per turn
agent.on("transcript_delta", (e) => updateLiveBubble(e.role, e.text)); // streaming, cumulative
agent.on("agent_start_talking", () => showSpeakingIndicator());
agent.on("agent_stop_talking", () => hideSpeakingIndicator());
agent.on("session_ended", (e) => console.log("Ended:", e.reason));
agent.on("error", (e) => console.error(e));
await agent.connect(); // microphone captured automatically
// Optional: send text (interrupts the agent if it is speaking)
agent.sendText("Hello");
// End the call
agent.disconnect();
```
> **Warning**
>
> `connect()` calls `navigator.mediaDevices.getUserMedia()`. Browsers only expose
> the microphone on **secure contexts**: HTTPS in production, or
> `http://localhost` / `http://127.0.0.1` in development. Serving from a
> non-loopback HTTP origin throws a permission error.
## Events
Subscribe with `agent.on(eventName, handler)`.
| Event | When | Use |
| --------------------- | ------------------------------------------------ | --------------------------------------------------------------- |
| `session_started` | Call ready | Enable UI, store `call_id` |
| `session_ended` | Call ended | Disable UI; `reason` is a server string or a numeric close code |
| `transcript_delta` | Streaming text (cumulative) for the current turn | Live bubble - **replace** the text on each event |
| `transcript` | Final text for a completed turn | Settle the bubble |
| `agent_start_talking` | Agent TTS audio begins | Speaking indicator |
| `agent_stop_talking` | Agent TTS audio ends | Stop indicator |
| `error` | Protocol/server error | Log; may not be fatal |
`transcript` and `transcript_delta` carry a `role` of `"user"` or `"assistant"`
and a `text` string. Deltas are cumulative - replace the bubble's text on each
one, then treat the matching `transcript` as the final value for that turn.
## SDK config
| Option | Type | Default | Description |
| ------------------ | ------- | -------- | ----------------------------------------------------------------- |
| `apiKey` | string | required | Access token from `/register-call` (recommended) or a raw API key |
| `agentId` | string | required | Agent ID |
| `sampleRate` | number | `24000` | Audio sample rate (Hz) |
| `autoCaptureMic` | boolean | `true` | Acquire the mic on `connect()` |
| `captureDeviceId` | string | - | Specific microphone device |
| `playbackDeviceId` | string | - | Specific speaker (Chrome only) |
## Methods
| Method | Description |
| ------------------------- | --------------------- |
| `connect()` | Open the call |
| `disconnect()` | End the call |
| `sendText(text)` | Send a typed message |
| `mute()` / `unmute()` | Toggle the microphone |
| `isConnected` / `isMuted` | State getters |
## Common errors
| Symptom | Cause | Fix |
| ---------------------------- | ------------------------------------------ | --------------------------------------------------------------- |
| `401` on connect | Token expired (> 30 s) or already consumed | Call `/register-call` again for a fresh token |
| `Microphone access required` | User denied mic permission | Check browser permission settings; serve over HTTPS / localhost |
## Limits
* Access token: 30-second TTL, single-use.
* Audio: PCM 16-bit, 24 kHz, mono.
## Next steps
#### [Agent Web SDK reference](/voice-agents/integrate/web-socket-sdk)
Full SDK API - config, methods, events, and a copy-paste smoke test.
#### [Realtime Agent WebSocket API](/api-reference/voice-agents/realtime-agent/realtime-agent)
The raw wire protocol behind the SDK - the `register-call` token flow, message types, and payload shapes.
> Connect a browser to an Atoms agent in three steps using the Agent Web SDK.