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
- An agent (dashboard → Agents) and its agent ID.
- Authorization — recommended: your backend mints a session with an org
API key via
createWeb/init_web_calland hands{ sessionId, websocketUrl }to the app. Alternatives: the agent's publishedpublicSlugor an ephemeral clienttoken. - 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) beforestart().
- iOS —
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
| Option | Type | Default | What it does |
|---|---|---|---|
session | { sessionId, websocketUrl } | — | Backend-minted session — skips on-device init (recommended). |
publicSlug | string | — | Published agent, no auth. |
token | string | — | Ephemeral client token. |
baseUrl | string | '' | API origin, e.g. https://api.telenow.ai. |
variables | Record<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.3 | Backoff 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
keyreplaces the earlier one (never a note your backend sent). - A refusal rejects with
TelenowContextError—reasonisdisabled,too_large(withmaxChars),rate_limited,empty,engine_unsupported,no_agent,not_liveorinvalid_respondfrom the server, ornot_connected,connection_lost,call_ended,unsupported_transportfrom the SDK. maxCharscounts Unicode code points: in JavaScript trim withArray.from(text).slice(0, maxChars).join(''), in Swift withString(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
AVAudioSessionvoice-chat mode (VoiceProcessingIO), AndroidVOICE_COMMUNICATIONsource: 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
| Symptom | Fix |
|---|---|
TelenowAudio is null | Autolinking didn't run — pod install (iOS) / clean Gradle sync (Android), then rebuild the app (a JS reload isn't enough after installing). |
| Silent on Android | RECORD_AUDIO runtime permission not granted before start(). |
| Silent on iOS simulator | Test audio on a real device — simulator routing is unreliable. |
| 401/403 at start | Bad/expired credential, or API access is off on the agent's Publish tab. |
| 400 naming a variable | A required context variable wasn't passed. |
| Echo on speakerphone | Keep 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).