React Native SDK

Real-time AI voice agent calls inside your iOS & Android app with @telenow/react-native. The control plane, codecs, and jitter buffer run in JS (shared with @telenow/client); a small native module does only mic capture + PCM playback in the platform's voice-communication mode, so hardware echo cancellation and noise suppression are on automatically — speakerphone works without the agent hearing itself.

npm i @telenow/react-native @telenow/client
cd ios && pod install        # autolinks the native TelenowAudio module

Before you start

  1. An agent (dashboard → Agents) and its agent ID.
  2. Authorization — recommended: your backend mints a session with an org API key via createWeb / init_web_call and hands { sessionId, websocketUrl } to the app. Alternatives: the agent's published publicSlug or an ephemeral client token.
  3. Microphone permission:
    • iOS — Info.plist:
      <key>NSMicrophoneUsageDescription</key>
      <string>Voice calls with our assistant</string>
      
    • Android — <uses-permission android:name="android.permission.RECORD_AUDIO" /> in the manifest and a runtime request (PermissionsAndroid.request) before start().

Quickstart

import { TelenowCall } from '@telenow/react-native';

const call = new TelenowCall({
  session,                    // { sessionId, websocketUrl } from YOUR backend (recommended)
  // publicSlug: 'my-agent',  // or: published agent, no auth
  baseUrl: 'https://api.telenow.ai',
});

call.onState = (s) => setCallState(s);   // connecting | live | reconnecting | ended | error
call.onTranscript = (role, text) => append({ role, text });

await call.start();     // starts native audio + connects → live
call.setMuted(true);
call.stop();

Options reference

OptionTypeDefaultWhat it does
session{ sessionId, websocketUrl }—Backend-minted session — skips on-device init (recommended).
publicSlugstring—Published agent, no auth.
tokenstring—Ephemeral client token.
baseUrlstring''API origin, e.g. https://api.telenow.ai.
variablesRecord<string,string>—Context variables — or bake them into the minted session (tamper-proof).
uplinkEncoding'mulaw' | 'pcm16''mulaw'Wire format — keep the default.
reconnect{ maxAttempts?, baseDelayMs?, maxDelayMs?, jitter? }6 / 500 / 10000 / 0.3Backoff after network drops.

Callbacks: onState(state), onTranscript(role, text). Methods: start(): Promise<void>, stop(), setMuted(boolean), sendContext(text, { key?, respond? }), sendActivity().

Tell the agent what's happening in the app

While the call is live your app can tell the agent things it can't hear — the screen the user is on, what is in their basket — without them counting as something the caller said (live context notes):

const delivery = await call.sendContext('Basket: 2 items, ₹1,240', { key: 'basket' });
// 'next_turn' (default) · 'held' (the agent is on hold, or a transfer is under way) · 'speaking_now'
await call.sendContext('Payment confirmed in the app', { respond: 'when_idle' }); // agent speaks up once the line is free
const nextCheckinInMs = await call.sendActivity(); // "still here, just busy" — pushes back "are you still there?"
  • The agent must have Notes from the caller's app turned on (agent builder → Call handling); it is off by default, and the agent is always told these notes are unverified — they never prove a payment or an identity.
  • A later note from the app with the same key replaces the earlier one (never a note your backend sent).
  • A refusal rejects with TelenowContextError — reason is disabled, too_large (with maxChars), rate_limited, empty, engine_unsupported, no_agent, not_live or invalid_respond from the server, or not_connected, connection_lost, call_ended, unsupported_transport from the SDK.
  • maxChars counts Unicode code points: in JavaScript trim with Array.from(text).slice(0, maxChars).join(''), in Swift with String(text.unicodeScalars.prefix(maxChars)). (The React Native SDK repairs half an emoji before sending, so the reply still arrives — but the emoji is lost.)
  • WebSocket calls only: on a WebRTC agent both reject with unsupported_transport.

The native Swift package (TelenowSDK) has the same two calls: try await call.sendContext("…", key: "basket", respond: .whenIdle) and try await call.sendActivity(), throwing TelenowContextError. Safe from any thread or task: each reply pairs with its own call, however many run at once. Everything about notes: Live context notes.

What the SDK handles

  • Echo & noise — iOS AVAudioSession voice-chat mode (VoiceProcessingIO), Android VOICE_COMMUNICATION source: hardware AEC/NS/AGC. Agent playback is routed through the voice-processing unit so the echo canceller removes the agent's own voice from the mic.
  • Barge-in — interrupting the agent flushes queued audio instantly.
  • Reconnect — drops retry with backoff while audio keeps running.
  • Latency pings — answered automatically for your analytics.

Troubleshooting

SymptomFix
TelenowAudio is nullAutolinking didn't run — pod install (iOS) / clean Gradle sync (Android), then rebuild the app (a JS reload isn't enough after installing).
Silent on AndroidRECORD_AUDIO runtime permission not granted before start().
Silent on iOS simulatorTest audio on a real device — simulator routing is unreliable.
401/403 at startBad/expired credential, or API access is off on the agent's Publish tab.
400 naming a variableA required context variable wasn't passed.
Echo on speakerphoneKeep playback inside the SDK — it runs through the echo canceller.

Other mobile stacks

Native Swift, Kotlin/Android, and Flutter SDKs are coming soon. Until then: embed the hosted widget or public page in a web view, or drive everything over the REST API (your agent's Publish tab has copy-paste cURL, Node, Python, and Rust samples).