Graine AI

How calls work

The architecture of in-app calls — what runs where, the two engines, each call step by step, its states, how a ring reaches a locked phone on iOS and Android, audio over WebRTC or the WebSocket, networks, and what data goes where.

This page explains the moving parts behind In-app calling, for the engineers who build on it and run it. Set-up steps are on that page; the server side is in Your calling server.

What runs where

The customer's phoneYour serverdecides to callstores device tokensrecords outcomesPush serviceAPNs (iOS)FCM (Android)Your appyour Call buttonwho, screen, actionsreports outcomesGraine SDKnative call enginethe ring and the call screenmicrophone and speakerecho cancelling, Bluetoothlock screen, screen offthe line to your agentYour agenthears and speaksruns your actionsCall Historytranscript, outcomestartCall123456

The parts of an agent's call. The numbers follow one call; a call the customer places starts at your app's startCall instead of a push.

  1. Your server decides to call and sends a push — type: agent_call, your call_id, a reason — to the customer's device through Apple's push service (APNs) or Firebase Cloud Messaging (FCM).
  2. The push wakes your app and the SDK rings, even when the app was closed and the phone locked.
  3. The customer answers. The SDK connects them to your agent: audio both ways, with what your app already tells the agent — who the customer is, your prompt variables, the current screen — and the agent's requests for your actions.
  4. The SDK tells your app what happened: answered, then ended — or declined, missed or failed — each with the call_id.
  5. Your app reports it to your server, which keeps the call's record.
  6. The conversation lands in Call History, under the customer's name: the transcript, the outcome, the actions the agent took.

With your own push server, Graine is in steps 3 and 6 only. Your server and your app own the decision to call, the push, the device tokens and the record — no customer data has to be copied to Graine to ring someone. With hosted ringing, Graine takes steps 1, 2 and 5 as well: a booked call falls due, Graine sends the push with your credentials, and the SDK reports each outcome to Graine instead of your server — while still telling your app.

Two engines, one agent

Assistant barthe in-app enginevoice and textdraws components on screenlives in your app's own screensCallthe native call enginevoice onlyrings on the lock screen: CallKit, call notificationkeeps going with the screen offYour agentone prompt · your screen context · actionsOnly one runs at a time — which ends which is in the table below.

The assistant bar and a call are two engines in the same SDK, talking to the same agent.

The assistant bar runs on the in-app engine: voice and text, with components drawn on your screens. A call runs on the native call engine, built into the SDK: the call's audio runs in your app's own process, through the platform's microphone, speaker and echo canceller. That is what lets a call ring through CallKit on iOS and a full-screen notification on Android, and keep going with the screen off.

Both talk to the same agent, with the same prompt, and a call carries what your app already tells the agent. On iOS and Android that happens by itself: who the customer is, your prompt variables and the current screen go with the call when it connects, screen changes keep reaching it while it lasts, and the agent's requests run your registered actions. Variables are read once, at the start, so a change made during a call reaches the next one. React Native and Flutter hand the same things to a call through attach().

A call is voice only: components the agent shows are not drawn during a call.

Only one engine runs at a time:

A call ringsA call is answered or placed
iOSEnds a conversation in the barEnds it
Android—Ends it
FlutterEnds itEnds it
React NativeEnds it, with attach() and the in-app SDK 0.40.2Ends it, likewise

While a call is live, starting the bar is refused with an error — "A call is already in progress." — on every platform (React Native with attach()).

A call, step by step

The agent calls the customer

Your serverAPNs / FCMYour appGraine SDKCustomerYour agentcall record: ringingconversation: audio, screen context, actionspushtype · call_id · reasondelivers it, waking your appVoIP push (iOS) · data message (Android)ringsCallKit · call notificationanswersoutcome: answeredyour app reports itconnectswho_called = agent · call_reasonhangs up, or the agent doesoutcome: endedlocal · remote · failedreports it; you close the recordNot answered within the ring timeout → missed · declined → declinedA ring during another call → missed at once, and the call goes on

An agent's call, from your server's push to the closed record. Solid arrows happen on the call's path; dashed ones are reports.

The outcome reports are the only way your server hears about the call, which is why the record has timers for the reports that never come.

The customer calls the agent

CustomerYour appGraine SDKYour agentconversation: audio, screen context, actionstaps CallstartCall(reason)returns the call's idcall screenthe system's on iOS · the SDK's on Androidconnectswho_called = customer · call_reasonhangs upoutcome: ended (local)Your app reports the outcome to your server, as for an agent's call.

