Browser Voice Cookbook

View as Markdown

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

┌─────────────┐ 1. POST /register-call ┌──────────────────┐
│ Your │ Authorization: Bearer sk_… │ Atoms API │
│ Backend │ { agent_id: "...", │ │
│ │ variables: {...} } ──────▶│ Returns: │
│ │ │ access_token │
│ │ ◀────────────────────────────────── │ (wct_…, 30s) │
└─────────────┘ └──────────────────┘
│ 2. Return access_token to browser
┌─────────────┐ 3. AtomsAgent({ apiKey: access_token, agentId }).connect()
│ Browser │ ──────────────────────────────────▶ wss://api.smallest.ai/atoms/v1
│ + SDK │
└─────────────┘
  • Your API key never reaches the browser.
  • The token is single-use and expires in 30 seconds — mint a fresh one per call.

You need an API key (from 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 reference.

1

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.

$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:

1{
2 "status": true,
3 "data": {
4 "access_token": "wct_<uuid>",
5 "expires_in": 30,
6 "sample_rate": 24000
7 }
8}

variables is optional — it fills {{key}} placeholders in the agent prompt for this call only.

2

Install the SDK

$npm install @smallest-ai/agent-sdk
3

Connect the browser

Pass the access_token from step 1 as the SDK’s apiKey.

1import { AtomsAgent } from "@smallest-ai/agent-sdk";
2
3// Fetch the token from your backend (which calls /register-call internally)
4const { data } = await fetch("/start-call").then((r) => r.json());
5
6const agent = new AtomsAgent({
7 apiKey: data.access_token, // short-lived wct_ token
8 agentId: "your_agent_id",
9});
10
11agent.on("session_started", (e) => console.log("Connected:", e.call_id));
12agent.on("transcript", (e) => renderBubble(e.role, e.text)); // final per turn
13agent.on("transcript_delta", (e) => updateLiveBubble(e.role, e.text)); // streaming, cumulative
14agent.on("agent_start_talking", () => showSpeakingIndicator());
15agent.on("agent_stop_talking", () => hideSpeakingIndicator());
16agent.on("session_ended", (e) => console.log("Ended:", e.reason));
17agent.on("error", (e) => console.error(e));
18
19await agent.connect(); // microphone captured automatically
20
21// Optional: send text (interrupts the agent if it is speaking)
22agent.sendText("Hello");
23
24// End the call
25agent.disconnect();

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

EventWhenUse
session_startedCall readyEnable UI, store call_id
session_endedCall endedDisable UI; reason is a server string or a numeric close code
transcript_deltaStreaming text (cumulative) for the current turnLive bubble — replace the text on each event
transcriptFinal text for a completed turnSettle the bubble
agent_start_talkingAgent TTS audio beginsSpeaking indicator
agent_stop_talkingAgent TTS audio endsStop indicator
errorProtocol/server errorLog; 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

OptionTypeDefaultDescription
apiKeystringrequiredAccess token from /register-call (recommended) or a raw API key
agentIdstringrequiredAgent ID
sampleRatenumber24000Audio sample rate (Hz)
autoCaptureMicbooleantrueAcquire the mic on connect()
captureDeviceIdstringSpecific microphone device
playbackDeviceIdstringSpecific speaker (Chrome only)

Methods

MethodDescription
connect()Open the call
disconnect()End the call
sendText(text)Send a typed message
mute() / unmute()Toggle the microphone
isConnected / isMutedState getters

Common errors

SymptomCauseFix
401 on connectToken expired (> 30 s) or already consumedCall /register-call again for a fresh token
Microphone access requiredUser denied mic permissionCheck browser permission settings; serve over HTTPS / localhost

Limits

  • Access token: 30-second TTL, single-use.
  • Audio: PCM 16-bit, 24 kHz, mono.

Next steps