Skip to main content
An iOS, Android, React Native, or Flutter app can talk to your Retell voice agent over WebRTC, with no browser and no phone number. Your server creates a web call, and the app connects to it with a standard WebRTC library using a short HTTP signaling exchange.

When to use it

  • Support inside your app. Add a “Talk to us” button to a help or order screen, so users reach the agent without leaving the app or dialing a number.
  • Voice-driven flows. Let users book, reschedule, or check a status by voice, with the call controls and visuals in your own native UI.
In a browser, use the Web SDK instead, since it handles all of this for you. To reach users on their phone number rather than in your app, see outbound calls. For example, a pharmacy app adds a “Talk to us” button to its refill screen. The app asks its own backend for a call, the backend passes the patient’s name and prescription ID as dynamic variables, and the agent can check the refill status during the conversation.

How does a native web call work?

  1. Your app asks your backend for a call.
  2. Your backend calls Create Web Call with your API key and returns the connection details to the app.
  3. The app creates a WebRTC peer connection, adds the microphone, and opens a data channel named control.
  4. The app posts its SDP offer to Retell and applies the SDP answer it gets back.
  5. Audio flows directly between the app and Retell. The control channel reports when it opens and when the call ends.
  6. The app ends the call by sending a DELETE request, or the agent ends it.
Signaling goes through api.retellai.com over HTTPS. Media is Opus audio over DTLS-SRTP, relayed through the TURN servers in ice_servers when the network requires it.

Prerequisites

  • A voice agent and its agent ID.
  • A Retell API key on your backend.
  • A WebRTC library for your platform. The examples on this page use:
  • On iOS, NSMicrophoneUsageDescription in Info.plist.
  • On Android, these permissions in AndroidManifest.xml: INTERNET, RECORD_AUDIO, MODIFY_AUDIO_SETTINGS, and ACCESS_NETWORK_STATE. React Native apps that use react-native-incall-manager also need WAKE_LOCK. Without ACCESS_NETWORK_STATE or WAKE_LOCK, the app crashes when the call starts.
Create native web calls on your server with an API key. Never ship an API key inside an app, since anyone can extract it. Public keys don’t help either: they limit calls to your allowed domains by checking the Origin or Referer header, which a native app can set to anything.

Connect a native app

1

Create the call on your server

Add an endpoint your app can call. It creates the web call and returns the fields the app needs.
server.js
Create Web Call returns these fields. The example forwards only the three the app needs.Connect right after creating the call. If the app doesn’t connect within 5 minutes, the call ends with the disconnection reason error_user_not_joined (as of October 2026).
2

Create the peer connection

Configure the peer connection with the ice_servers from your backend, add a microphone audio track, and create a data channel labeled control before you create the offer. The data channel must exist in the first offer, or the call has no event channel.Put the device in voice-call audio mode: playAndRecord with the voiceChat mode on iOS, and MODE_IN_COMMUNICATION on Android. This turns on the device’s echo cancellation, so the agent doesn’t hear itself.
3

Send the offer

Create an offer and set it as the local description. Wait 1 second for ICE gathering so the offer contains your candidates, then post the current local description. Don’t wait for gathering to finish without a limit: in our tests on the iOS Simulator and Android Emulator over Wi-Fi, every candidate arrived within 200 ms, but gathering didn’t report complete until 5 seconds later. To connect even sooner, use trickle ICE.
identity must be client, the identity the access token was issued for. A successful response is 201 Created:
Set sdp as the remote description with type answer. The answer already contains all of Retell’s candidates, so there are none to wait for. Keep session_id; you need it to end the call.If the peer connection doesn’t reach connected within 15 seconds of the POST, Retell closes the session (as of October 2026).
4

Handle call events

Read JSON messages from the control data channel:Also watch the peer connection state, since some endings send no message. If it reaches failed or closed, the call is over. If Retell’s side of the connection stays disconnected for 5 seconds, Retell ends the call (as of October 2026); see reconnect after a network change.You can send these messages on the same channel:To mute, you can also disable the local audio track; either way the call stays connected.
5

End the call