A customer's call: no push, no ring — startCall places it at once.

startCall returns the call's id straight away, or nothing while another call is live. The agent hears who_called = "customer" and your reason, so one prompt can handle both directions.

The call's states

idleringingconnectingactiveendedpushanswerconnectedhang upstartCallcan't connect → faileddecline → declined · no answer → missed · busy → missedreported once, then back to idleansweredended

Every state a call passes through, and the outcome each ending reports.

StateMeans
idleNo call
ringingAn agent's call is ringing on this device (incoming calls only)
connectingAnswered, or placed by the customer; the line is opening
activeThe agent and the customer can hear each other
endedOver. Reported once, then back to idle

A screen of your own should treat ended and idle alike: on Android a call can go from active straight to idle, and the outcome says how it ended. The SDK holds one call at a time on a device. A ring that arrives during a call is ended at once and reported missed, without touching the call in progress, and startCall returns nothing while a call is live.

How a ring reaches a locked phone

iOSAndroidVoIP pushfrom APNsiOS wakesyour appthe SDK reportsit to CallKitat oncethe system ringslock screen tooanswer: CallKitturns on audiothe SDK connectsto your agentFCM datamessage,high priorityyour servicecalls handlePushcall notificationfull screenwhen lockedanswer opensthe call screenit starts themicrophoneservicethe SDK connectsto your agentOnce connected, the call goes on with the screen off, over Bluetooth, while the customer uses other apps.

The path from a push to a connected call, on each platform.

iOS

  • A VoIP push wakes your app, or launches it if it was closed. iOS then requires the app to report a call to CallKit before it finishes handling the push, and stops delivering VoIP pushes to apps that do not. The SDK reports every one — even a malformed push, or one that arrives during a call, which it ends at once. This is why enableVoIPPushes runs at every launch, in didFinishLaunching: iOS delivers a push only to an app that has registered for it in that launch.
  • CallKit draws the ring — on the lock screen, and as a banner when the phone is in use — and the call screen while the phone is locked or your app is in the background.
  • The system turns the audio on. Under CallKit the SDK does not start audio itself: iOS activates it when the call is answered, and the SDK starts the microphone and speaker at that moment.
  • What the ring shows is your configured name, or the push's caller_name, with the system ringtone unless you name a sound file, and a template icon on the system's call screen if you give one. Calls stay out of the phone's Recents unless you set includesCallsInRecents.
  • The phone's settings come first. If Do Not Disturb, a Focus or the block list stops CallKit from ringing, the SDK reports missed at once.

Android

  • A high-priority FCM data message reaches your app even when it was closed. The SDK's own messaging service hands its data to handlePush — or your app's, when it has one of its own (which then receives every message): it calls handlePush first. For a call push the SDK takes over and returns true.
  • The SDK posts a call notification with Answer and Decline and a ringtone. On a locked phone it opens the SDK's call screen full screen — from Android 14 that needs the full-screen-intent permission, without which the ring is a heads-up notification that still rings. On Android 12 and later it is the system's call-style notification.
  • Notification channels. Your app gets two, Incoming calls and Ongoing calls. A customer who turns Incoming calls off gets no ring.
  • Notifications must be allowed. On Android 13 and later a ring is a notification, so without the notification permission nothing rings — unless your app is open, when the SDK opens its call screen straight away — and the call times out as missed. Ask for it during onboarding; the SDK does not.
  • The microphone service starts from the call screen. Android does not let an app start using the microphone from the background, so the SDK starts its microphone service when the customer answers on the call screen, or when your app calls startCall. The service keeps the call going when the screen locks or another app opens, with an ongoing notification and Hang up.
  • Battery savers. Some manufacturers' battery savers stop background work beyond Android's own rules. If calls drop on a particular phone, the customer may need to exempt your app in its battery settings.

Audio

  • Routes. A call starts on the earpiece, like a phone call; setSpeaker(true) moves it to the loudspeaker.
  • Headsets. A call goes to a Bluetooth headset or car kit when one is connected, else a wired headset, else the earpiece — as a phone call does — and follows when a headset is switched on or off mid-call. Turning the loudspeaker off brings the call back to the headset. (Android from SDK 0.6.1.)
  • Echo. The platform's echo canceller runs on every call. On the WebSocket the SDK adds its own check so the agent does not hear itself on the loudspeaker and interrupt itself; on WebRTC the echo canceller does that alone.
  • Mute stops sending the customer's audio; the agent goes on speaking. On iOS it is the system call's mute, so the system's call screen shows it too.
  • Interruptions. On iOS, a phone call that arrives during an agent's call lets the customer end one and take the other — an agent's call cannot be put on hold — and if iOS interrupts the audio, the SDK rebuilds it when the interruption ends, or ends the call as failed if it cannot. On Android, a phone call ringing or another app taking the audio puts the call on hold — nothing is sent or played until the audio comes back — and answering the phone call ends the agent's call. A notification sound changes nothing.

