Calling reference
Every method, option, field, state, outcome and push key for in-app calls, side by side for iOS, Android, React Native and Flutter — with the limits, the error messages and what each platform does differently.
Versions
| Platform | Package | Calls from | Requires |
|---|---|---|---|
| iOS | GraineInApp (Swift package, https://github.com/GraineAI/inapp-ios) | 0.6.0 | iOS 15 |
| Android | ai.graine:inapp-android | 0.6.0 | Android 7 (API 24) |
| React Native | @graineai/inapp-react-native-calls | 0.1.0 | React Native 0.71, a native build. Screen context and actions need @graineai/inapp-react-native 0.40.1 |
| Flutter | graine_inapp | 0.6.1 | Flutter 3.19 |
| Calls over WebRTC (optional) | iOS GraineInAppRTC (https://github.com/GraineAI/inapp-ios-rtc); Android ai.graine:inapp-android-rtc; React Native calls 0.4.0 and Flutter built with WebRTC | 0.9.0 | The same version as the SDK it sits beside — Calls over WebRTC |
The latest are iOS, Android and Flutter 0.7.0, React Native 0.41.0 and its calls package 0.2.0 — the versions Graine's own ringing (hosted ringing) and several agents in one app need. What each changed is in the changelog.
Configuration
iOS — GraineCallsConfig
Passed to GraineInApp.shared.calls.configure(_:). You can call configure again;
the change applies to the next ring.
| Option | Type | Default | |
|---|---|---|---|
displayName | String | "": your app's name | What the ring and the call screen show. A push's caller_name replaces it for that call |
ringtoneSound | String? | nil: the system ringtone | A sound file in your app's bundle |
iconTemplateImageData | Data? | nil | A PNG template image, 40 × 40 points on a transparent background, for your app's button on the system call screen |
includesCallsInRecents | Bool | false | Whether calls appear in the phone's Recents |
ringTimeout | TimeInterval | 30 | Seconds before an unanswered ring is missed; at least 1 |
callTransport | GraineCallTransport | .auto | What carries the calls' audio: .auto (WebRTC when GraineInAppRTC is in the app), .websocket or .webrtc. 0.9.0 |
Android — GraineCallsConfig
Passed to GraineInApp.get().calls.configure(config), in Application.onCreate.
| Option | Type | Default | |
|---|---|---|---|
displayName | String | "Assistant" | What the ring and the call screen show. A push's caller_name replaces it for that call |
smallIcon | Int (a drawable) | 0: the platform's call icon | The monochrome status-bar icon |
ringtone | Uri? | null: the phone's ringtone | |
callActivity | Class<out Activity>? | null: GraineCallActivity | Your own call screen — see the call screen |
ringTimeoutMs | Long | 30_000 | Milliseconds before an unanswered ring is missed |
callTransport | GraineCallTransport | AUTO | What carries the calls' audio: AUTO (WebRTC when ai.graine:inapp-android-rtc is in the app), WEBSOCKET or WEBRTC. 0.9.0 |
React Native and Flutter
GraineCalls.configure(options) in React Native, and
GraineCalls.instance.configure(GraineCallsOptions(...)) in Flutter. The options
are kept on the phone, so a push that wakes a closed app rings before any
JavaScript or Dart has run. A new publishableKey or baseUrl takes effect at
the app's next launch.
| React Native | Flutter | Default | |
|---|---|---|---|
publishableKey | publishableKey | Required | The agent's publishable key (Agent → Embed & Widgets) |
displayName | displayName | Required | What the ring and the call screen show |
baseUrl | baseUrl | https://www.graine.ai | |
ringtoneSound | ringtoneSound | The phone's ringtone | iOS: a sound file in your bundle. Android: a raw resource — ring and sounds/ring.caf both mean res/raw/ring; an unknown name logs a warning and uses the phone's ringtone |
ringTimeoutMs | ringTimeout (a Duration) | 30 seconds | |
appVersion | appVersion | Lands on the call record | |
callScreen | callScreen | "sdk" (GraineCallScreen.sdk) | Who draws the call while your app is open: the SDK's screen, or "app" for your own |
android.smallIcon | androidSmallIcon | The platform's call icon | A drawable's name; an unknown name logs a warning |
ios.iconTemplate | iosIconTemplate | An image's name in your asset catalogue | |
ios.includesCallsInRecents | iosIncludesCallsInRecents | false | |
debug | debug | false | The native SDK's debug log for calls — see Logging |
callTransport | callTransport (GraineCallTransport) | "auto" (GraineCallTransport.auto) | What carries the calls' audio: "auto" (WebRTC in a build with WebRTC), "websocket" or "webrtc" — exact, case-sensitive words. Kept for cold starts only when it is not auto. React Native calls 0.4.0, Flutter 0.9.0 |
A wrong value fails configure with the code invalid_options and a message
naming the option — for example callScreen must be "sdk" or "app". Unknown keys
are ignored.
callScreen: "app" — on iOS the SDK presents nothing while your app is open,
and a locked iPhone still shows the system's call screen. On Android your app's
launch activity draws the call; if it cannot be found, the SDK uses its own
screen and logs why.
Methods
iOS — GraineInApp.shared.calls | Android — GraineInApp.get().calls | React Native — GraineCalls | Flutter — GraineCalls.instance | |
|---|---|---|---|---|
| Configure | configure(_:) | configure(config) | configure(options) | configure(options) |
| Receive VoIP pushes | enableVoIPPushes(onToken:) | — | enableVoIPPushes(onToken) | enableVoIPPushes(onToken) |
| Hand over a push | Automatic; your own registry: reportIncomingCall(payload:completion:) | Automatic with Firebase in the app; from a messaging service of your own: handlePush(data) | Automatic with Firebase in the app; from a messaging service of your own: handlePush(data) | Automatic with Firebase in the app; from a messaging service of your own: handlePush(data) |
| Hand over the push token, for hosted ringing | Automatic with enableVoIPPushes() | Automatic with Firebase in the app; from a messaging service of your own: GraineCalls.onNewToken(context, token) | Automatic with Firebase in the app; from a messaging service of your own: registerPushToken({ token }) | Automatic with Firebase in the app; from a messaging service of your own: registerPushToken(token) |
| Place a call | startCall(reason:variables:) | startCall(activity, reason, variables) | startCall({ reason, variables }) | startCall(reason:, variables:) |
| Answer | answer() | answer() | answer() | answer() |
| Decline | end() while ringing | decline() | decline() | decline() |
| Hang up | end() | end() | end() | end() |
| Mute | setMuted(_:) | setMuted(value) | setMuted(muted) | setMuted(muted) |
| Loudspeaker | setSpeaker(_:) | setSpeaker(on) | setSpeaker(on) | setSpeaker(on) |
| Outcomes | GraineCallsListener.onCallOutcome(_:callId:) | GraineCallListener.onCallOutcome(callId, outcome) | onOutcome(fn) | outcomes (a stream) |
| Rings | onIncomingCall(_:) | onIncomingCall(call) | onIncomingCall(fn) | incomingCalls (a stream) |
| The call, to draw | Published properties | Properties and GraineCallListener | useGraineCall(), getSnapshot(), subscribe(fn) | value — a ValueListenable<GraineCallSnapshot> |
| Screen context and actions | Automatic | Automatic | attach(client) | attach() |
| Permission results | — | onRequestPermissionsResult(code, results), from your own call screen | Automatic | Automatic |
| Ask for the call permissions | requestCallPermissions() (async) or requestCallPermissions(completion:) | requestCallPermissions(activity) { p -> } | requestCallPermissions() | requestCallPermissions() |
| Read them | callPermissions | callPermissions | callPermissions() | callPermissions() |
| Open Settings | openCallSettings(_:) | openCallSettings(item) | openCallSettings(item) | openCallSettings(item) |
| What the setup lacks | callSetupProblems | callSetupProblems | callSetupProblems() | callSetupProblems() |
| Platform check | — | — | isAvailable | isSupported |
startCallreturns the call's id, or nothing while another call is live. On Android it must be called on the main thread.handlePush(Android, and the Android side of React Native and Flutter) is what the SDK's own messaging service calls for every message; an app with a messaging service of its own, which receives every message instead, calls it first. It returnsfalsefor a push that is not a call, andtruefor any call push: it rings, or is reportedmissedduring another call, and one without acall_idis dropped with a warning. On iOS it always returnsfalse: VoIP pushes reach the SDK directly.enableVoIPPushesdelivers the token as hex, at once if it is already known, andnil/nullwhen iOS invalidates it. In React Native and Flutter it returns a function that stops the updates.attach(React Native, Flutter) hands a call who the customer is, your prompt variables, the current screen and your actions, and keeps them in step. It returns a function that detaches. Without it, the agent's action requests during a call are refused. In React Native with@graineai/inapp-react-native0.40.2 or later, it also ends a conversation in the assistant bar when a call goes live, and keeps the bar from starting until the call is over.- Listeners are called on the main thread. iOS holds them weakly; Android holds them until you remove them.
- Before your listener exists — an app launched by a push, say — React Native and Flutter keep the last 20 outcomes and rings and deliver them when you subscribe.
The call
| iOS | Android | React Native | Flutter | |
|---|---|---|---|---|
| State | state | state | state | status |
| Call id | callId | call?.callId | callId | callId |
| Direction | direction | call?.direction | direction | direction |
| Reason | — | call?.reason | reason | reason |
| Name shown | callerName | call?.callerName | callerName | callerName |
| Muted | muted | muted | muted | muted |
| Loudspeaker | speakerOn | speaker | speakerOn | speakerOn |
| The agent is speaking | agentSpeaking | agentSpeaking | agentSpeaking | agentSpeaking |
| The line being said | caption | caption | caption | caption |
| Connected at | connectedAt (Date) | connectedAt (SystemClock.elapsedRealtime() ms) | connectedAt (ms since the epoch) | connectedAt (DateTime) |
| Why it could not connect | In the failed outcome | In the failed outcome | error | error |
| What carries the audio | transport (GraineCallTransport?) | transport ("webrtc" / "websocket") | transport | transport (GraineCallTransport?) |
| Why a call that tried WebRTC is on the WebSocket | transportFallbackReason | transportFallbackReason | transportFallbackReason | transportFallbackReason |
Call ids are your server's call_id for a call it rang. A call the customer
places gets a generated one — call_ and a lowercase UUID, on every platform.
Treat ids as opaque strings.
States
| State | iOS | Android | React Native | Flutter | Means |
|---|---|---|---|---|---|
| Idle | .idle | IDLE | "idle" | GraineCallStatus.idle | No call |
| Ringing | .ringing | RINGING | "ringing" | .ringing | An agent's call is ringing (incoming only) |
| Connecting | .connecting | CONNECTING | "connecting" | .connecting | Answered or placed; the line is opening |
| Active | .active | ACTIVE | "active" | .active | The agent and the customer can hear each other |
| Ended | .ended(reason) | ENDED | "ended" | .ended | Over |
On iOS, ended lasts one second before idle, so a call screen can show how the
call ended. On Android, ended passes at once; React Native and Flutter can go
from ringing, connecting or active straight to idle. Treat ended and
idle alike, and take how the call ended from its outcome. On iOS a state
change arrives before its outcome; on Android, after — don't depend on the order.
Outcomes
| Outcome | Reported when | With |
|---|---|---|
answered | The customer answered an incoming call. ended or failed follows | |
declined | The customer declined, or hung up while it rang | |
missed | The ring timed out; or it arrived during another call. iOS also: Do Not Disturb, a Focus or the block list stopped the ring, or the system reset the call | |
ended | A call that connected is over (on Android, also when the customer answers a phone call during a hold) | local (the customer hung up), remote (the agent did) or failed (the line broke or, on Android, the microphone stopped working — with a message) |
failed | The call never connected | A message saying why |
failed messages, for your logs — don't match on them, they may be reworded:
| iOS | Android | |
|---|---|---|
| Microphone | The microphone permission was not granted. | The microphone permission was denied. · …was not granted. (no answer to the prompt within 60 seconds) · The microphone could not start: … |
| Placing the call | The call could not be placed: … | The call could not start: … · The call's foreground service could not start: … |
| The agent | The agent's configuration could not be loaded. | The agent could not be loaded. |
| The line | The conversation did not open within 15 seconds. · The conversation was refused (status). · The line closed before the call connected. | The call could not connect within 15 seconds. · The call was refused (close …). · The call could not be connected. |
| The push | The push was not an agent call this SDK can ring. | — |
The push
| Key | ||
|---|---|---|
type | Required | agent_call, exactly |
call_id | Required | Your id for the call; every outcome carries it back. A push without one never rings |
reason | Optional | Becomes call_reason |
caller_name | Optional | What the ring shows, instead of displayName |
variables | Optional | A JSON object, as a string: more prompt variables. Strings, numbers and booleans are kept; anything else is dropped, and if the string cannot be read at all, it is ignored and the call still rings |
Values other than type are trimmed, and an empty one counts as absent. Every value must be a
string: FCM requires it, and React Native and Flutter turn other values into
strings before handing them over. On iOS, a VoIP push that is not an agent call
is still reported to CallKit and ended at once — iOS requires it — and reported
failed if it carries a call_id.
Prompt variables
| Variable | Value |
|---|---|
who_called | agent for an answered push, customer for startCall. Always set; a variable of the same name cannot replace it |
call_reason | The push's or startCall's reason, when there is one. It replaces a call_reason variable |
| The call's own variables | From the push's variables or startCall's variables. They replace your app's variables of the same name |
| Your app's variables | What your app set (setVariables), read when the call starts |
- Masking. The call's own variables are masked on the phone before they reach
the agent — card numbers, email addresses, phone numbers and other personal
numbers — as screen context is. The
reasonis sent as it is. - Limits. A call carries up to 12 variables in all,
who_calledandcall_reasonincluded, with names up to 40 characters, values up to 200 characters, and about 1 KB together. The call's own come first; your app's are added while they fit, and the rest are left off — keep to the limits so every variable reaches the agent.
Limits and timeouts
| Calls at once | One per device |
| Ring | 30 seconds by default (ringTimeout, ringTimeoutMs); at least 1 second on iOS |
| Opening the line | 15 seconds, then failed |
| Setting a call up over WebRTC | 12 seconds in all, then the call continues on the WebSocket inside the same call |
| A dropped connection | The call ends (ended, failed); it does not move to a new connection. Over WebRTC it gets one chance to recover first, up to about 10 seconds |
| An action's answer | 6 seconds, then the agent hears that the action did not finish in time |
| The microphone prompt during a call | Android: 60 seconds, then failed. iOS: 60 seconds after Answer, then failed; a call the customer places waits for their answer |
| Outcomes kept before a listener exists | 20 (React Native, Flutter) |
Logging
| Platform | How | Where |
|---|---|---|
| iOS | GraineConfig(debug: true) | The Xcode console, subsystem ai.graine.inapp |
| Android | GraineConfig(debug = true) | logcat, tag Graine |
| React Native, Flutter | debug: true in the calls options | The Xcode console and logcat |
Warnings and errors are always logged. Audio, the conversation's messages and variable values are never logged.

