Web & React
Web & React SDK
Run agent voice calls in the browser with your own UI. @telenow/client is
framework-agnostic; @telenow/react adds a hook on top of it. Both handle the
microphone, echo/noise suppression, jitter-buffered playback, barge-in,
auto-reconnect, transcripts, and latency telemetry — you render buttons and
text.
npm i @telenow/client # vanilla / any framework
npm i @telenow/react # + React hook (peer-depends on @telenow/client)
Before you start
- An agent — create one in the dashboard (Agents → New agent) and copy its agent ID.
- A way to authorize the call (pick one):
- Backend-minted session — recommended. Your server calls
createWeb/init_web_callwith an org API key and returns{ sessionId, websocketUrl }to the browser. The key never leaves your server. - Public slug — publish the agent (Publish tab),
pass
publicSlug. No backend needed; rate-limited, optional access code. - Client token — short-lived agent-scoped token passed as
token.
- Backend-minted session — recommended. Your server calls
- A secure context — browsers expose the mic only on
https://(orlocalhost), andstart()must be triggered by a user gesture (click) or autoplay policies will block audio.
Quickstart — vanilla JS (TelenowCall)
import { TelenowCall } from '@telenow/client';
// Recommended: fetch a session minted by YOUR backend (no credential here).
const session = await fetch('/voice/session', { method: 'POST' }).then((r) => r.json());
const call = new TelenowCall({
session, // or publicSlug: 'my-agent' / token: '…'
baseUrl: 'https://api.telenow.ai', // used only when the SDK does its own init
onState: (s) => render(s), // idle|connecting|live|reconnecting|ended|error
onTranscript: (t) => addLine(t.role, t.text, t.isFinal),
onLevel: (dbfs) => meter(dbfs), // mic level per 20 ms frame — drive a VU meter
onError: (msg) => showError(msg),
});
startButton.addEventListener('click', () => call.start()); // mic permission → live
During the call:
call.setMuted(true); // mic on/off
call.sendText('Use my work email'); // typed turn → spoken reply
call.sendText('What are your hours?', { chat: true }); // typed turn → text-only reply
call.stop(); // hang up locally
call.sessionId; // hand to your backend for transfer/end (server SDK)
When the agent ends the call (or your backend calls calls.end()), the
SDK receives the end event, tears down audio, and fires onState('ended') —
you never touch protocol events.
Quickstart — React (useVoiceCall)
import { useVoiceCall } from '@telenow/react';
function CallPanel({ session }) {
const { state, transcript, muted, error, start, stop, mute, sendText } = useVoiceCall({
session, // or publicSlug / token — same three options
});
return (
<div>
<button onClick={state === 'live' ? stop : start} disabled={state === 'connecting'}>
{state === 'live' ? 'End call' : state === 'connecting' ? 'Connecting…' : 'Start call'}
</button>
{state === 'live' && <button onClick={() => mute(!muted)}>{muted ? 'Unmute' : 'Mute'}</button>}
{state === 'reconnecting' && <span>Reconnecting…</span>}
{error && <p role="alert">{error}</p>}
<ul>
{transcript.map((t, i) => (
<li key={i} style={{ opacity: t.isFinal ? 1 : 0.6 }}>
<b>{t.role}:</b> {t.text}
</li>
))}
</ul>
</div>
);
}
The hook tears the call down on unmount. It returns
{ state, transcript, muted, error, start, stop, mute, sendText }.
Options reference
| Option | Type | Default | What it does |
|---|---|---|---|
session | { sessionId, websocketUrl } | — | Pre-minted session from your backend. Skips init entirely. |
publicSlug | string | — | Published-agent slug (no auth). |
token | string | — | Ephemeral client token (Authorization: Bearer). |
baseUrl | string | same-origin | API origin for init. |
variables | Record<string,string> | — | Context variables. Required ones must be present or init fails with 400. With a minted session, pass them server-side instead — tamper-proof. |
audio.encoding | 'mulaw' | 'pcm16' | 'mulaw' | Uplink wire format — keep the default, it's what the platform decodes. |
audio.echoCancellation | boolean | true | Browser AEC — keep on for two-way audio. |
audio.noiseSuppression | boolean | true | Browser noise suppression. |
audio.autoGainControl | boolean | true | On by default (SDK ≥0.1.5) — without AGC, typical laptop mics sit below the barge-in detection gate, so callers can't interrupt the agent mid-speech. Set false only for pre-levelled/broadcast inputs. |
audio.deviceId | string | system default | Pick a specific microphone. |
reconnect | { maxAttempts?, baseDelayMs?, maxDelayMs?, jitter? } | 6 / 500 / 10000 / 0.3 | Exponential backoff after network drops. |
TelenowCall additionally accepts callbacks (onState, onTranscript,
onLevel, onError); the React hook returns the same data as state instead.
Use as a softphone (manual telephony)
The same session option also powers a human softphone for click-to-call
in a CRM. When your backend mints a
manual call
(tn.calls.createManual({ from, to }) — no AI in the loop), it returns the same
{ sessionId, websocketUrl }. Pass it to TelenowCall({ session }) (or
useVoiceCall) and the browser becomes the rep's microphone + speaker,
bridged to the customer over the carrier. Controls are identical
(setMuted, stop, sessionId for transfer/end); there are no agent
transcripts, since a person is talking rather than the AI.
What happens under the hood
- Uplink —
getUserMedia→ AudioWorklet (ScriptProcessor fallback) → resample → G.711 μ-law 8 kHz → 20 ms base64 frames over the WebSocket. - Downlink — agent audio (up to 24 kHz PCM) scheduled through an adaptive jitter buffer (depth self-tunes 60–400 ms) with fade-in concealment — no clicks on bad Wi-Fi.
- Barge-in — when the caller talks over the agent, queued audio is flushed instantly.
- Reconnect — drops re-dial with backoff and re-attach to the same
session; UI sees
reconnecting→live. - Latency — the SDK answers server pings, so analytics show the real web⇄server round-trip for your calls.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
getUserMedia is unavailable | Not https:// (or localhost), or mic permission blocked. |
start() rejects 401/403 | Bad/expired token, or the agent's API access is off (Publish tab). |
| 400 naming a variable | A required context variable is missing. |
| Connects, no audio | start() wasn't called from a user gesture (autoplay policy). |
| Agent hears itself | echoCancellation was disabled — leave it on. |
Stuck reconnecting → ended | Network down or session expired; start a new call. |
Going lower-level
All building blocks are exported (CaptureEngine, PlaybackEngine,
ReconnectingSocket, the jitter buffer, μ-law/PCM codecs) and the raw
WebSocket protocol is documented in
Programmatic web calls and the
Web-call API. Your agent's Publish tab shows these
exact snippets with your agent id pre-filled.