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

  1. An agent — create one in the dashboard (Agents → New agent) and copy its agent ID.
  2. A way to authorize the call (pick one):
    • Backend-minted session — recommended. Your server calls createWeb / init_web_call with 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.
  3. A secure context — browsers expose the mic only on https:// (or localhost), and start() 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.sendContext('Cart: 1× blue kurta (M)', { key: 'cart' }); // a note, never a turn — see below
call.sendActivity();                                  // the user is still here, just busy
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, sendContext, sendActivity }.

Options reference

OptionTypeDefaultWhat it does
session{ sessionId, websocketUrl }—Pre-minted session from your backend. Skips init entirely.
publicSlugstring—Published-agent slug (no auth).
tokenstring—Ephemeral client token (Authorization: Bearer).
baseUrlstringsame-originAPI origin for init.
variablesRecord<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.echoCancellationbooleantrueBrowser AEC — keep on for two-way audio.
audio.noiseSuppressionbooleantrueBrowser noise suppression.
audio.autoGainControlbooleantrueOn 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.deviceIdstringsystem defaultPick a specific microphone.
reconnect{ maxAttempts?, baseDelayMs?, maxDelayMs?, jitter? }6 / 500 / 10000 / 0.3Exponential backoff after network drops.
autoActivitybooleanfalseTell the agent the user is still here while they type or click, so it doesn't ask "are you still there?" mid-form. Paced by the agent's own check-in delay: one ping per window, none for an idle user.

TelenowCall additionally accepts callbacks (onState, onTranscript, onLevel, onError); the React hook returns the same data as state instead.

Notes and activity from your page

Your page knows things the agent can't hear — what the user is looking at, what is in their cart, how far they got in a form. Send them as notes:

await call.sendContext('Viewing: blue kurta, ₹1,299, sizes M and L', { key: 'page' });
  • A note is never a turn: the agent doesn't answer it. It uses the note from its next reply, so when the user asks "is this available in L?" it knows what "this" is.
  • key replaces an earlier note from your page with the same key — send page or cart state under one key. It never replaces a note your backend sent.
  • respond: 'when_idle' asks the agent to tell the user: it speaks up once the line is free, never over them (call.sendContext('Payment confirmed', { respond: 'when_idle' })). In chat mode it works like 'none': the agent's next reply tells the user.
  • The agent must accept notes from the caller's app: "Notes from the caller's app" in the builder (Call handling), or sessionConfig.liveContext.acceptClientNotes: true through the agents API. It is off by default. The agent is always told these notes come from the user's own app and prove nothing.
  • sendContext resolves with 'next_turn', 'speaking_now' (when_idle into a free line) or 'held' (the agent is on hold, or a transfer is under way), and rejects with a TelenowContextError whose reason is, from the server, disabled, too_large (with maxChars), rate_limited, empty, engine_unsupported, no_agent, not_live or invalid_respond; from the SDK, not_connected, connection_lost, call_ended or unsupported_transport.
  • maxChars counts Unicode code points: trim with Array.from(text).slice(0, maxChars).join(''). (The SDK repairs half an emoji left by a plain slice before sending, so the reply still arrives — but the emoji is lost.)
  • Notes and activity travel over the WebSocket transport; on a WebRTC call they reject with unsupported_transport for now.

When the user goes quiet because they are busy on the page, call.sendActivity() tells the agent they are still there; it resolves with how long until the agent would speak up unprompted, or null when nothing is armed right now (a hold, a wait being acknowledged). Or set autoActivity: true and the SDK does it while the user types or clicks: one ping per check-in window, timed to land before the window runs out, and none for a user who has stopped. After a null, it waits until the conversation moves on before pinging again. Everything about notes and activity: Live context notes.

Notes from your backend use the server SDK instead (calls.sendContext) — they work on phone calls too and need no agent setting. See Sessions & calls.

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

SymptomCause / fix
getUserMedia is unavailableNot https:// (or localhost), or mic permission blocked.
start() rejects 401/403Bad/expired token, or the agent's API access is off (Publish tab).
400 naming a variableA required context variable is missing.
Connects, no audiostart() wasn't called from a user gesture (autoplay policy).
Agent hears itselfechoCancellation was disabled — leave it on.
Stuck reconnecting → endedNetwork 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.