ai.poly:voice places live, two-way WebRTC voice calls to a PolyAI agent. It is the companion artifact to the Android SDK’s chat product. It ships separately so chat-only apps stay lean (the call path pulls in the native libwebrtc audio engine), and it reuses the messaging Configuration plus the same CallState / PolyError.Voice / Environment vocabulary.
Calls are user-initiated: the user taps to call your agent. Inbound (push-triggered) calls are not supported.
Source on GitHub
polyai/android-sdk: the full polyvoice technical guide and runnable voice example apps.
Installation
The0.11.0 voice artifacts are compiled with Kotlin 2.4, so they require Kotlin plugin 2.4.10 or newer. In a fresh Android Studio project, set the Kotlin version first:
mavenCentral() is already present in new Android Studio projects. If your project removed it, restore it under dependencyResolutionManagement.repositories in settings.gradle.kts.
Initialize once, then call
Both credentials go on the sharedConfiguration in PolyMessaging.initialize(...). PolyVoice.call(context) then reads the initialized configuration, the same way PolyMessaging.chat() does for chat:
- Both credentials are required and distinct.
- The WebRTC token belongs in
Configuration.webrtcToken. Do not set it onVoiceOptions. PolyVoice.call(context)reads the configuration set byPolyMessaging.initialize(...). Callinitializefirst.- If
Configuration.webrtcTokenis missing,PolyVoice.call(...)throwsPolyError.InvalidConfiguration.
initialize(...) set, pass an explicit Configuration with both credentials:
CallState, PolyError.Voice.*, Configuration, and Environment are the same types from ai.poly:messaging.
Quick start
The following is a complete foreground-only calling app for the current Android Studio Empty Activity Compose template. Create a project with minimum SDK 24 or newer, update Kotlin and add the dependencies above, then replace the generated activity. No manifest changes or extra application class are needed for this first call.1
Paste one file: MainActivity.kt
Open the
MainActivity.kt Android Studio generated. Keep its first package … line, delete everything below that line, and paste the block below directly after it. Replace the two credential placeholders with your connector token and WebRTC token from Agent Studio › Connector Settings.2
Sync and run
Click Sync Project with Gradle Files, select a physical Android device, and run the app. Tap Start call and allow microphone access. The status moves through Connecting to Connected.
- Runtime permission: it requests
RECORD_AUDIObeforestart(), and shows a message if the user denies it. - State: it observes
call.statewithcollectAsStateWithLifecycle()and renders the connecting, connected, ended and failed states.start()returning means setup is under way, not that the call is already connected. - Controls: start, mute and end.
- Cleanup: the screen holds one
VoiceCalland callscall.close()inDisposableEffectwhen it leaves the composition.
Unresolved reference 'implementation', the dependency lines were added to the top-level build.gradle.kts; move them into app/build.gradle.kts. If it reports incompatible Kotlin metadata version 2.4.0, update the Kotlin version as shown under Installation, then sync again.
Credentials
A voice call needs two credentials, both on your agent in Agent Studio › Connector Settings (the same connector you use for chat):
The API key authenticates the connector. The WebRTC token authenticates the media backend.
Two more values have sensible defaults, so most apps don’t set them:
environmentdefaults toEnvironment.US. Set.UK/.EUW, or.cluster("…")for a named cluster, only if your agent lives in another region. The sameEnvironmentalso selects the correspondingwebrtc-bridgedeployment.hostIdentifier(sent asX-Host) defaults to your app’s package name (applicationId).
Custom / self-hosted bridge: the
webrtc-bridge host is derived from your Environment. If you run a dev or self-hosted bridge, set VoiceOptions.signalingHost (no scheme, e.g. "webrtc-bridge.example.com"). It is required with Environment.Custom, since the bridge host can’t be derived from a custom messaging endpoint.How a call connects
Calls are placed over PolyAI’swebrtc-bridge. The older webrtc-gateway path has been removed and no longer works, so there is nothing to choose between.
- The SDK calls
POST /api/v1/call, sending the WebRTC token asAuthorization: Bearer. webrtc-bridgecreates the call ID and returns it, together with the ICE servers for the call.- ICE gathering completes before the SDP offer is sent.
- The SDP offer and answer are exchanged over HTTPS.
- Once media connects, a second negotiation starts the agent’s audio.
- Teardown uses
DELETE /api/v1/call/{id}.
start() returns as soon as the call is under way, with the state Connecting. Observe state for Connected. The agent-track negotiation in step 5 runs after that, on your behalf. The call still links to the same messaging session, so the agent transcript is the same. The default fallback STUN server (IceServer.DEFAULT) is stun.cloudflare.com.
Permissions
The SDK’s manifest auto-merges the three permissions every call needs. You don’t declare these:RECORD_AUDIO is a runtime permission. Your app must still request it before start(); the call fails fast with PolyError.Voice.MediaFailed if it’s missing. Add the rest only for the optional features you use:
BLUETOOTH_CONNECT is not declared by the SDK. Your app adds it and requests the grant when it needs Bluetooth routing.
Call lifecycle
- Retain one
VoiceCallwhile the owning screen is active. - Observe its
stateStateFlow:collectAsStateWithLifecycle()in Compose, orrepeatOnLifecycle(Lifecycle.State.STARTED)in Views. - Call
call.close()when the owning screen or component is permanently disposed.
- Jetpack Compose
- Android Views
Audio output
By default the call follows the connected accessory: a wired or Bluetooth headset is used automatically (and auto-switches when you plug/unplug one mid-call). When nothing is connected it falls back to the loudspeaker (VoiceOptions(speakerphone = false) falls back to the earpiece instead). To let users pin a specific output, observe call.audio and call setAudioDevice. Pass null to return to automatic:
AudioDevice.typeis one ofEARPIECE/SPEAKER_PHONE/WIRED_HEADSET/BLUETOOTH;nameis a picker-friendly label. The list updates live as headsets connect and disconnect.- Switching is asynchronous. Bluetooth can take a few seconds to engage. Drive your UI off
call.audio, not offsetAudioDevicereturning. Selecting an unavailable device is a no-op. - Bluetooth needs
BLUETOOTH_CONNECT. The library does not declare this runtime permission for you. Add it to your app and request the grant if you want Bluetooth outputs to appear. Without it, Bluetooth is simply absent, with no crash.
Interruptions (incoming calls, other apps)
The SDK manages audio focus for you: it takes focus onstart() and releases it on end() or teardown. It also reacts to losing focus while a call is live:
- A transient loss (a notification, a navigation prompt) mutes the mic for the duration and restores it automatically when focus returns. The call stays
Connected. - A permanent loss (the user answers an incoming phone call, or another app starts an exclusive audio session) ends the call. It surfaces as
CallState.Failed(PolyError.Voice.Interrupted)and the mic is released.
state and tell the user; the mic is already released:
Background calls
AVoiceCall is a plain object on its own coroutine scope. It is not tied to your Activity or Fragment lifecycle, so the SDK won’t end a call just because your UI is backgrounded. However, when your app goes to the background, Android 9+ cuts mic capture and throttles the WebRTC media and network threads, so the connection silently dies within ~15s and the SDK reports CallState.Failed(PolyError.Voice.Disconnected). To keep a call alive in the background you need two things while the call is active:
- A microphone foreground service: grants background mic access and keeps the process foregrounded.
- A partial wake lock: keeps the CPU running for the media and network threads.
call.start() and stop it when the call ends. The SDK is headless and deliberately doesn’t impose a service (it has no notification UI). A foreground-only call works without any of this. Both voice example apps ship a complete, working CallForegroundService you can copy.
R8 / ProGuard
No keep rules needed in your app. Theai.poly:voice AAR ships consumer R8 rules (applied automatically) that keep org.webrtc.**. libwebrtc is reached by name over JNI from native code, which R8 can’t see, so stripping it would crash the audio engine. If you maintain an unusually aggressive global proguard-rules.pro, the shipped consumer rules still protect the SDK; you don’t add anything.
Example apps
A one-screen tap-to-call demo with the audio-output picker ships in both toolkits. Add your connector token and WebRTC token to thePolyMessaging.initialize(...) call in the example’s Application class and run:
Related pages
Android SDK
Chat with ai.poly:messaging: installation, authentication, sessions, and UI
Multichannel agents
Build agents that work across voice, webchat, and mobile

