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:

{
"status": true,
"data": {
"access_token": "wct_<uuid>",
"expires_in": 30,
"sample_rate": 24000
}
}

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.

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();

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()
captureDeviceIdstring-Specific microphone device
playbackDeviceIdstring-Specific 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