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

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

OptionTypeDefaultWhat it does
session{ sessionId, websocketUrl }Pre-minted session from your backend. Skips init entirely.
publicSlugstringPublished-agent slug (no auth).
tokenstringEphemeral 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.

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

  • UplinkgetUserMedia → 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 reconnectinglive.
  • 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 reconnectingendedNetwork 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.