Skip to main content
ai.poly:voice places live, two-way WebRTC voice calls to a PolyAI agent — 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 — no new concepts. Calls are user-initiated: the user taps to call your agent. Inbound (push-triggered) calls are not supported.

Source on GitHub

polyai/android-sdk — includes the full polyvoice technical guide and runnable voice example apps.

Installation

Quickstart

A call needs the RECORD_AUDIO runtime permission. The SDK declares it in its manifest, but your app must request the grant from the user before starting a call.
CallState, PolyError.Voice.*, Configuration, and Environment are the same types from ai.poly:messaging. Java callers get Executor + Callback<Unit> overloads of start / end / setMuted, mirroring the chat API.

Credentials

A voice call needs two credentials, both on your agent in Agent Studio › Connector Settings (the same connector you use for chat): Both are always required and always distinct: the API key authenticates the connector and is shared by chat and voice, so it lives on the shared Configuration; the WebRTC token authenticates the media gateway only, so it lives on VoiceOptions. Two more values have sensible defaults, so most apps don’t set them:
  • environment defaults to Environment.US. Set .UK / .EUW — or .cluster("…") for a named cluster — only if your agent lives in another region. The same Environment you use for chat also selects the voice gateway.
  • hostIdentifier (sent as X-Host) defaults to your app’s package name (applicationId).
Custom / self-hosted gateway: the WebRTC gateway host is derived from your Environment. If you run a dev or self-hosted gateway, set VoiceOptions.signalingHost (no scheme, e.g. "webrtc-gateway.example.com") — it’s required with Environment.Custom, since the gateway host can’t be derived from a custom messaging endpoint.

Permissions

The SDK’s manifest auto-merges the three permissions every call needs — you don’t declare these:
RECORD_AUDIO is a runtime permission — 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:

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’s 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 — and pass null to return to automatic:
  • AudioDevice.type is one of EARPIECE / SPEAKER_PHONE / WIRED_HEADSET / BLUETOOTH; name is a picker-friendly label. The list updates live as headsets connect/disconnect.
  • Switching is asynchronous — Bluetooth can take a few seconds to engage. Drive your UI off call.audio, not off setAudioDevice returning. 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 — no crash.

Interruptions (incoming calls, other apps)

The SDK manages audio focus for you — it takes focus on start() and releases it on end()/teardown. It also reacts to losing focus while a call is live, so you don’t have to:
  • 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. Nothing to handle.
  • 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.
So all you do is observe state and tell the user — the mic is already released for you:

Background calls

A VoiceCall is a plain object on its own coroutine scope — it is not tied to your Activity/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/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:
  1. A microphone foreground service — grants background mic + keeps the process foregrounded.
  2. A partial wake lock — keeps the CPU running for the media/network threads.
Start the service before 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) — that’s your app’s call. A foreground-only call works fine 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. The ai.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 a global proguard-rules.pro that’s unusually aggressive, 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 — drop your connector token and WebRTC token into the PolyVoice.call(...) block and run:

Android SDK

Chat with ai.poly:messaging: installation, authentication, sessions, and UI

Multichannel agents

Build agents that work across voice, webchat, and mobile
Last modified on August 13, 2026