Retell returns 204 No Content, including when the call has already ended, or 401 if the access token has expired. Close the peer connection and release the microphone too. The examples close locally before sending the DELETE, so the audio stops right away.

Platform examples

Each example shows the core of a call: connect() takes the call_id, access_token, and ice_servers from your backend endpoint, and hangUp() ends the call. App plumbing, such as calling your backend and converting ice_servers into the library’s type, is left out. The examples are trimmed from apps tested on the iOS Simulator and Android Emulator with stasel/WebRTC 156.0.0, stream-webrtc-android 1.3.10, react-native-webrtc 155.0.1 on React Native 0.88, and flutter_webrtc 1.6.3 (as of October 2026).
Remote audio plays through the device’s audio output automatically in all four libraries; you don’t need to attach the incoming track to a player.

Common pitfalls

  • urls can be a string or an array. When you convert ice_servers for the iOS or Android SDK, handle both forms. React Native and Flutter accept either.
  • Cap the wait for ICE gathering. Gathering can take 5 seconds to report complete even though the candidates arrive within 200 ms. Wait 1 second, or use trickle ICE. If calls fail on networks that block UDP, switch to trickle ICE, since the TURN-over-TLS candidate can arrive after the wait.
  • Make close() safe to call twice. Closing the peer connection fires its state callback, which calls close() again. Clear your reference before you close it.
  • Clean up when connect() fails or the user hangs up mid-connect. Close the peer connection on any error. After each wait, such as the microphone prompt, the ICE wait, or the POST, check that the user didn’t hang up in the meantime, and release what you just acquired if they did.
  • On Android, don’t close the peer connection on a WebRTC callback thread. It can deadlock. Hand the close off to another thread, as the Kotlin example does.

Signaling reference

All signaling requests go to https://api.retellai.com/webrtc-proxy/{call_id}/v1/webrtc/sessions and carry Authorization: Bearer {access_token}.

Trickle ICE

Waiting briefly for ICE gathering before the POST is simplest. To connect as fast as possible, post the offer right away and send each later candidate with PATCH once you have the session_id; in the same tests, this connected in under 100 ms. Queue candidates that arrive before the POST returns, then send them in order. The fields are candidate, sdpMid, and sdpMLineIndex, as in the standard RTCIceCandidateInit.

Errors

Reconnect after a network change

When the app switches networks, the peer connection goes disconnected. Reconnect right away: create a new peer connection, POST a new offer with the same call_id and access_token, then close the old peer connection. The new connection takes over the call. If the old connection is still reachable, it also receives {"type":"status","state":"replaced"}. If Retell’s side of the connection stays disconnected for 5 seconds (as of October 2026), Retell ends the call, and a later reconnect fails. Create a new call instead.

After the call

  • Register a webhook to receive call_started, call_ended, and call_analyzed on your server.
  • Use the call_id with Get Call for the transcript, recording, and analysis, or review the call in session history.
  • For live captions in the app, have your backend connect to the monitoring WebSocket with your API key and relay the transcript to the app.

FAQ

Not a dedicated one (as of October 2026). Native apps use a standard WebRTC library with the signaling described on this page. The browser Web SDK uses the same protocol.
No. Public keys limit calls to your allowed domains through the Origin or Referer header. A browser sets those headers itself, but a native app can send any value, so the check doesn’t protect calls from an app. Create the call on your backend with an API key and send the app only the call_id, access_token, and ice_servers.
Check AndroidManifest.xml for ACCESS_NETWORK_STATE, which WebRTC needs to monitor the network, and for WAKE_LOCK if you use react-native-incall-manager. Logcat shows a SecurityException naming the missing permission just before the crash.
Check that the audio session is active in a voice-call mode (playAndRecord with voiceChat on iOS, MODE_IN_COMMUNICATION on Android) and that the output route is the speaker or headset you expect.
Echo cancellation is off. Use the voice-call audio modes above, which turn on the device’s echo cancellation.
Make sure you pass all of ice_servers to the peer connection, including the TURN entries and their username and credential. TURN over TLS on port 443 reaches networks that block UDP.
Yes. They are web calls and draw from the same concurrency pool as other calls.