WebRTC or the WebSocket

A call's audio travels one of two ways — the same for a call the customer places and a call Graine rang:

WebRTCThe WebSocket
What your app needsThe SDK's WebRTC module, added to the app (from 0.9.0)Nothing: every build has it
A weak or changing networkA jitter buffer and loss concealment keep the agent's voice steady through a moment's lossA lossy network sounds lossy: the voice stalls, then catches up
EchoThe platform's echo cancellerThe platform's echo canceller, and the SDK's own check on the loudspeaker
The agent's voiceTelephone bandwidth todayWider on a good network
A dropped connectionOne chance to recover, then the call endsThe call ends

Everything else is the same on both: the ring, the call screen, the states, the outcomes, who the customer is, memory, variables, screen context, actions and Call History.

Which one a call takes is decided when it starts — at the tap, or when a ring arrives:

  1. The agent's In-app call audio setting (agent → Calls & scheduling): Standard keeps every call on the WebSocket, whatever the app asked, from each app's next start. WebRTC (recommended) leaves the choice to the app.
  2. The app's callTransport: auto (the default) and webrtc ask for WebRTC; websocket keeps the WebSocket.
  3. WebRTC needs the module in the app. Without it the call takes the WebSocket.

Prepared early. When a call will use WebRTC, the SDK sets it up while the ring is still showing, or the moment the customer taps Call, so answering connects as fast as before. A declined, missed or expired ring lets go of what was prepared: nothing was ever dialled. On iOS the audio starts only once the system call hands it over, so the agent's first words are never clipped.

When WebRTC cannot carry a call before any audio flows — a network that blocks it, a refusal, no answer — the call continues on the WebSocket inside the same call: no second ring, no new call screen, no error. WebRTC gets 12 seconds in all; the customer sees Connecting… a moment longer. A call the agent cannot take at all — no credit left, the agent switched off — ends with that reason, as it does on the WebSocket. Once the audio flows a call never changes road.

Which one ran is on the call: transport (webrtc or websocket) and, for a call that tried WebRTC and runs on the WebSocket, transportFallbackReason — mint_failed, register_failed, no_response, no_answer, ice_timeout or error (what each means). Adding the module: Calls over WebRTC.

Networks

  • Opening the line must succeed within 15 seconds, or the call is reported failed.
  • A dropped connection ends the call. The SDK does not move a call to a new connection: if the network drops, the call ends — ended, with failed — so offer a call back. On WebRTC a connection that drops gets one chance to recover first; if it is not back within about 10 seconds, the call ends.
  • A call needs a data connection on the phone for its whole length, like any internet call.

What data goes where

DataGoes toWhen
The push (call_id, reason, variables)Your server → APNs or FCM → the phoneWhen you ring
The push, with hosted ringing (call_id, the call name and the agent's id only)Graine → APNs or FCM → the phoneWhen a booked call is due
Push tokensThe phone → your server; with hosted ringing, the phone → Graine, stored encryptedAt every launch
The call's audioThe phone ↔ your agent, over WebRTC or the WebSocket — encrypted either wayOnly while a call is connected
Who the customer is, prompt variables, the current screenThe phone → your agentWhen the call connects, and as the screen changes
Action requests and resultsYour agent ↔ your appDuring the call
OutcomesThe SDK → your app → your server; with hosted ringing, the SDK → Graine as wellAs they happen
Transcript and outcomeCall HistoryAfter the call

Screen context is masked on the phone before it is sent, exactly as for the assistant bar, and so are the variables a push or startCall brings — card numbers, email addresses, phone numbers and other personal numbers. The reason is sent as it is, which is one more reason to make it an identifier. See Privacy and security.

Limits

Calls at onceOne per device
Ring timeout30 seconds unless you set another (at least 1 second on iOS)
Opening the line15 seconds
Setting a call up on WebRTC12 seconds, then the call continues on the WebSocket
An action's answerWithin 6 seconds, or the agent hears that it did not finish in time
A call's variablesUp to 12, who_called and call_reason included; names up to 40 characters, values up to 200, about 1 KB in all — the call's own first, then your app's while they fit
MediaVoice only

On this page